SpringBoot中的定时任务
SpringBoot中的定时任务主要通过@Scheduled注解以及SchedulingConfigurer接口实现。@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Repeatable(Schedules.class) public @interface Scheduled { String cron() default ""; long fixedDelay() default -1; long fixedRate() default -1; long initialDelay() default -1; }以上为@Scheduled源码中关键属性,各属性含义如下:
* cron属性可以设置指定时间执行,cron表达式跟linux一样 */ @Scheduled(cron = "0 45 14 ? * *") public void fixTimeExecution() { System.out.println("指定时间 "+dateFormat.format(new Date())+"执行"); }fixedRate: 以固定的频率执行任务,指定两次执行之间的间隔时间(单位是毫秒)。
* fixedRate属性设置每隔固定时间执行 */ @Scheduled(fixedRate = 5000) public void reportCurrentTime() { System.out.println("每隔五秒执行一次" + dateFormat.format(new Date())); }fixedDelay:在每次任务完成后等待一定的时间再进行下一次执行,指定连续执行之间的延迟时间。
* 上一次任务执行完成之后10秒后在执行 */ @Scheduled(fixedDelay = 10000) public void runWithFixedDelay() { System.out.println("指定时间 "+dateFormat.format(new Date())+"执行"); }initialDelay:首次执行前的延迟时间。
* 初始延迟1秒后开始,然后每10秒执行一次 */ @Scheduled(initialDelay=1000, fixedDelay=10000) public void executeWithInitialAndFixedDelay() { System.out.println("指定时间 "+dateFormat.format(new Date())+"执行"); }这里要注意fixedRate与fixedDelay的区别:fixedRate是基于任务开始执行的时间点来计算下一次任务开始执行的时间,因此任务的执行时间间隔是相对固定的,不受到任务执行时间的影响。如果指定的时间间隔小于任务执行的实际时间,则任务可能会并发执行。而fixedDelay是基于任务执行完成的时间点来计算下一次任务开始执行的时间,因此任务的执行时间间隔是相对不规则的,受到任务执行时间的影响。
@Configuration @EnableScheduling public class ScheduledTaskConfig { }或者
@EnableScheduling @SpringBootApplication public class SpringBootBaseApplication { public static void main(String[] args) { SpringApplication.run(SpringBootBaseApplication.class, args); } }在SpringBoot应用程序中,除了在代码中使用注解配置定时任务外,还可以通过配置文件来配置定时任务的执行规则。这种方式更加灵活,可以在不修改源代码的情况下,动态调整定时任务的执行规则。比如我们在application.properties中配置@Scheduled的属性:
custom.scheduled.cron = 0/5 * * * * ? custom.scheduled.fixedRate=5000 custom.scheduled.fixedDelay=10000 custom.scheduled.initialDelay=1000然后在@Scheduled的方法使用属性配置定时任务执行频率。
@Service public class DemoScheduledTaskService { private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); /** * fixedRate属性设置每隔固定时间执行 */ @Scheduled(fixedRateString = "${custom.scheduled.fixedRate}") public void reportCurrentTime() { System.out.println("每隔五秒执行一次" + dateFormat.format(new Date())); } /** * cron属性可以设置指定时间执行,cron表达式跟linux一样 */ @Scheduled(cron = "${custom.scheduled.cron}") public void fixTimeExecution() { System.out.println("指定时间 "+dateFormat.format(new Date())+"执行"); } /** * 上一次任务执行完成之后10秒后在执行 */ @Scheduled(fixedDelayString = "${custom.scheduled.fixedDelay}") public void runWithFixedDelay() { System.out.println("指定时间 "+dateFormat.format(new Date())+"执行"); } /** * 初始延迟1秒后开始,然后每10秒执行一次 */ @Scheduled(initialDelayString = "${custom.scheduled.initialDelay}", fixedDelayString = "${custom.scheduled.fixedDelay}") public void executeWithInitialAndFixedDelay() { System.out.println("指定时间 "+dateFormat.format(new Date())+"执行"); } }注意,这里使用属性来指定任务执行频率时,要通过@Scheduled的fixedRateString、fixedDelayString、initialDelayString三个可以指定字符串的值的属性去指定,效果等同于long类型的属性。
通过配置文件配置定时任务具有很高的灵活性,可以在不重新编译和部署应用程序的情况下,随时调整定时任务的执行规则。同时,也可以根据不同的环境(例如开发、测试、生产)配置不同的定时任务规则,以满足不同环境下的需求。这种方式可以有效地解耦定时任务的配置和业务代码,提高系统的灵活性和可维护性。
虽然 @Scheduled 注解是一个方便的方式来定义定时任务,但它也存在一些弊端。因为任务的执行计划(如cron表达式)在编译时被硬编码,因此无法在运行时动态修改,除非重新部署。此外,@Scheduled注解对于配置不同的调度策略(如使用不同的线程池)显得力不从心,而且默认情况下,@Scheduled任务在单线程环境下执行,可能出现任务堆积的情况,尤其在任务量大或任务执行时间长的情况下,而且这些任务可能会变得混乱和难以管理。定时任务的配置分散在各个任务方法中,不利于统一管理和维护。对于需要根据动态条件创建或销毁定时任务的情况,@Scheduled注解也无法满足需求。
为了解决这些问题,可以使用SchedulingConfigurer接口来动态地创建和管理定时任务。通过实现 SchedulingConfigurer 接口,我们可以编写代码来动态地注册和管理定时任务,从而实现灵活的任务调度需求。接下来,我们将介绍如何使用SchedulingConfigurer接口来创建定时任务。
@Configuration @EnableScheduling public class CustomSchedulingConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { // 定时任务逻辑 } }通过实现SchedulingConfigurer接口,重写configureTasks方法,自定义任务调度器的配置。此外我们还可以配置线程池,用于控制定时任务执行时的线程数量、并发性等参数。
@Bean(destroyMethod = "shutdown") public ThreadPoolTaskScheduler threadPoolTaskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); // 设置线程池大小 scheduler.setThreadNamePrefix("scheduled-task-"); // 设置线程名称前缀 scheduler.setAwaitTerminationSeconds(60); // 设置终止等待时间 // 设置处理拒绝执行的任务异常 scheduler.setRejectedExecutionHandler((r, executor) -> log.error("Task rejected", r)); // 处理定时任务执行过程中抛出的未捕获异常 scheduler.setErrorHandler(e -> log.error("Error in scheduled task", e)); return scheduler; }然后将自定义的ThreadPoolTaskScheduler设置到ScheduledTaskRegistrar中去:
@Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { // 定时任务逻辑 taskRegistrar.setTaskScheduler(threadPoolTaskScheduler()); }有关线程池的配置参数讲解,请移步:通过SchedulingConfigurer接口,可以更灵活地配置任务调度器和定时任务的执行规则,比如动态注册定时任务、动态修改任务执行规则等。
@Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { ThreadPoolTaskScheduler scheduler = threadPoolTaskScheduler(); taskRegistrar.setTaskScheduler(scheduler); List<CronTaskInfo> tasksFromDB = listTasksFromDatabase(); for (CronTaskInfo task : tasksFromDB) { Runnable taskRunner = new MyTaskExecutor(task.getTaskData()); CronTrigger cronTrigger = new CronTrigger(task.getCronExpression()); scheduler.schedule(taskRunner, cronTrigger); } }关于这里在应用运行时,动态的添加新的任务,我们可以通过事件驱动,轮训检查,消息队列等多种方式,监听到数据库中或者配置文件中新增任务信息,然后通过SchedulingConfigurer接口动态创建定时任务。而这种方式是@Scheduled注解做不到的。
// 假设我们有一个方法用于获取更新后的任务信息 CronTaskInfo updatedTask = getUpdatedTaskInfoFromDatabase(); // 取消旧的任务(需要知道旧任务的TriggerKey) TriggerKey triggerKey = ...; // 获取旧任务的TriggerKey scheduler.unschedule(triggerKey); // 创建新任务并设置新的Cron表达式 MyTaskExecutor taskExecutor = new MyTaskExecutor(updatedTask.getTaskData()); CronTrigger updatedCronTrigger = new CronTrigger(updatedTask.getCronExpression()); // 重新调度新任务 scheduler.schedule(taskRunner, updatedCronTrigger);
另外,我们还可以通过添加任务时对其排序或设置优先级等方式间接实现设置定时任务的执行顺序。通过实现SchedulingConfigurer接口,我们可以拥有对定时任务调度的更多控制权,比如自定义线程池、动态添加任务以及调整任务执行策略。这种灵活性使得在复杂环境下,特别是需要动态管理定时任务时,SchedulingConfigurer成为了理想的选择。