1. 整体设计与思路拆解1.1 为什么选 Quartz定时任务的选型逻辑做后端开发的兄弟应该都有体会业务里总有一堆到点该干的事凌晨同步订单状态、每天定时清理临时文件、每周给用户推送汇总报表、月末结算生成账单。这些需求逃不掉而Spring Boot自带的Scheduled注解虽然能搞定简单场景一旦遇到需要动态调整执行时间、任务需要持久化、多个服务节点要避免重复执行这类情况立刻就显得力不从心。我最早做定时任务也偷懒直接用Spring的Scheduled注解一贴完事。后来项目上线了产品经理隔三差五说这个报表之前凌晨跑现在改成中午12点我每次都要改代码、重新打jar包、部署烦得不行。更坑的是后来服务做了多节点部署同一个任务每个节点都跑一遍数据重复处理事故都出了好几次。这才下决心整体切到Quartz。Quartz是Java生态里最老牌、最可靠的定时任务框架没有之一。它从2004年左右开始发展在Spring还没有定时任务功能的时候Quartz就已经被各大企业广泛使用了。到今天它仍然是很多银行、电商、ERP系统的底层调度引擎。对比起来方案优势劣势SpringScheduled使用简单、无额外依赖无法动态调整不持久化集群下重复执行Quartz功能全支持持久化、集群、动态增删改配置相对复杂学习成本略高XXL-Job自带管理界面分布式调度能力强需要额外部署调度中心重依赖我当时选的思路很明确项目规模还没大到需要引入独立的调度中心但已经出现了动态任务和集群部署的需求Quartz恰好卡在中间既能本地嵌入Spring Boot又能支持JDBC持久化和集群模式。这个均衡让Quartz成了大多数Java中型项目的首选。1.2 Quartz 在 Spring Boot 中的角色定位要理解整合思路先得看清楚Quartz在架构里到底是什么角色。简单说Quartz是一个独立的调度引擎它自己负责三件事记录任务定义、计算触发时间、到点执行任务。而Spring Boot要做的事情是把Quartz的引擎生命周期交给Spring容器管理同时让Quartz执行的任务能使用Spring的Bean比如注入Mapper、Service。这里最容易踩的坑是直接用Quartz原生API也能跑但任务里的new出来的类完全不归Spring管Autowired全是null。所以整合的关键不是引入依赖这么简单而是要把Spring容器和Quartz的Job实例化过程打通。后面我会详细说怎么用委托类实现这个目标。1.3 适合哪些场景与项目阶段从我实操经验看这个整合方案最适合下面几类项目第一类已经有Spring Boot项目内部存量的定时任务开始变多比如超过5个以上管理起来开始混乱。第二类有动态任务需求的业务比如用户在前端配置一个推广活动后台根据活动时间自动上下架这种就必须动态创建和销毁任务。第三类需要多实例部署的系统Quartz的JDBC持久化配合集群锁天然避免重复执行。第四类任务执行结果需要追溯比如跑了没跑、跑成功没有Quartz有完整的JobHistory机制可以查。如果你的需求只是每天凌晨三点清一次临时文件而且永远不需要改时间那用Scheduled就够了别过度设计。但只要有上面任何一种情况Quartz这套整合方案就是值得投入的。2. 核心概念解析Job、Trigger、Scheduler 三件套2.1 Quartz 的核心抽象模型Quartz的设计里有三个核心概念把这仨吃透了整个框架就通了。Job你要执行的业务逻辑本身。比如同步订单状态生成日报都实现一个Job接口在execute方法里写业务代码。但注意Quartz里的Job不是一次性的它是可以被重复触发的每次触发Quartz都会创建Job的一个新实例。所以不要在Job里持有需要跨执行保存的状态这种状态应该放在JobDataMap里。Trigger什么时候触发任务。它描述的是时间规则比如每隔10秒跑一次每周一早上9点跑。最常用的Trigger类型是CronTrigger基于Cron表达式灵活度极高另一种是SimpleTrigger适合固定间隔、固定次数的场景。Scheduler调度器全局的总指挥。它把Job和Trigger组合在一起负责管理所有任务的注册、暂停、恢复、删除以及到了时间以后拉起线程执行Job。一个Scheduler可以挂多个Job和多个Trigger它们是多对多的关系。这三个概念对应到数据库里就是QRTZ_JOB_DETAILS、QRTZ_TRIGGERS、QRTZ_CRON_TRIGGERS这几张表。理解这个映射关系后面排查问题会非常有帮助。2.2 Cron 表达式与 Trigger 的匹配逻辑Cron表达式是Quartz最常用的时间规则描述方式格式是7位秒 分 时 日 月 周 年最后一位年可以省略。我刚接触的时候总记混后来形成了一张速查表位置含义取值范围常用通配符第1位秒0-59*,-/第2位分0-59同上第3位时0-23同上第4位日1-31同上另含?第5位月1-12或JAN-DEC同上第6位周1-7或SUN-SAT同上另含?第7位年1970-2099可省略几个常用例子可以直接抄0 0 3 * * ?每天凌晨3点整0 30 2 * * MON-FRI每周一到周五凌晨2点30分0 0/5 * * * ?每5分钟执行一次0 0 12 */2 * ?每月的偶数日中午12点0 0 9 ? * 1每周日早上9点这里有个容易踩的坑第4位和第6位不能同时指定值。因为月和星期会互相冲突比如你写每月15号又写周一到底是15号跑还是周一跑Quartz要求这两个位置至少一个是?表示不指定。排查线上Cron表达式是否生效时我推荐直接在测试类里写一个CronExpression的校验CronExpression cronExpression new CronExpression(0 0 3 * * ?); System.out.println(cronExpression.getNextValidTimeAfter(new Date()));能打印出下一次执行时间就说明表达式没毛病。调试线上任务时这一步能省下大量等待时间。2.3 JobDataMap往任务里传参数的官方方案上面提到Job实例每次执行都会被重新创建所以要给任务传递这次跑哪个订单ID这批处理哪些用户这类参数就得靠JobDataMap。实操中我有两个习惯一是给Job传参时能用JobDataMap就不要用静态变量或者全局缓存因为Job可能会被多个Trigger共用静态变量会串数据。二是在构造Trigger时设置JobDataMap而不是在Job执行时手动塞这样参数状态更清晰。JobDataMap jobDataMap new JobDataMap(); jobDataMap.put(batchId, BATCH-20250115-001); jobDataMap.put(sourceTable, order_info); CronTrigger trigger TriggerBuilder.newTrigger() .withIdentity(orderSyncTrigger, orderGroup) .usingJobData(jobDataMap) .withSchedule(CronScheduleBuilder.cronSchedule(0 0 3 * * ?)) .build();在Job实现类里通过context.getMergedJobDataMap()就能读到参数。注意这里用getMergedJobDataMap()而不是getJobDataMap()区别在于前者会把Trigger和JobDetail上的JobDataMap合并读取时优先级是Trigger覆盖JobDetail这个细节在任务参数来源复杂时特别重要。3. 整合实操从零到可用的完整配置3.1 环境准备与依赖引入先在pom.xml里添加依赖。我用的是当前最稳定的版本组合Spring Boot 2.7.x配Quartz 2.3.2这是两年多实战验证过的组合几乎没有兼容性坑dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-quartz/artifactId /dependency如果你用的Spring Boot 3.xQuartz已经内置于starter里版本跟随Boot走直接引入starter即可不需要额外指定Quartz的版本号。但请注意spring-boot-starter-quartz包不包含数据库表结构要使用JDBC持久化还需要把官方SQL脚本放进项目里。脚本位于Quartz发行包的docs/db/tables目录下对应不同数据库有不同文件我用的MySQL是tables_mysql_innodb.sql。建议在项目的resources/db/quartz/目录下存放这个脚本并在初始化时执行一次。3.2 application.yml 配置该开什么、该关什么配置文件是整合里最容易出错的地方因为Quartz的配置项分散几个参数搭配不对就出非常难排查的问题。我整理一份经历过生产环境考验的配置spring: quartz: job-store-type: jdbc scheduler-name: cloudTaskScheduler wait-for-jobs-to-complete-on-shutdown: true overwrite-existing-jobs: true jdbc: initialize-schema: never schema: classpath:db/quartz/tables_mysql_innodb.sql properties: org: quartz: scheduler: instanceName: cloudScheduler instanceId: AUTO jobStore: class: org.springframework.scheduling.quartz.LocalDataSourceJobStore driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate tablePrefix: QRTZ_ isClustered: true clusterCheckinInterval: 10000 misfireThreshold: 30000 threadPool: class: org.quartz.simpl.SimpleThreadPool threadCount: 10 threadPriority: 5这里说几个关键参数的含义理解了才不会瞎配job-store-type: jdbc把Job和Trigger持久化到数据库。这是整个整合最重要的开关。如果是memory模式应用一重启所有任务全部失忆前面说的动态任务、集群防重复就都不存在了。isClustered: true开启集群模式。即使你现在只有单机部署也建议打开这个开关。原因很简单Quartz的集群模式要求数据库里有锁表QRTZ_LOCKS以后扩容到多节点只是改配置的事不用换架构。单机开启也没副作用。wait-for-jobs-to-complete-on-shutdown: true应用关闭时等待正在执行的任务跑完再停止。这是个非常实用的保护参数否则线上发版时任务跑到一半被强杀数据就残了。misfireThreshold任务补偿触发阈值单位毫秒。意思是任务到了执行时间但线程池繁忙导致没跑如果在30秒内Quartz会尽快补偿。我把它设为30000避免系统压力大时任务堆积。有几个配置不用加spring.quartz.auto-startup默认就是true不用管spring.quartz.start-delay一般也不用设除非你的服务刚启动时依赖还没就绪。3.3 创建 Job让 Spring 容器接管 Job 实例化整合的第一步难点来了怎么让Job里能注入Spring的Bean。Quartz原生机制是直接new一个Job类Spring的Autowired完全失效。解决方案是写一个SpringBeanJobFactory把ApplicationContext塞进去让Quartz创建实例时走Spring的逻辑。Spring Boot 2.x的spring-boot-starter-quartz已经提供了AutowireCapableBeanJobFactory在配置类里注册给Scheduler即可。完整做法Configuration public class QuartzConfig { Bean public SchedulerFactoryBean schedulerFactoryBean(ApplicationContext applicationContext) { SchedulerFactoryBean factory new SchedulerFactoryBean(); AutowireCapableBeanJobFactory jobFactory new AutowireCapableBeanJobFactory(); jobFactory.setBeanFactory(applicationContext); factory.setJobFactory(jobFactory); return factory; } Bean public Scheduler scheduler(SchedulerFactoryBean factory) throws Exception { return factory.getScheduler(); } }有了这个配置Job实现类里就可以大大方方写Component public class OrderSyncJob implements Job { Autowired private OrderService orderService; Override public void execute(JobExecutionContext context) throws JobExecutionException { String batchId context.getMergedJobDataMap().getString(batchId); orderService.syncOrderStatus(batchId); } }这里有个细节值得注意OrderSyncJob这个类本身要标Component但实现类里不能加Scope(prototype)之类的注解去限制它因为Quartz每次执行都会通过JobFactory创建一个新实例你自己管好Spring的默认单例即可。3.4 注册任务的两种方式任务注册是我实际项目里写的最多的代码。我把常见的方式归纳为两种启动时注册和运行时动态注册。启动时注册适合固定不变的任务在CommandLineRunner里完成Component public class QuartzJobInitializer implements CommandLineRunner { Autowired private Scheduler scheduler; Override public void run(String... args) throws Exception { JobDetail jobDetail JobBuilder.newJob(OrderSyncJob.class) .withIdentity(orderSyncJob, tradeGroup) .storeDurably(true) .build(); CronTrigger trigger TriggerBuilder.newTrigger() .withIdentity(orderSyncTrigger, tradeGroup) .withSchedule(CronScheduleBuilder.cronSchedule(0 0 3 * * ?)) .build(); scheduler.scheduleJob(jobDetail, trigger); } }动态注册适合业务方自主维护任务的场景。比如用户配置了一个促销活动业务要求活动开始时自动把商品改价活动结束时恢复原价。这时候在后台管理接口里动态注册任务Service public class QuartzDynamicTaskService { private final Scheduler scheduler; public void createTask(String jobName, String groupName, String cronExpression, Class? extends Job jobClass, JobDataMap dataMap) { JobDetail jobDetail JobBuilder.newJob(jobClass) .withIdentity(jobName, groupName) .storeDurably(true) .build(); CronTrigger trigger TriggerBuilder.newTrigger() .withIdentity(jobName Trigger, groupName) .usingJobData(dataMap) .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)) .build(); scheduler.scheduleJob(jobDetail, trigger); } public void updateTaskCron(String jobName, String groupName, String newCronExpression) { TriggerKey triggerKey new TriggerKey(jobName Trigger, groupName); CronTrigger newTrigger TriggerBuilder.newTrigger() .withIdentity(triggerKey) .withSchedule(CronScheduleBuilder.cronSchedule(newCronExpression)) .build(); scheduler.rescheduleJob(triggerKey, newTrigger); } public void deleteTask(String jobName, String groupName) { JobKey jobKey new JobKey(jobName, groupName); scheduler.deleteJob(jobKey); } }这个Service对应的接口建议包一层校验任务名不能重复scheduler.checkExists(jobKey)、Cron表达式先解析验证、删除任务前确认不是系统核心任务。我在项目里就靠这个Service接了几个第三方平台的对接需求产品改任务时间不用再反复发版了。3.5 DisallowConcurrentExecution 与错误处理Quartz里有一个策略注解容易被忽略DisallowConcurrentExecution。默认情况下同一个Job如果上一次执行还没结束下一次触发时间到了Quartz会再开一个线程并发执行这个Job。如果任务是同步数据、生成报表这类不适合并发的操作就可能出现数据错乱。我遇到过真实案例凌晨批量任务因为某次数据库响应慢前一个任务没跑完后一个触发又进来了两个线程同时更新同一批订单把状态改得乱七八糟。解决办法很简单在Job类上加上这个注解Component DisallowConcurrentExecution public class OrderSyncJob implements Job { // ... }加上以后Quartz会保证同一个JobDetail的多次执行串行化上一次执行完才会开始下一次。注意这个注解是加在Job实现类上的而不是Trigger上。执行过程中的异常处理我习惯在Job的execute方法里捕获业务异常记录日志然后决定是否抛出JobExecutionException让Quartz尝试处理。当业务异常是可重试的比如下游接口超时会抛出异常让任务标记失败配合后续补偿机制处理。如果是不可重试的比如参数错误就只记录日志不打断调度周期保证后面的任务不受影响。4. 生产环境实用技巧动态任务的完整闭环4.1 任务注册、暂停、恢复、删除的状态机管理Quartz里任务有几种状态NONE不存在、NORMAL正常、PAUSED已暂停、COMPLETE已结束、ERROR错误。整套动态任务管理本质上就是一个状态机的转换。我在实际项目中封装了一套状态管理的工具核心方法是这几个// 暂停任务不影响JobDetailTrigger状态变为PAUSED scheduler.pauseJob(new JobKey(jobName, groupName)); // 恢复任务 scheduler.resumeJob(new JobKey(jobName, groupName)); // 判断任务是否存在 boolean exists scheduler.checkExists(new JobKey(jobName, groupName)); // 查看任务状态 JobKey jobKey new JobKey(jobName, groupName); JobDetail jobDetail scheduler.getJobDetail(jobKey); Trigger.TriggerState state scheduler.getTriggerState(new TriggerKey(jobName Trigger, groupName));其中getTriggerState返回的值有BLOCKED和WAITING等BLOCKED表示任务正在执行中但被DisallowConcurrentExecution限制所以排队等待这个状态在监控大屏上非常有价值能直观看出任务有没有积压。管理界面我会配合接口对外输出当前任务列表任务标识、触发表达式、下次触发时间、上次执行时间、当前状态。Quartz提供了getNextFireTime()和getPreviousFireTime()直接放在DTO里返回给前端产品经理自己就能看到任务执行情况。4.2 基于 MyBatis-Plus 的持久化任务管理Quartz自身的JDBC持久化只是把任务的定义和触发记录存到数据库它不关心你业务层的任务配置。为了让业务方能在界面上增删改查任务我额外建了一张业务表task_config - id - task_name - group_name - description - cron_expression - job_class_name - job_data_map (JSON) - status (ENABLE/DISABLE) - create_time - update_time这张表由业务后台管理业务方修改了Cron表达式后通过Service层同步更新Quartz调度器里的Trigger。这样做的最大好处是Quartz原生表结构复杂直接给运营人员看会疯的业务表结构简单明了而且能关联权限、审批、操作日志等公司内部的管理要求。同步时需要注意一致性更新Quartz成功后再更新业务表如果Quartz更新失败则业务表保持不变并返回错误提示。千万别两边各自为政时间长了数据就对不上了。4.3 与前端管理界面的交互设计如果让我给这个方案配一个前端管理界面我不会做得很复杂核心就一个列表加一个编辑弹窗。列表展示的信息任务名称、分组、描述、Cron表达式、任务类名、下次执行时间、上次执行时间、状态。编辑弹窗支持修改Cron表达式、启用/停用、立即执行一次。立即执行一次这个功能后台实际上调的是scheduler.triggerJob(new JobKey(jobName, groupName));这个方法会绕过Trigger的时间规则直接把Job加入执行队列非常适合业务方在配置完成后想马上验证任务逻辑的场景。部署时我建议给这个操作加上权限控制毕竟立即执行是有副作用的操作比如马上发短信。5. 集群部署与高可用避免多节点重复执行5.1 单机与集群的差异很多项目一开始是单机部署但随着并发上来一定会走向多节点。这时如果用Scheduled每个节点都会执行定时任务重复处理数据是必然的。Quartz的集群模式解决这个问题靠的是数据库锁。它内部有一套锁机制任务调度时各节点尝试获取数据库里的行级锁QRTZ_LOCKS表谁拿到了锁谁就负责在下一个触发周期内执行任务。其他节点只是看客在下一个周期继续竞争。开启集群模式的必要条件有三个isClustered: true、数据库表已初始化、所有节点的instanceName和instanceId唯一配置里instanceId: AUTO即可自动生成。这三条缺一不可。如果节点数比较多还需要注意clusterCheckinInterval参数。这个参数表示节点向数据库报到的间隔默认是15000毫秒。如果某个节点宕机了其他节点要等到超过这个间隔的两倍才会判定它失联对应的任务才会有其他节点接管。所以这个值设置得越短故障转移越快但设置得太短会让数据库压力变大。我实测下来10000毫秒是比较平衡的值。5.2 如果任务必须每个节点都执行怎么办集群模式下Quartz默认是全局唯一执行但有业务就是需要每个节点都跑一遍比如每个节点自己心跳上报、每个节点定时清理本地缓存。这种任务就别走Quartz的集群调度了直接用Spring的Scheduled放行或者单独建一个非集群的Scheduler来管理。我自己项目里的做法就是两个Scheduler并存主调度器处理全局任务用集群模式副调度器处理节点本地任务用memory模式互不干扰。配置两个SchedulerFactoryBean时注意命名避免Bean冲突。5.3 配置数据源时必须避开的坑Quartz的JDBC存储会占用一个数据源连接池。如果项目引用了多数据源比如主库、从库、业务库分开Quartz的表必须和它使用的DataSource绑定在同一套数据库里不能业务库一套、Quartz一套。我踩过一个特别闹心的坑Quartz表初始化时用了主数据源但运行时报找不到QRTZ_LOCKS表。仔细排查才发现我的项目里配置了动态数据源路由Quartz创建连接拿到的是路由后的从库而表却建在主库上。解决办法是在Quartz配置中显式指定一个专用的DataSource哪怕和业务库共用一个连接池不要让它走动态路由。6. 常见问题与排查技巧实录6.1 任务不执行或延迟优先检查这三个地方生产环境里任务没跑是最常见的故障但绝大多数情况不是代码逻辑问题而是环境配置问题。我按排查优先级整理一下第一检查线程池是否被占满了。Quartz的SimpleThreadPool如果threadCount设得太小而任务里又有耗时的阻塞调用比如HTTP调用、数据库大查询线程池耗尽后后续任务全部排队看起来就是任务消失了。排查方法把threadCount调大临时验证或者用jstack看线程状态。第二检查Cron表达式的时区。Quartz默认使用服务器本地时区如果服务器是UTC而业务期望北京时间凌晨3点执行的任务到下午3点才跑。解决办法是在配置CronScheduleBuilder时显式设置时区CronScheduleBuilder.cronSchedule(cron).inTimeZone(TimeZone.getTimeZone(Asia/Shanghai));第三检查misfire策略。如果任务到了触发时间但线程池忙错过之后Quartz的默认策略会等下一个周期。如果业务要求错过必须立即补偿就要在Trigger配置里显式指定withMisfireHandlingInstructionFireAndProceed()。6.2 数据库表被锁导致任务集体停滞Quartz的集群模式依赖QRTZ_LOCKS表这个表里的锁如果长时间不释放整个集群的调度都会瘫痪。我遇到过一次事故数据库出现长时间事务未提交QRTZ_LOCKS表的行锁一直持有所有节点都拿不到锁所有任务全部停摆。排查方法是执行SQLSELECT * FROM information_schema.innodb_trx WHERE trx_state RUNNING;如果发现长时间运行的事务并且它的会话卡在QRTZ_LOCKS相关的SQL上基本可以确认是这个原因。预防手段是给数据库设置合理的事务超时时间并且不要手动在QRTZ_前缀表上执行长事务操作。6.3 任务重复执行除了集群锁还有这些原因虽然集群模式解决了多节点重复执行问题但还有两个情况会导致任务重复跑一是应用重启时任务补偿。如果应用正在执行任务时被强杀重启后Quartz检测到上次执行没有被记录完成会按misfire规则补偿执行一次。如果业务要求幂等性比如任务处理的是已处理和未处理两张表必须在Job代码里做幂等检查。二是Cron表达式本身跨时区的边界问题。比如0 0 0 1 1 ?这样每年1月1日执行的表达式如果业务方在多时区操作可能出现同一批数据被不同时区的节点各处理一遍。解决办法依然是统一时区配置。6.4 动态任务修改后不生效注意 Trigger 重建我遇到过业务方在界面上改了任务的Cron表达式日志里也显示更新成功了但任务还是按旧时间去跑。排查后发现我的更新代码只调用了rescheduleJob但没有处理JobDetail关联的Trigger。Quartz里一个JobDetail可以关联多个Trigger而rescheduleJob(triggerKey, newTrigger)只是替换了一个Trigger如果原逻辑里还有别的Trigger还在生效就会造成怎么改都没用的假象。正确的做法是更新前先getTriggersOfJob(jobKey)把所有关联Trigger都处理一遍或者更新时直接删掉旧Trigger、重新注册新Trigger。我现在封装工具类时统一走的都是删除重建的逻辑最省心。7. 再聊点关于性能和监控的经验任务数量一旦上来比如超过几百个Quartz的调度压力就开始显现。我的经验是JobDetail和Trigger的粒度设计会直接影响性能。如果业务场景相似、执行逻辑相同只是参数不同合并成一个Job类用JobDataMap区分会比创建几百个Job类高效得多。比如我项目里的报表生成任务有日报、周报、月报三个入口它们核心逻辑一样只是聚合时间段不同。我建了三个Trigger指向同一个Job类利用JobDataMap里的参数区分调度器里只有一份Job逻辑管理界面也更清爽。监控方面我用Spring Boot的Actuator暴露健康检查然后在SchedulerListener里监听任务事件public class TaskMonitorListener implements SchedulerListener { Override public void jobScheduled(Trigger trigger) { // 任务注册时记录 } Override public void jobExecuted(JobExecutionContext context, JobExecutionException jobException) { // 记录每次执行结果和耗时 } }把这些事件数据推送到监控系统比如PrometheusGrafana就能看到每个任务的历史执行时长、失败次数。这比出了问题再翻日志高效得多。结尾Quartz这套整合方案我前后在三个项目里实践过从最初的单机内存模式到后来加JDBC持久化最后扩展成多节点集群模式每一步都是被实际需求推着走的。它的代码并不复杂复杂的是在各种边缘场景下集群锁、动态修改、任务积压、故障转移保持调度稳定。我个人最想强调的是先把核心概念吃透再动手写代码。Job、Trigger、Scheduler三件套的边界、持久化对架构的影响、集群模式下幂等的重要性这些理解了后面的代码就是水到渠成的事。最后分享一个小技巧在我参与的团队里每次有人要加一个新的定时任务我都会要求他至少回答三个问题——这个任务需要持久化吗任务在集群环境会被重复执行吗任务执行失败后能接受丢失吗回答清楚这三个问题任务的设计基本不会跑偏。如果你正在做Spring Boot项目并且定时任务开始变多希望这篇实操记录能帮你少走一些弯路。