springboot如何配置定时任务
作者:ZY笔记 发布时间:2021-06-22 09:16:12
概述
在Java环境下创建定时任务有多种方式:
使用while循环配合 Thread.sleep(),虽然稍嫌粗陋但也勉强可用
使用 Timer和 TimerTask
使用 ScheduledExecutorService
定时任务框架,如Quartz
在SpringBoot下执行定时任务无非也就这几种方式(主要还是后两种)。只不过SpringBoot做了许多底层的工作,我们只需要做些简单的配置就行了。
通过注解实现定时任务
在SpringBoot中仅通过注解就可以实现常用的定时任务。步骤就两步:
在启动类中添加 @EnableScheduling注解
@EnableScheduling
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
在目标方法中添加 @Scheduled注解,同时在 @Scheduled注解中添加触发定时任务的元数据。
@Scheduled(fixedRate = 1000)
public void job() {
System.out.println(Thread.currentThread().getId() + " ----- job1 ----- " + System.currentTimeMillis());
}
注意: 目标方法需要没有任何参数,并且返回类型为 void 。
这里的定时任务元数据是“fixRate=1000”,意思是固定间隔每1000毫秒即执行一次该任务。
再来看几个 @Schedule注解的参数:
fixedRate:设置定时任务执行的时间间隔,该值为当前任务启动时间与下次任务启动时间之差;
fixedDelay:设置定时任务执行的时间间隔,该值为当前任务结束时间与下次任务启动时间之差;
cron:通过cron表达式来设置定时任务启动时间,在Cron Generator网站可以直接生成cron表达式。
这样创建的定时任务存在一个问题:如存在多个定时任务,这些任务会同步执行,也就是说所有的定时任务都是在一个线程中执行。
再添几个定时任务来执行下看看:
@Scheduled(fixedRate = 1000)
public void job1() {
System.out.println(Thread.currentThread().getId() + " ----- job1 ----- " + System.currentTimeMillis());
}
@Scheduled(fixedRate = 1000)
public void job2() {
System.out.println(Thread.currentThread().getId() + " ----- job2 ----- " + System.currentTimeMillis());
}
@Scheduled(fixedRate = 1000)
public void job3() {
System.out.println(Thread.currentThread().getId() + " ----- job3 ----- " + System.currentTimeMillis());
}
代码中一共创建了三个定时任务,每个定时任务的执行间隔都是1000毫秒,在任务体中输出了执行任务的线程ID和执行时间。
看下执行结果:
20 ----- job3 ----- 1573120568263
20 ----- job1 ----- 1573120568263
20 ----- job2 ----- 1573120568263
20 ----- job3 ----- 1573120569264
20 ----- job1 ----- 1573120569264
20 ----- job2 ----- 1573120569264
20 ----- job3 ----- 1573120570263
20 ----- job1 ----- 1573120570263
20 ----- job2 ----- 1573120570263
可以看到这三个定时任务的执行有如下的特点:
所有的定时任务每次都是在同一个线程上执行;
虽然未必是job1第一个开始执行,但是每批任务的执行次序是固定的——这是由fixRate参数决定的
这样的定时任务已经能够覆盖绝大部分的使用场景了,但是它的缺点也很明显:前面的任务执行时间过长必然会影响之后的任务的执行。为了解决这个问题,我们需要异步执行定时任务。接下来的部分我们将主要着眼于如何实现异步执行定时任务。
通过@Async注解实现异步定时任务
最常用的方式是使用 @Async注解来实现异步执行定时任务。启用 @Async注解的步骤如下:
在启动类中添加 @EnableAsync注解:
@EnableAsync
@EnableScheduling
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
在定时任务方法上添加 @Async注解
@Async
@Scheduled(fixedRate = 1000)
public void job1() {
System.out.println(Thread.currentThread().getId() + " ----- job1 ----- " + System.currentTimeMillis());
}
我们为前面的三个定时任务都加上 @Async注解再运行看看:
25 ----- job1 ----- 1573121781415
24 ----- job3 ----- 1573121781415
26 ----- job2 ----- 1573121781415
30 ----- job3 ----- 1573121782298
31 ----- job1 ----- 1573121782299
32 ----- job2 ----- 1573121782299
25 ----- job2 ----- 1573121783304
35 ----- job3 ----- 1573121783306
36 ----- job1 ----- 1573121783306
通过输出信息可以看到每个定时任务都在不同的线程上执行,彼此的执行次序和执行时间也互不影响,说明配置为异步执行已经成功。
通过配置实现异步定时任务
现在我们有必要稍稍深入了解下springboot定时任务的执行机制了。
springboot的定时任务主要涉及到两个接口: TaskScheduler和 TaskExecutor。在springboot的默认定时任务实现中,这两个接口的实现类是 ThreadPoolTaskScheduler和 ThreadPoolTaskExecutor。
ThreadPoolTaskScheduler负责实现任务的定时执行机制,而 ThreadPoolTaskExecutor则负责实现任务的异步执行机制。二者中, ThreadPoolTaskScheduler执行栈更偏底层一些。
尽管在职责上有些区别,但是两者在底层上都是依赖java的线程池机制实现的: ThreadPoolTaskScheduler依赖的底层线程池是 ScheduledExecutorService,springboot默认为其提供的coreSize是1,所以默认的定时任务都是在一个线程中执行; ThreadPoolTaskExecutor依赖的底层线程池是 ThreadPoolExecutor,springboot默认为其提供的corePoolSize是8。
说到这里应该清楚了:我们可以不添加 @Async注解,仅通过调整 ThreadPoolTaskScheduler依赖的线程池的coreSize也能实现多线程异步执行;同样的,即使添加了 @Async注解,将 ThreadPoolTaskExecutor依赖的线程池的corePoolSize设置为1,那定时任务还是只能在一个线程上同步执行。看下springboot的相关配置项:
spring:
task:
scheduling:
pool:
size: 1
execution:
pool:
core-size: 2
其中spring.task.scheduling是 ThreadPoolTaskScheduler的线程池配置项,spring.task.execution是 ThreadPoolExecutor的线程池配置项。
再稍稍扩展下: @Async注解的value属性就是用来指明使用的 TaskExecutor实例的。默认值是空字符串,表示使用的是springboot自启动的 TaskExecutor实例。如有需要,也可以使用自定义的 TaskExecutor实例,如下:
/**
* 配置线程池
* @return
*/
@Bean(name = "scheduledPoolTaskExecutor")
public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(20);
taskExecutor.setMaxPoolSize(200);
taskExecutor.setQueueCapacity(25);
taskExecutor.setKeepAliveSeconds(200);
taskExecutor.setThreadNamePrefix("my-task-executor-");
// 线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy;默认为后者
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//调度器shutdown被调用时等待当前被调度的任务完成
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
//等待时长
taskExecutor.setAwaitTerminationSeconds(60);
taskExecutor.initialize();
return taskExecutor;
}
此外,还有一种做法是通过提供自定义的 TaskScheduler Bean实例来实现异步执行。要提供提供自定义的 TaskScheduler 实例,可以直接通过 @Bean注解声明创建,也可以在 SchedulingConfigurer接口中配置。这些在后面我们会提到。
调用SpringBoot接口实现定时任务
有时候会需要将定时任务的定时元数据写在数据库或其他配置中心以便统一维护。这种情况就不是通过注解能够搞定的了,此时我们需要使用springboot定时任务一些组件来自行编程实现。常用的组件包括 TaskScheduler、 Triger接口和 SchedulingConfigurer接口。
注意:因为我们用到了springboot的定时任务组件,所以仍然需要在启动类上添加 @EnableScheduling注解。
Trigger接口
Trigger接口主要用来设置定时元数据。要通过程序实现定时任务就不能不用到这个接口。这个接口有两个实现类:
PeriodicTrigger用来配置固定时长的定时元数据
CronTrigger用来配置cron表达式定时元数据
使用TaskScheduler接口
TaskScheduler接口前面我们提过,这个接口需要配合 Trigger接口一起使用来实现定时任务,看个例子:
@Autowired
private TaskScheduler taskScheduler;
public void job() {
int fixRate = 10;
taskScheduler.schedule(() -> System.out.println(" job4 ----- " + System.currentTimeMillis()),
new PeriodicTrigger(fixRate, TimeUnit.SECONDS));
}
在上面的代码里,我们使用 @Autowired注解获取了springbootr容器里默认的 TaskScheduler实例,然后通过 PeriodicTrigger设置了定时元数据,定时任务的任务体则是一个 Runable接口的实现(在这里只是输出一行信息)。
因为默认的 TaskScheduler实例的线程池coreSize是1,所以如有多个并发任务,这些任务的执行仍然是同步的。要调整为异步可以在配置文件中配置,也可以通过提供一个自定义的 TaskScheduler实例来设置:
@Bean("taskScheduler")
public TaskScheduler taskExecutor() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
executor.setPoolSize(20);
executor.setThreadNamePrefix("my-task-scheduler");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//调度器shutdown被调用时等待当前被调度的任务完成
executor.setWaitForTasksToCompleteOnShutdown(true);
//等待时长
executor.setAwaitTerminationSeconds(60);
return executor;
}
使用SchedulingConfigurer接口
SchedulingConfigurer接口的主要用处是注册基于 Trigger接口自定义实现的定时任务。
在实现 SchedulingConfigurer接口后,通常还需要使用 @Configuration注解(当然启动类上的 @EnableScheduling注解也不能少)来声明它实现类。
这个接口唯一的一个方法就是configureTasks,字面意思是配置定时任务。这个方法最重要的参数是一个 ScheduledTaskRegistrar定时任务注册类实例,该类有8个方法,允许我们以不同的方式注册定时任务。
简单做了个实现:
@Configuration
public class MyTaskConfigurer implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar
.addCronTask(
() -> System.out.println(Thread.currentThread().getId() + " --- job5 ----- " + System.currentTimeMillis()),
"0/1 * * * * ?"
);
taskRegistrar
.addFixedDelayTask(
() -> System.out.println(Thread.currentThread().getId() + " --- job6 ----- " + System.currentTimeMillis()),
1000
);
taskRegistrar
.addFixedRateTask(
() -> System.out.println(Thread.currentThread().getId() + " --- job7 ----- " + System.currentTimeMillis()),
1000
);
}
}
这里我们只使用了三种注册任务的方法,分别尝试注册了fixDelay、fixRate以及cron触发的定时任务。
springboot会自动启动注册的定时任务。看下执行结果:
22 --- job7 ----- 1573613616349
22 --- job6 ----- 1573613616350
22 --- job5 ----- 1573613617001
22 --- job7 ----- 1573613617352
22 --- job6 ----- 1573613617353
22 --- job5 ----- 1573613618065
22 --- job7 ----- 1573613618350
22 --- job6 ----- 1573613618355
22 --- job5 ----- 1573613619002
在执行结果中可以看到这里的任务也是在单一线程同步执行的。要设置为异步执行也简单,因为 SchedulingConfigurer接口的另一个作用就是为定时任务提供自定义的 TaskScheduler实例。来看下:
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setThreadNamePrefix("my-task-scheduler");
scheduler.setPoolSize(10);
scheduler.initialize();
taskRegistrar.setTaskScheduler(scheduler);
}
在这里,我将之前注册的定时任务去掉了,目的是想验证下这里的配置是否对注解实现的定时任务有效。经检验是可行的。当然对在configureTasks方法中配置的定时任务肯定也是有效的。我就不一一贴结果了。
另外,需要注意:如 SchedulingConfigurer接口实例已经注入,将无法再获取到springboot默认提供的 TaskScheduler接口实例。
通过Quartz实现定时任务
Quartz是一个非常强大的定时任务管理框架。短短的一篇文章未必能介绍清楚Quartz的全部用法。所以这里只是简单地演示下如何在springboot中是如何使用Quartz的。更多的用法建议优先参考Quartz官方文档。
在spring-boot-web 2.0及之后的版本,已经自动集成了quartz,如果不使用spring-boot-web或使用较早的版本的话我们还需要加一些依赖:
<!-- quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
</dependency>
<!-- spring集成quartz -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<!-- SchedulerFactoryBean依赖了tx包中的PlatformTransactionManager类,因为quartz的分布式功能是基于数据库完成的 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
添加完成这些依赖后,springboot服务在启动时也会自启动内部的quartz。事实上springboot已经为我们准备好了几乎全部的quartz的配置。我们要做的只是把自定义的任务填进去。
首先我们需要创建一个Job实例,来实现Job的具体行为。
@Component
public class MyQuartzJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) {
JobDataMap map = context.getMergedJobDataMap();
// 从作业上下文中取出Key
String key = map.getString("key");
System.out.println(Thread.currentThread().getId() + " -- job8 ---------------------->>>>" + key);
}
}
QuartzJobBean是Spring提供的Quartz Job抽象类。在实现这个类的时候我们可以获取注入到spring中的其他Bean。
配置Job
@Configuration
public class QuartzConfig implements InitializingBean {
@Autowired
private SchedulerFactoryBean schedulerFactoryBean;
@Override
public void afterPropertiesSet() throws Exception {
config();
}
private void config() throws SchedulerException {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
JobDetail jobDetail = buildJobDetail();
Trigger trigger = buildJobTrigger(jobDetail);
scheduler.scheduleJob(jobDetail, trigger);
}
private JobDetail buildJobDetail() {
// 用来存储交互信息
JobDataMap dataMap = new JobDataMap();
dataMap.put("key", "zhyea.com");
return JobBuilder.newJob(MyQuartzJob.class)
.withIdentity(UUID.randomUUID().toString(), "chobit-job")
.usingJobData(dataMap)
.build();
}
private Trigger buildJobTrigger(JobDetail jobDetail) {
return TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withIdentity(jobDetail.getKey().getName(), "chobit-trigger")
.withSchedule(CronScheduleBuilder.cronSchedule("0/1 * * * * ?"))
.build();
}
}
在创建 QuartzConfig类的时候实现了 InitializingBean接口,目的是在 QuartzConfig实例及依赖类都完成注入后可以立即执行配置组装操作。
这里面有几个关键接口需要说明下:
SchedulerFactoryBean,Quartz Scheduler工厂类,springboot自动化配置实现;
Scheduer,负责Quartz Job调度,可从工厂类实例获取;
JobDetail,执行Quartz Job封装;
Trigger,完成Quartz Job启动。
还可以在配置文件中添加Quartz的配置:
spring:
quartz:
startupDelay: 180000 #这里是毫秒值
这里配置了让Quartz默认延迟启动3分钟。
看下执行结果:
30 -- job8 ---------------------->>>>zhyea.com
31 -- job8 ---------------------->>>>zhyea.com
32 -- job8 ---------------------->>>>zhyea.com
33 -- job8 ---------------------->>>>zhyea.com
34 -- job8 ---------------------->>>>zhyea.com
...
好了,就这些内容了。前面用到的程序都上传到了GITHUB,有需要可以参考下。
参考文档
Spring Task Execution and Scheduling
Scheduling Tasks
SpringBoot Quartz Scheduler
Spring Boot Quartz Scheduler Example: Building an Email Scheduling app
Quartz Scheduler Tutorials
来源:http://www.zhyea.com/2019/11/14/spring-boot-scheduling-executor.html


猜你喜欢
- 对变量延迟初始化Kotlin语言有许多特性,包括变量不可变,变量不可为空,等等。这些特性都是为了尽可能地保证程序安全而设计的,但是有些时候这
- 一、创建线程启动线程—start 方法通过覆写 run 方法创建⼀个线程对象,但线程对象被创建出来并不意味着线程就开始运
- 本文实例为大家分享了android实现手机截屏并保存截图功能的具体代码,供大家参考,具体内容如下一、准备一张图片拷贝screenshot_p
- 本文实例为大家分享了Android实现记住账号密码的具体代码,供大家参考,具体内容如下布局一个复选框<CheckBox
- 分部类(Partial Class)在C#2.0引入,分部方法(Partial Method)在C#3.0引入,这两个语法特性都具有相同的特
- 面试题1:说一下你对ReentrantLock的理解?ReentrantLock是JDK1.5引入的,它拥有与synchronized相同的
- 之前在学习RecyclerView的时候,建立了一个可以滑动的View列表,但是当滑动距离过长的时候,需要手动返回到顶部,于是加了一个一键返
- 1、配置maven环境变量,将maven安装的bin⽬录添加到path路径中(此电脑->属性->高级系统设置->环境变量-
- 目录1.Groovy特性2.核心涉及3.Java与Groovy转换第一步:引入Groovy依赖第二步:创建interface接口声明方法第三
- shiro是一个权限框架,具体的使用可以查看其官网 http://shiro.apache.org/ 它提供了很方便的权限认证和
- 本文实例展示了DevExpress实现GridView当无数据行时提示消息的方法,具体步骤如下:主要功能代码部分如下:/// <sum
- 在搜索引擎的开发中,我们需要对Html进行解析。本文介绍C#解析HTML的两种方法。AD: 在搜索引擎的开发中,我们需要对网页的Html内容
- 本文实例讲述了Java 线程的生命周期。分享给大家供大家参考,具体如下:一 代码/*** @Title: ThreadStatus.java
- Spring框架是由于软件开发的复杂性而创建的。Spring使用的是基本的JavaBean来完成以前只可能由EJB完成的事情。然而,Spri
- 当你在开发flutter应用的时候,有时会需要调用native的api,往往遇到flutter并没有相应的package, 这时候flutt
- 前面关于spring Boot的文章已经介绍了很多了,但是一直都没有涉及到数据库的操作问题,数据库操作当然也是我们在开发中无法回避的问题,那
- 本文实例分析了C#中var关键字用法。分享给大家供大家参考。具体方法如下:C#关键字是伴随着.NET 3.5以后,伴随着匿名函数、LINQ而
- 1. Mybatis JdbcType与Oracle、MySql数据类型对应列表MybatisJdbcTypeOracleMySqlJdbc
- 一:串口通信简介前段时间因为工作需要研究了一下android的串口通信,网上有很多讲串口通信的文章,我在做的时候也参考了很多文章,现在就将我
- 结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。所以,标准C中的结构体是不允许包含成员函数的,当然C++中的结构体对此进行了扩展