Spring框架提供定时任务的功能,可以通过@Scheduled注解的方式以及Quartz集成类的方式实现计划任务的开发。所以基于Spring上的SpringBoot也是提供定时任务的功能的。本文主要介绍基于@Scheduled注解开发定时任务,同时介绍多线程以及异步定时任务的开发。
官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#scheduling
在项目中,定时任务的需求是比较常见的,比如定时清除缓存、定时取消未支付订单、定时发送邮件等等。SpringBoot项目中我们可以直接使用@Scheduled注解实现定时任务的功能开发。
在启动类或者任意带@Configuration注解类上添加@EnableScheduling注解开启定时任务。详细介绍参考官方说明。
在方法上添加@Scheduled使其成为定时任务方法。@Scheduled注解中有几个比较常用的参数fixedRate,fixedDelay,initialDelay,cron。
示例中我们我们例举了使用不同参数设置定时任务的执行时间。同时也提供定时任务如何使用application.yml配置的时间的示例。官方也给我们提供了一个比较简单的示例,可以参考官方示例。
@Configuration
@EnableScheduling
public class AppTask {
/**
* 使用fixedDelay的任务
* fixedDelay表示从上次任务完成之后间隔多久执行
*
* @author flyduck
* @date 2020/12/28 21:53
* @param []
* @return void
*/
@Scheduled(fixedDelay = 1000)
public void taskA() {
System.out.println(Thread.currentThread().getName() + " Task A run ..." + new Date().toString());
}
/**
* 使用fixedRate的任务
* fixedRate表示从上次任务开始后间隔多久执行
*
* @author flyduck
* @date 2020/12/28 21:54
* @param []
* @return void
*/
@Scheduled(fixedRate = 1000)
public void taskB() {
System.out.println(Thread.currentThread().getName() + " Task B run ..." + new Date().toString());
}
/**
* 使用cron表达式的任务
* Cron表达式文档:https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#scheduling-cron-expression
*
* @author flyduck
* @date 2020/12/28 21:51
* @param []
* @return void
*/
@Scheduled(cron = "*/1 * * * * *")
public void taskC() {
System.out.println(Thread.currentThread().getName() + " Task C run ..." + new Date().toString());
}
/**
* 使用fixedRateString的任务
* fixedRateString示从上次任务开始后间隔多久执行,从配置中获取fixedRateString的值,如果没有配置则设置为1000
*
* @author flyduck
* @date 2020/12/28 22:04
* @param []
* @return void
*/
@Scheduled(fixedRateString = "${project.task.fixed-rate:1000}")
public void taskD() {
System.out.println(Thread.currentThread().getName() + " Task D run ..." + new Date().toString());
}
}
taskD()定时任务中fixedRateString是从项目的配置中读取project.task.fixed-rate的值,如果没有配置则设置为1000毫秒。application.yml配置片段如下:
project:
task:
fixed-rate: 1000
SpringBoot中默认的定时任务线程池只配置了一个线程,即默认情况下定时任务是单线程执行的。通过@Scheduled注解定义多个定时任务,默认情况下是串行的执行。
实际情况中,单线程串行执行多个计划任务可能会存在一些问题。比如有些定时任务执行的时间超过了另外的定时任务设置运行的时间,会存在定时任务不会按照设置的时间执行的情况。比如下列情况,会导致task2()定时任务每3秒执行一次,而不是设置的每2秒执行一次。
@Scheduled(fixedDelay = 1000)
public void task01() throws InterruptedException {
System.out.println("task01");
Thread.sleep(2000);
}
@Scheduled(fixedDelay = 2000)
public void task02() {
System.out.println("task02");
}
SpringBoot定时任务默认是由是单个线程串行调度所有任务,当定时任务很多的时候,为了提高任务执行效率,避免任务之间互相影响,可以配置多线程并行的方式执行定时任务。
@Configuration
@EnableScheduling
public class AppTask {
/**
* 自定义任务执行线程池
* 默认的线程池为1个线程 https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#scheduling-task-namespace-scheduler
*
* @author flyduck
* @date 2020/12/28 22:15
* @param []
* @return org.springframework.scheduling.TaskScheduler
*/
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
// 配置线程池大小,根据任务数量定制
taskScheduler.setPoolSize(10);
// 线程名称前缀
taskScheduler.setThreadNamePrefix("flyduck-task-scheduler-thread-");
// 线程池关闭前最大等待时间,确保最后一定关闭
taskScheduler.setAwaitTerminationSeconds(60);
// 线程池关闭时等待所有任务完成
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
// 线程池对拒绝任务的处理策略
taskScheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
return taskScheduler;
}
}
这里我们通过@Configuration注解的配置类中提供@Bean注解的TaskScheduler配置执行定时任务的多线程的线程池。这样就开启了多线程的定时任务。
SpringBoot定时任务默认是由是单个线程串行调度所有任务,当定时任务很多的时候,为了提高任务执行效率,避免任务之间互相影响,也可以异步调用定时任务的方式达到并行执行任务。
在启动类或者任意带@Configuration注解类上添加@EnableAsync注解开启定时任务。详细介绍参考官方说明。
在方法上添加@Async使其成为异步调用方法。在带@Scheduled注解的定时任务方法上再加上@Async注解使其成为异步调用的定时任务方法。
@Configuration
@EnableScheduling
@EnableAsync
public class AppTask {
/**
* 使用cron表达式的异步任务
* Cron表达式文档:https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#scheduling-cron-expression
* 从配置中获取cron的值,如果没有配置则设置为 *\/10 * * * * *
*
* @author flyduck
* @date 2020/12/28 22:06
* @param []
* @return void
*/
@Scheduled(cron = "${project.task.cron:*/10 * * * * *}")
@Async
public void taskE() {
System.out.println(Thread.currentThread().getName() + " Task E run ..." + new Date().toString());
}
}
SpringBoot默认是SimpleAsyncTaskExecutor调度执行异步方法,不重用任何线程,为每个调用启动一个新线程。我们可以自定义线程池来调度异步方法。用自定义的线程池来取代默认线程管理方式,无疑是一个更加安全和灵活的方式,可以避免大量的线程阻塞带来的系统崩溃。
@Configuration
@EnableScheduling
@EnableAsync
public class AppTask {
/**
* 自定义异步任务线程池
* 官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#scheduling-task-namespace-executor
*
* @author flyduck
* @date 2020/12/28 22:22
* @param []
* @return java.util.concurrent.Executor
*/
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 配置核心线程数
executor.setCorePoolSize(10);
// 配置最大线程数
executor.setMaxPoolSize(20);
// 配置缓存队列大小
executor.setQueueCapacity(100);
// 空闲线程存活时间
executor.setKeepAliveSeconds(15);
// 线程名称前缀
executor.setThreadNamePrefix("flyduck-task-executor-thread-");
// 线程池对拒绝任务的处理策略:这里采用了CallerRunsPolicy策略,当线程池没有处理能力的时候,该策略会直接在execute方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是被没有完成的任务阻塞
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
}
这里我们通过@Configuration注解的配置类中提供@Bean注解的TaskExecutor配置异步调用的线程池,实现自定义线程池替换默认的线程管理方式来调度异步方法。
这里将SpringBoot项目中定时任务的开发做了一个总结,希望其它网友开发定时任务的时候,能够通过此文获得一些帮助和参考。本文中的示例已经上传到码云上,源码地址。
[1] Getting Started | Scheduling Tasks
[2] Integration
[3] [springboot] 基于Spring Task实现定时任务