阅读完需:约 19 分钟
在 Spring + SpringMVC 环境中,一般来说,要实现定时任务,我们有两中方案,一种是使用 Spring 自带的定时任务处理器 @Scheduled 注解,另一种就是使用第三方框架 Quartz ,Spring Boot 源自 Spring+SpringMVC ,因此天然具备这两个 Spring 中的定时任务实现策略,当然也支持 Quartz,本文我们就来看下 Spring Boot 中两种定时任务的实现方式。
1.@Scheduled
使用 @Scheduled 非常容易,直接创建一个 Spring Boot 项目,并且添加 web 依赖 spring-boot-starter-web
,项目创建成功后,添加 @EnableScheduling
注解,开启定时任务:
@SpringBootApplication
@EnableScheduling
public class ScheduledApplication {
public static void main(String[] args) {
SpringApplication.run(ScheduledApplication.class, args);
}
}
接下来配置定时任务:
@Scheduled(fixedRate = 2000)
public void fixedRate() {
System.out.println("fixedRate>>>"+new Date());
}
@Scheduled(fixedDelay = 2000)
public void fixedDelay() {
System.out.println("fixedDelay>>>"+new Date());
}
@Scheduled(initialDelay = 2000,fixedDelay = 2000)
public void initialDelay() {
System.out.println("initialDelay>>>"+new Date());
}
- 首先使用 @Scheduled 注解开启一个定时任务。
- fixedRate 表示任务执行之间的时间间隔,具体是指两次任务的开始时间间隔,即第二次任务开始时,第一次任务可能还没结束。
- fixedDelay 表示任务执行之间的时间间隔,具体是指本次任务结束到下次任务开始之间的时间间隔。
- initialDelay 表示首次任务启动的延迟时间。
- 所有时间的单位都是毫秒。
上面这是一个基本用法,除了这几个基本属性之外,@Scheduled 注解也支持 cron 表达式,使用 cron 表达式,可以非常丰富的描述定时任务的时间。cron 表达式格式如下:
[秒] [分] [小时] [日] [月] [周] [年]

这一块需要大家注意的是,月份中的日期和星期可能会起冲突,因此在配置时这两个得有一个是 ?
通配符含义:
-
?
表示不指定值,即不关心某个字段的取值时使用。需要注意的是,月份中的日期和星期可能会起冲突,因此在配置时这两个得有一个是?
-
*
表示所有值,例如:在秒的字段上设置*
,表示每一秒都会触发 -
,
用来分开多个值,例如在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五触发 -
-
表示区间,例如在秒上设置 “10-12”,表示 10,11,12秒都会触发 -
/
用于递增触发,如在秒上面设置”5/15” 表示从5秒开始,每增15秒触发(5,20,35,50) -
#
序号(表示每月的第几个周几),例如在周字段上设置”6#3”表示在每月的第三个周六,(用 在母亲节和父亲节再合适不过了) - 周字段的设置,若使用英文字母是不区分大小写的 ,即 MON 与mon相同
-
L
表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会自动判断是否是润年), 在周字段上表示星期六,相当于”7”或”SAT”(注意周日算是第一天)。如果在”L”前加上数字,则表示该数据的最后一个。例如在周字段上设置”6L”这样的格式,则表示”本月最后一个星期五” -
W
表示离指定日期的最近工作日(周一至周五),例如在日字段上设置”15W”,表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发,如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 “1W”,它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,”W”前只能设置具体的数字,不允许区间”-“) -
L
和W
可以一组合使用。如果在日字段上设置”LW”,则表示在本月的最后一个工作日触发(一般指发工资 )
例如,在 @Scheduled 注解中来一个简单的 cron 表达式,每隔5秒触发一次,如下:
@Scheduled(cron = "0/5 * * * * *")
public void cron() {
System.out.println(new Date());
}
常用的表达式
(1)0/2 * * * * ? 表示每2秒 执行任务
(1)0 0/2 * * * ? 表示每2分钟 执行任务
(1)0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务
(2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业
(3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作
(4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
(5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
(6)0 0 12 ? * WED 表示每个星期三中午12点
(7)0 0 12 * * ? 每天中午12点触发
(8)0 15 10 ? * * 每天上午10:15触发
(9)0 15 10 * * ? 每天上午10:15触发
(10)0 15 10 * * ? 每天上午10:15触发
(11)0 15 10 * * ? 2005 2005年的每天上午10:15触发
(12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
(13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
(14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
(15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
(16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
(17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
(18)0 15 10 15 * ? 每月15日上午10:15触发
(19)0 15 10 L * ? 每月最后一日的上午10:15触发
(20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
(21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
(22)0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
我们来详细了解下cron表达式
http://www.bejson.com/othertools/cron/
上面介绍的是使用 @Scheduled 注解的方式来实现定时任务,接下来我们再来看看如何使用 Quartz 实现定时任务。
尽管使用@Scheduled注解配置定时任务是十分方便的,但是由于其设置是在注解中,因此如果需要改变当前任务的定时规划,则需要重新启动项目才可以生效。为了解决这种不能及时变更的问题,我们可以使用实现接口的方式来完成定时任务的创建。
数据库中新增任务规则表和数据
在本地数据库中新增一个task表,用来记录定时任务的规则信息
CREATE TABLE `task` (
`id` int NOT NULL PRIMARY KEY,
`cron` varchar(30) NOT NULL
);
INSERT INTO `task` VALUES (1, '* * * ? * ?');
由上述sql语句中的'* * * ? * ?'
值可以看出,我们添加数据的定时任务规则是每秒执行一次。 随后在SpringBoot项目的配置文件中配置数据源信息:
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource..password=123456
创建定时器类
准备好数据并配置数据源信息后,我们定义一个定时器类,并在该类上使用@EnableScheduling注解开启定时任务。
@Configuration
@EnableScheduling
public class TaskDemo implements SchedulingConfigurer {
@Mapper
public interface CronMapper {
@Select("select cron from cron limit 1")
public String getCron();
}
@Autowired
@SuppressWarnings("all")
CronMapper cronMapper;
/**
* 执行定时任务.
*/
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(
() -> System.out.println("执行动态定时任务: " + LocalDateTime.now().toLocalTime()),
triggerContext -> {
String cron = cronMapper.getCron();
if (StringUtils.isEmpty(cron)) {
}
return new CronTrigger(cron).nextExecutionTime(triggerContext);
}
);
}
}
配置完成后,我们启动项目,发现定时任务按照设定的每秒执行输出,而如果我们修改数据库中cron字段对应的值为'0/5 * * ? * ?'
时,定时任务的规则自动发生了变化。如果最终修改的值不符合cron表达式规范,那么定时器类会报错停止执行。
2.Quartz
一般在项目中,除非定时任务涉及到的业务实在是太简单,使用 @Scheduled 注解来解决定时任务,否则大部分情况可能都是使用 Quartz 来做定时任务。在 Spring Boot 中使用 Quartz ,只需要在创建项目时,添加 Quartz 依赖即可:

项目创建完成后,也需要添加开启定时任务的注解:
@SpringBootApplication
@EnableScheduling
public class QuartzApplication {
public static void main(String[] args) {
SpringApplication.run(QuartzApplication.class, args);
}
}
Quartz 在使用过程中,有两个关键概念,一个是JobDetail(要做的事情),另一个是触发器(什么时候做),要定义 JobDetail,需要先定义 Job,Job 的定义有两种方式:
第一种方式,直接定义一个Bean:
@Component
public class MyJob1 {
public void sayHello() {
System.out.println("MyJob1>>>"+new Date());
}
}
关于这种定义方式说两点:
- 首先将这个 Job 注册到 Spring 容器中。
- 这种定义方式有一个缺陷,就是无法传参。
第二种定义方式,则是继承 QuartzJobBean
并实现默认的方法:
public class MyJob2 extends QuartzJobBean {
HelloService helloService;
public HelloService getHelloService() {
return helloService;
}
public void setHelloService(HelloService helloService) {
this.helloService = helloService;
}
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
helloService.sayHello();
}
}
public class HelloService {
public void sayHello() {
System.out.println("hello service >>>"+new Date());
}
}
和第1种方式相比,这种方式支持传参,任务启动时,executeInternal 方法将会被执行。
Job 有了之后,接下来创建类,配置 JobDetail 和 Trigger 触发器,如下:
@Configuration
public class QuartzConfig {
@Bean
MethodInvokingJobDetailFactoryBean methodInvokingJobDetailFactoryBean() {
MethodInvokingJobDetailFactoryBean bean = new MethodInvokingJobDetailFactoryBean();
bean.setTargetBeanName("myJob1");
bean.setTargetMethod("sayHello");
return bean;
}
@Bean
JobDetailFactoryBean jobDetailFactoryBean() {
JobDetailFactoryBean bean = new JobDetailFactoryBean();
bean.setJobClass(MyJob2.class);
JobDataMap map = new JobDataMap();
map.put("helloService", helloService());
bean.setJobDataMap(map);
return bean;
}
@Bean
SimpleTriggerFactoryBean simpleTriggerFactoryBean() {
SimpleTriggerFactoryBean bean = new SimpleTriggerFactoryBean();
bean.setStartTime(new Date());
bean.setRepeatCount(5);
bean.setJobDetail(methodInvokingJobDetailFactoryBean().getObject());
bean.setRepeatInterval(3000);
return bean;
}
@Bean
CronTriggerFactoryBean cronTrigger() {
CronTriggerFactoryBean bean = new CronTriggerFactoryBean();
bean.setCronExpression("0/10 * * * * ?");
bean.setJobDetail(jobDetailFactoryBean().getObject());
return bean;
}
@Bean
SchedulerFactoryBean schedulerFactoryBean() {
SchedulerFactoryBean bean = new SchedulerFactoryBean();
bean.setTriggers(cronTrigger().getObject(), simpleTriggerFactoryBean().getObject());
return bean;
}
@Bean
HelloService helloService() {
return new HelloService();
}
}
关于这个配置说如下几点:
- JobDetail 的配置有两种方式:MethodInvokingJobDetailFactoryBean 和 JobDetailFactoryBean 。
- 使用 MethodInvokingJobDetailFactoryBean 可以配置目标 Bean 的名字和目标方法的名字,这种方式不支持传参。
- 使用 JobDetailFactoryBean 可以配置 JobDetail ,任务类继承自 QuartzJobBean ,这种方式支持传参,将参数封装在 JobDataMap 中进行传递。
- Trigger 是指触发器,Quartz 中定义了多个触发器,这里向大家展示其中两种的用法,SimpleTrigger 和 CronTrigger 。
- SimpleTrigger 有点类似于前面说的 @Scheduled 的基本用法。
- CronTrigger 则有点类似于 @Scheduled 中 cron 表达式的用法。

全部定义完成后,启动 Spring Boot 项目就可以看到定时任务的执行了。
总结
这里主要向大家展示了 Spring Boot 中整合两种定时任务的方法,整合成功之后,剩下的用法基本上就和在 SSM 中使用一致了
异步任务
1、创建一个service包
2、创建一个类AsyncService
异步处理还是非常常用的,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务。
编写方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况;
@Service
public class AsyncService {
public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("业务进行中....");
}
}
3、编写controller包
4、编写AsyncController类
我们去写一个Controller测试一下
@RestController
public class AsyncController {
@Autowired
AsyncService asyncService;
@GetMapping("/hello")
public String hello(){
asyncService.hello();
return "success";
}
}
5、访问http://localhost:8080/hello进行测试,3秒后出现success,这是同步等待的情况。
问题:我们如果想让用户直接得到消息,就在后台使用多线程的方式进行处理即可,但是每次都需要自己手动去编写多线程的实现的话,太麻烦了,我们只需要用一个简单的办法,在我们的方法上加一个简单的注解即可,如下:
6、给hello方法添加@Async注解;
//告诉Spring这是一个异步方法
@Async
public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("业务进行中....");
}
SpringBoot就会自己开一个线程池,进行调用!但是要让这个注解生效,我们还需要在主程序上添加一个注解@EnableAsync ,开启异步注解功能;
@EnableAsync //开启异步注解功能
@SpringBootApplication
public class SpringbootTaskApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTaskApplication.class, args);
}
}
7、重启测试,网页瞬间响应,后台代码依旧执行!
@Async调用线程池,推荐使用自定义线程池的模式。自定义线程池常用方案:重新实现接口AsyncConfigurer。
Spring 已经实现的线程池
-
SimpleAsyncTaskExecutor
:不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程。 -
SyncTaskExecutor
:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。 -
ConcurrentTaskExecutor
:Executor
的适配类,不推荐使用。如果ThreadPoolTaskExecutor
不满足要求时,才用考虑使用这个类。 -
SimpleThreadPoolTaskExecutor
:是Quartz
的SimpleThreadPool
的类。线程池同时被quartz
和非quartz
使用,才需要使用此类。 -
ThreadPoolTaskExecutor
:最常使用,推荐。 其实质是对java.util.concurrent.ThreadPoolExecutor
的包装。
@Async应用默认线程池
Spring应用默认的线程池,指在@Async注解在使用时,不指定线程池的名称。查看源码,@Async的默认线程池为SimpleAsyncTaskExecutor
。
在线程池应用中,参考阿里巴巴java开发规范:线程池不允许使用Executors
去创建,不允许使用系统默认的线程池,推荐通过ThreadPoolExecutor
的方式,这样的处理方式让开发的工程师更加明确线程池的运行规则,规避资源耗尽的风险。Executors各个方法的弊端:
-
newFixedThreadPool
和newSingleThreadExecutor
:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。 -
newCachedThreadPool
和newScheduledThreadPool
:要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
@Async默认异步配置使用的是SimpleAsyncTaskExecutor
,该线程池默认来一个任务创建一个线程,若系统中不断的创建线程,最终会导致系统占用内存过高,引发OutOfMemoryError错误。
针对线程创建问题,SimpleAsyncTaskExecutor
提供了限流机制,通过concurrencyLimit
属性来控制开关,当concurrencyLimit>=0时开启限流机制,默认关闭限流机制即concurrencyLimit=-1,当关闭情况下,会不断创建新的线程来处理任务。基于默认配置,SimpleAsyncTaskExecutor
并不是严格意义的线程池,达不到线程复用的功能。
@Async应用自定义线程池
自定义线程池,可对系统中线程池更加细粒度的控制,方便调整线程池大小配置,线程执行异常控制和处理。在设置系统自定义线程池代替默认线程池时,虽可通过多种模式设置,但替换默认线程池最终产生的线程池有且只能设置一个(不能设置多个类继承AsyncConfigurer)。自定义线程池有如下模式:
- 重新实现接口AsyncConfigurer
- 继承AsyncConfigurerSupport
- 配置由自定义的TaskExecutor替代内置的任务执行器
通过查看Spring源码关于@Async的默认调用规则,会优先查询源码中实现AsyncConfigurer
这个接口的类,实现这个接口的类为AsyncConfigurerSupport
。但默认配置的线程池和异步处理方法均为空,所以,无论是继承或者重新实现接口,都需指定一个线程池。且重新实现 public Executor getAsyncExecutor()
方法。
实现接口AsyncConfigurer
@Configuration
public class AsyncConfiguration implements AsyncConfigurer {
@Bean("kingAsyncExecutor")
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
int corePoolSize = 10;
executor.setCorePoolSize(corePoolSize);
int maxPoolSize = 50;
executor.setMaxPoolSize(maxPoolSize);
int queueCapacity = 10;
executor.setQueueCapacity(queueCapacity);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
String threadNamePrefix = "kingDeeAsyncExecutor-";
executor.setThreadNamePrefix(threadNamePrefix);
executor.setWaitForTasksToCompleteOnShutdown(true);
// 使用自定义的跨线程的请求级别线程工厂类19 int awaitTerminationSeconds = 5;
executor.setAwaitTerminationSeconds(awaitTerminationSeconds);
executor.initialize();
return executor;
}
@Override
public Executor getAsyncExecutor() {
return executor();
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> ErrorLogger.getInstance().log(String.format("执行异步任务'%s'", method), ex);
}
}
继承AsyncConfigurerSupport
@Configuration
@EnableAsync
class SpringAsyncConfigurer extends AsyncConfigurerSupport {
@Bean
public ThreadPoolTaskExecutor asyncExecutor() {
ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
threadPool.setCorePoolSize(3);
threadPool.setMaxPoolSize(3);
threadPool.setWaitForTasksToCompleteOnShutdown(true);
threadPool.setAwaitTerminationSeconds(60 * 15);
return threadPool;
}
@Override
public Executor getAsyncExecutor() {
return asyncExecutor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> ErrorLogger.getInstance().log(String.format("执行异步任务'%s'", method), ex);
}
}
配置自定义的TaskExecutor
由于AsyncConfigurer的默认线程池在源码中为空,Spring通过beanFactory.getBean(TaskExecutor.class)
先查看是否有线程池,未配置时,通过beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class)
,又查询是否存在默认名称为TaskExecutor
的线程池。所以可在项目中,定义名称为TaskExecutor
的bean生成一个默认线程池。也可不指定线程池的名称,申明一个线程池,本身底层是基于TaskExecutor.class
便可。
比如:
Executor.class:ThreadPoolExecutorAdapter->ThreadPoolExecutor->AbstractExecutorService->ExecutorService->Executor(这样的模式,最终底层为Executor.class,在替换默认的线程池时,需设置默认的线程池名称为TaskExecutor)
TaskExecutor.class:ThreadPoolTaskExecutor->SchedulingTaskExecutor->AsyncTaskExecutor->TaskExecutor(这样的模式,最终底层为TaskExecutor.class,在替换默认的线程池时,可不指定线程池名称。)
@EnableAsync
@Configuration
public class TaskPoolConfig {
@Bean(name = AsyncExecutionAspectSupport.DEFAULT_TASK_EXECUTOR_BEAN_NAME)
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程池大小
executor.setCorePoolSize(10);
//最大线程数
executor.setMaxPoolSize(20);
//队列容量
executor.setQueueCapacity(200);
//活跃时间
executor.setKeepAliveSeconds(60);
//线程名字前缀
executor.setThreadNamePrefix("taskExecutor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
@Bean(name = "new_task")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程池大小
executor.setCorePoolSize(10);
//最大线程数
executor.setMaxPoolSize(20);
//队列容量
executor.setQueueCapacity(200);
//活跃时间
executor.setKeepAliveSeconds(60);
//线程名字前缀
executor.setThreadNamePrefix("taskExecutor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
多个线程池
@Async注解,使用系统默认或者自定义的线程池(代替默认线程池)。可在项目中设置多个线程池,在异步调用时,指明需要调用的线程池名称,如@Async(“new_task”)。