在开发程序中总是避免不了一些周期性定时任务,比如定时同步数据库、定时发送邮件、定时初始化数据等等。
那么在springboot中如何使用定时任务呢? 本文将由浅入深逐渐揭开定时任务的“神秘面纱”。
本文知识点:
springboot中如何使用Scheduler
springboot中如何动态修改Scheduler的执行时间(cron)
springboot中如何实现多线程并行任务
想了解springboot中的Scheduler?看懂本文妥妥的了~!
如何使用Scheduler?
使用@EnableScheduling启用定时任务
使用@Scheduled编写相关定时任务
开启定时任务
在程序中添加@EnableScheduling注解即可启用Spring的定时任务功能,这类似于Spring的XML中的
@SpringBootApplication
@EnableScheduling
public class ScheduleApplaction {
public static void main(String[] args) throws Exception {
SpringApplication.run(ScheduleApplaction.class, args);
}
}
关于@Scheduled
通过查看Scheduled注解类的源码可知该注解支持如下几种方式使用定时任务
cron()
fixedDelay()
fixedDelayString()
fixedRate()
fixedRateString()
initialDelay()
initialDelayString()
注:由于xx和xxString方法只是参数类型不同用法一致,所以本文对xxString表达式方法不做解释。感兴趣的同学可以去查阅下ScheduledAnnotationBeanPostProcessor类的processScheduled方法。
cron
这个也是定时任务中最常用的一种调度方式,主要在于它的功能强大,使用方便。
支持cron语法,与linux中的crontab表达式类似,不过Java中的cron支持到了秒。
表达式规则如下:
{秒} {分} {时} {日} {月} {周} {年(可选)}
这儿有很多朋友记不住,或者和crontab的表达式记混。
Tips:linux的crontab表达式为:{分} {时} {日} {月} {周}
其实这儿也没有很好的记忆方式,采用最简单的方式:死记硬背即可(( ╯╰ ))。
cron各选项的取值范围及解释:
项取值范围null备注
秒0-59否参考①
分0-59否参考①
时0-23否参考①
日1-31否参考②
月1-12或JAN-DEC否参考①
周1-7或SUN-SAT否参考③
年1970-2099是参考①
参考①
“*” 每隔1单位时间触发;
"," 在指定单位时间触发,比如"10,20"代表10单位时间、20单位时间时触发任务
"-" 在指定的范围内触发,比如"5-30"代表从5单位时间到30单位时间时每隔1单位时间触发一次
"/" 触发步进(step),"/“前面的值代表初始值(”“等同"0”),后面的值代表偏移量,比如"0/25"或者"/25"代表从0单位时间开始,每隔25单位时间触发1次;"10-45/20"代表在[10-45]单位时间内每隔20单位时间触发一次
参考②
“* , - /” 这四个用法同①
"?" 与{周}互斥,即意味着若明确指定{周}触发,则表示{日}无意义,以免引起 冲突和混乱;
“L” 如果{日}占位符如果是"L",即意味着当月的最后一天触发
"W "意味着在本月内离当天最近的工作日触发,所谓最近工作日,即当天到工作日的前后最短距离,如果当天即为工作日,则距离为0;所谓本月内的说法,就是不能跨月取到最近工作日,即使前/后月份的最后一天/第一天确实满足最近工作日;因此,"LW"则意味着本月的最后一个工作日触发,"W"强烈依赖{月}
“C” 根据日历触发,由于使用较少,暂时不做解释
参考③
“* , - /” 这四个用法同①
"?" 与{日}互斥,即意味着若明确指定{日}触发,则表示{周}无意义,以免引起冲突和混乱
"L" 如果{周}占位符如果是"L",即意味着星期的的最后一天触发,即星期六触发,L= 7或者 L = SAT,因此,“5L"意味着一个月的最后一个星期四触发
”#" 用来指定具体的周数,"#“前面代表星期,”#"后面代表本月第几周,比如"2#2"表示本月第二周的星期一,"5#3"表示本月第三周的星期四,因此,“5L"这种形式只不过是”#“的特殊形式而已
"C” 根据日历触发,由于使用较少,暂时不做解释
其他关于cron的详细介绍请参考Spring Task 中cron表达式整理记录
/**
* 在11月7号晚上22点的7分到8分之间每隔半分钟(30秒)执行一次任务
*
* @author zhangyd
*/
@Scheduled(cron = "0/30 7-8 22 7 11 ? ")
public void doJobByCron() throws InterruptedException {
int index = integer.incrementAndGet();
System.out.println(String.format("[%s] %s doJobByCron start @ %s", index, Thread.currentThread(), LocalTime.now()));
// 这儿随机睡几秒,方便查看执行效果
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(String.format("[%s] %s doJobByCron end @ %s", index, Thread.currentThread(), LocalTime.now()));
}
查看打印的结果
[1] Thread[pool-1-thread-1,5,main] doJobByCron start @ 22:07:00.004
[1] Thread[pool-1-thread-1,5,main] doJobByCron end @ 22:07:01.004
[2] Thread[pool-1-thread-1,5,main] doJobByCron start @ 22:07:30.001
[2] Thread[pool-1-thread-1,5,main] doJobByCron end @ 22:07:30.001
[3] Thread[pool-1-thread-1,5,main] doJobByCron start @ 22:08:00.001
[3] Thread[pool-1-thread-1,5,main] doJobByCron end @ 22:08:01.002
[4] Thread[pool-1-thread-1,5,main] doJobByCron start @ 22:08:30.002
[4] Thread[pool-1-thread-1,5,main] doJobByCron end @ 22:08:33.002
给大家安利一款在线生成Cron表达式的神器:http://cron.qqe2.com。使用简单,一看就会。
fixedDelay
上一次任务执行完成后,延时固定长度时间执行下一次任务。关键词:上一次任务执行完成后
就如官方文档中所说:在最后一次调用结束到下一次调用开始之间以毫秒为单位进行等待,等待完成后执行下一次任务
Execute the annotated method with a fixed period in milliseconds
between the end of the last invocation and the start of the next.
通过代码可以更好的理解
private AtomicInteger integer = new AtomicInteger(0);
/**
* 上次任务执行完的3秒后再次执行
*
* @author zhangyd
*/
@Scheduled(fixedDelay = 3000)
public void doJobByFixedDelay() throws InterruptedException {
int index = integer.incrementAndGet();
System.out.println(String.format("[%s] %s doJobByFixedDelay start @ %s", index, Thread.currentThread(), LocalTime.now()));
// 这儿随机睡几秒,方便查看执行效果
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
System.out.println(String.format("[%s] %s doJobByFixedDelay end @ %s", index, Thread.currentThread(), LocalTime.now()));
}
查看控制台输出
[1] Thread[pool-1-thread-1,5,main] doJobByFixedDelay start @ 18:28:03.235
[1] Thread[pool-1-thread-1,5,main] doJobByFixedDelay end @ 18:28:06.236
[2] Thread[pool-1-thread-1,5,main] doJobByFixedDelay start @ 18:28:09.238
[2] Thread[pool-1-thread-1,5,main] doJobByFixedDelay end @ 18:28:10.238
[3] Thread[pool-1-thread-1,5,main] doJobByFixedDelay start @ 18:28:13.240
[3] Thread[pool-1-thread-1,5,main] doJobByFixedDelay end @ 18:28:21.240
可以看到不管每个任务实际需要执行多长时间,该任务再次执行的时机永远都是在上一次任务执行完后,延时固定长度时间后执行下一次任务。
fixedRate
官方文档中已经很明白的说明了此方法的用法。
Execute the annotated method with a fixed period in milliseconds between invocations.
按照固定的速率执行任务,无论之前的任务是否执行完毕。关键词:不管前面的任务是否执行完毕
private AtomicInteger integer = new AtomicInteger(0);
/**
* 固定每3秒执行一次
*
* @author zhangyd
*/
@Scheduled(fixedRate = 3000)
public void doJobByFixedRate() throws InterruptedException {
int index = integer.incrementAndGet();
System.out.println(String.format("[%s] %s doJobByFixedRate start @ %s", index, Thread.currentThread(), LocalTime.now()));
// 这儿随机睡几秒,方便查看执行效果
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
System.out.println(String.format("[%s] %s doJobByFixedRate end @ %s", index, Thread.currentThread(), LocalTime.now()));
}
查看控制台打印的结果
[1] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:23.520
[1] Thread[pool-1-thread-1,5,main] doJobByFixedRate end @ 18:57:24.521
[2] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:26.515
[2] Thread[pool-1-thread-1,5,main] doJobByFixedRate end @ 18:57:31.516
[3] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:31.516
[3] Thread[pool-1-thread-1,5,main] doJobByFixedRate end @ 18:57:31.516
[4] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:32.515
[4] Thread[pool-1-thread-1,5,main] doJobByFixedRate end @ 18:57:39.516
[5] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:39.516
[5] Thread[pool-1-thread-1,5,main] doJobByFixedRate end @ 18:57:40.517
[6] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:40.517
[6] Thread[pool-1-thread-1,5,main] doJobByFixedRate end @ 18:57:41.517
[7] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:41.517
[7] Thread[pool-1-thread-1,5,main] doJobByFixedRate end @ 18:57:47.519
这儿需要重点关注每个任务的起始时间和下一次任务的起始时间。如果说我们不看这个结果,直接就代码来说,我们会认为,程序每隔3秒执行一次,第二次任务和第一次任务的间隔时间是3秒,没毛病。但是我们通过上面打印的日志发现,程序并不是按照我们预想的流程执行的。
注:为方便阅读,任务命名为T+任务的编号,比如T1表示第一个任务,以此类推。
从上面结果看,T1起始时间到T2起始时间之间间隔了3秒,T2起始时间到T3起始时间间隔了5秒,T3起始时间到T4起始时间间隔了1秒。
为什么会有这种现象呢?这正如上面所说:“按照固定的延迟时间执行任务,无论之前的任务是否执行完毕”
那么按照这种规则,咱们预先估计下每次任务的执行时间就能解释上面的问题。T1起始时间是23秒,T2起始时间是26,以此类推,T3理论上是29,T4是32,T5是35,T6是38,T7是41…
这种机制也可以称为“任务编排”,也就是说从fixedRate任务开始的那一刻,它后续的任务执行的时间已经被预先编排好了。如果定时任务执行的时间大于fixedRate指定的延迟时间,则定时任务会在上一个任务结束后立即执行;如果定时任务执行的时间小于fixedRate指定的延迟时间,则定时任务会在上一个任务结束后等到预编排的时间时执行。
initialDelay
该条指令主要是用于配合fixedRate和fixedDelay使用的,作用是在fixedRate或fixedDelay任务第一次执行之前要延迟的毫秒数,说白了:它的作用就是在程序启动后,并不会立即执行该定时任务,它将在延迟一段时间后才会执行该定时任务
Number of milliseconds to delay before the first execution of a fixedRate() or fixedDelay() task.
本例为了方便测试,需要首先知道这个定时任务的bean在什么时候装载完成,或者说这个定时任务的bean什么时候初始化完成,所以,需要处理一下代码。
@Component
public class AppSchedulingConfigurer implements ApplicationListener
private AtomicInteger integer = new AtomicInteger(0);
/**
* 第一次延迟5秒后执行,之后按fixedRate的规则每3秒执行一次
*
* @author zhangyd
*/
@Scheduled(initialDelay = 5000, fixedRate = 3000)
public void doJobByInitialDelay() throws InterruptedException {
int index = integer.incrementAndGet();
System.out.println(String.format("[%s] %s doJobByInitialDelay start @ %s", index, Thread.currentThread(), LocalTime.now()));
// 这儿随机睡几秒,方便查看执行效果
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(String.format("[%s] %s doJobByInitialDelay end @ %s", index, Thread.currentThread(), LocalTime.now()));
}
/**
* Spring容器初始化完成
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
System.out.println(String.format("springboot上下文context准备完毕时. %s start @ %s", Thread.currentThread(), LocalTime.now()));
}
}
如上,使用Spring的事件监听应用启动的时间,以此打印出initialDelay应该从什么时候开始等待。
(关于Spring和Springboot的事件,本文不做深入介绍,后期会专文讲解这一块内容)
查看打印结果
springboot上下文context准备完毕时. Thread[main,5,main] start @ 21:18:42.092
[1] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:47.023
[1] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end @ 21:18:51.024
[2] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:51.024
[2] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end @ 21:18:53.025
[3] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:53.025
[3] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end @ 21:18:57.025
[4] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:57.025
[4] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end @ 21:18:57.025
[5] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:59.025
[5] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end @ 21:19:00.025
由此可以明显看出,initialDelay确实是在bean上下文准备完毕(容器已初始化完成)时延迟了5秒钟后执行的fixedRate任务。
如何动态修改执行时间(cron)?
实现动态配置cron的好处就是可以随时修改任务的执行表达式而不用重启服务。
想实现这种功能,我们只需要实现SchedulingConfigurer接口重写configureTasks接口即可。
@Component
public class DynamicScheduledConfigurer implements SchedulingConfigurer {
// 默认每秒执行一次定时任务
private String cron = "0/1 * * * * ?";
private AtomicInteger integer = new AtomicInteger(0);
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(() -> {
int index = integer.incrementAndGet();
System.out.println(String.format("[%s] %s 动态定时任务 start @ %s", index, Thread.currentThread(), LocalTime.now()));
}, triggerContext -> {
CronTrigger trigger = new CronTrigger(this.getCron());
return trigger.nextExecutionTime(triggerContext);
});
}
public String getCron() {
return cron;
}
public void setCron(String cron) {
System.out.println(String.format("%s Cron已修改!修改前:%s,修改后:%s @ %s", Thread.currentThread(), this.cron, cron, LocalTime.now()));
this.cron = cron;
}
}
例子中默认的cron表示每秒执行一次定时任务,我们要做的就是动态修改这个执行时间。
接下来编写一个controller负责处理cron以及预编排5组该cron将要执行的时间节点
@SpringBootApplication
@Controller
@EnableScheduling
public class ScheduleApplaction {
@Autowired
DynamicScheduledConfigurer dynamicScheduledConfigurer;
public static void main(String[] args) {
SpringApplication.run(ScheduleApplaction.class, args);
}
@RequestMapping("/")
public String index() {
return "index";
}
/**
* 修改动态定时任务的cron值
*/
@RequestMapping("/updateTask")
@ResponseBody
public void updateTask(String cron) {
dynamicScheduledConfigurer.setCron(cron);
}
/**
* 预解析5次该cron将要执行的时间节点
*
* @param cron 带解析的cron
* @return
* @throws IOException
*/
@RequestMapping("/parseCron")
@ResponseBody
public List
String urlNameString = "http://cron.qqe2.com/CalcRunTime.ashx?CronExpression=" + URLEncoder.encode(cron, "UTF-8");
URL realUrl = new URL(urlNameString);
URLConnection connection = realUrl.openConnection();
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36");
connection.connect();
StringBuilder result = new StringBuilder();
try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
String line;
while ((line = in.readLine()) != null) {
result.append(line);
}
} catch (Exception e) {
e.printStackTrace();
}
return JSONArray.parseArray(result.toString(), String.class);
}
}
html页面
- 最近5次运行的时间
$(function () {
$("#submitBtn").click(function () {
var cron = $("#cron").val();
$.post("/updateTask", {cron: cron});
$.getJSON("/parseCron?cron=" + encodeURIComponent(cron), function (rs) {
var html = '';
$.each(rs, function (i, v) {
html += '
});
var $parserBox = $("#parserBox");
$parserBox.children("dd").remove();
$parserBox.append(html);
})
});
})
启动项目后先查看控制台输出,默认应该是每秒执行一次定时任务。
[1] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:00:23.010
[2] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:00:24.001
[3] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:00:25.001
[4] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:00:26.002
接下来修改一下默认cron,如下图,将定时任务修改为每5秒执行一次
[56] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:18.002
[57] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:19.001
Thread[http-nio-8080-exec-7,5,main] Cron已修改!修改前:0/1 * * * * ?,修改后:0/5 * * * * ? @ 14:01:19.963
[58] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:20.001
[59] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:25.002
[60] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:30.002
[61] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:35.002
[62] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:40.001
[63] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:45.002
ok,到此为止就实现了动态修改定时任务的功能。
如何实现多线程并行任务
通过上面几个例子,可能细心的朋友已经发现了一个规律:上面所有的定时任务都是单线程的。
其实也正式如此,如官方文档中解释的:
By default, will be searching for an associated scheduler definition:
either a unique TaskScheduler bean in the context, or a TaskScheduler
bean named “taskScheduler” otherwise; the same lookup will also be
performed for a ScheduledExecutorService bean. If neither of the two is
resolvable, a local single-threaded default scheduler will be created
and used within the registrar.
大概意思就是:默认情况下会检索是否指定了一个自定义的TaskScheduler,如果没有的情况下,会创建并使用一个本地单线程的任务调度器(线程池)。这一点,可以在ScheduledTaskRegistrar类(定时任务注册类)中加以佐证:
public void afterPropertiesSet() {
this.scheduleTasks();
}
protected void scheduleTasks() {
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
// ...
}
那么我们如何将调度器改造成多线程形式的呢?继续向下看文档
When more control is desired, a @Configuration class may implement
SchedulingConfigurer. This allows access to the underlying
ScheduledTaskRegistrar instance.
意思很明了,我们可以通过实现SchedulingConfigurer接口,然后通过ScheduledTaskRegistrar类去注册自定义的线程池。在SchedulingConfigurer类中已经提供了一个setScheduler方法用来注册自定义的Scheduler Bean
public void setScheduler(Object scheduler) {
Assert.notNull(scheduler, "Scheduler object must not be null");
if (scheduler instanceof TaskScheduler) {
this.taskScheduler = (TaskScheduler)scheduler;
} else {
if (!(scheduler instanceof ScheduledExecutorService)) {
throw new IllegalArgumentException("Unsupported scheduler type: " + scheduler.getClass());
}
this.taskScheduler = new ConcurrentTaskScheduler((ScheduledExecutorService)scheduler);
}
}
接下来我们就按照这种方式扩展一个SchedulingConfigurer,代码如下:
@Component
public class MultiThreadSchedulingConfigurer implements SchedulingConfigurer {
private AtomicInteger integer = new AtomicInteger(0);
@Scheduled(cron = "0/1 * * * * ?")
public void multiThread() {
System.out.println(String.format("[1] %s exec @ %s", Thread.currentThread().getName(), LocalTime.now()));
}
@Scheduled(cron = "0/1 * * * * ?")
public void multiThread2() {
System.out.println(String.format("[2] %s exec @ %s", Thread.currentThread().getName(), LocalTime.now()));
}
@Scheduled(cron = "0/1 * * * * ?")
public void multiThread3() {
System.out.println(String.format("[3] %s exec @ %s", Thread.currentThread().getName(), LocalTime.now()));
}
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.setScheduler(newExecutors());
}
@Bean(destroyMethod = "shutdown")
private Executor newExecutors() {
return Executors.newScheduledThreadPool(10, r -> new Thread(r, String.format("ZHYD-Task-%s", integer.incrementAndGet())));
}
}
如代码所示,我们自定义一个容量为10得线程池,并且自定义一下线程池中线程得命名格式,同时为了方便查看,我们同时开启三个定时任务,在同样的时间同时执行,以此来观察定时器在并行执行时的具体线程分配情况
注意在上面的例子中自定义线程池时,使用了@Bean注解的(destroyMethod=“shutdown”)属性,这个属性可确保在Spring应用程序上下文本身关闭时正确关闭定时任务。
Note in the example above the use of @Bean(destroyMethod=“shutdown”).
This ensures that the task executor is properly shut down when the
Spring application context itself is closed.
[2] ZHYD-Task-2 exec @ 15:44:14.008
[1] ZHYD-Task-3 exec @ 15:44:14.008
[3] ZHYD-Task-1 exec @ 15:44:14.008
[2] ZHYD-Task-1 exec @ 15:44:15
[1] ZHYD-Task-3 exec @ 15:44:15
[3] ZHYD-Task-2 exec @ 15:44:15
[1] ZHYD-Task-4 exec @ 15:44:16.001
[3] ZHYD-Task-6 exec @ 15:44:16.001
[2] ZHYD-Task-5 exec @ 15:44:16.001
[1] ZHYD-Task-1 exec @ 15:44:17.002
[2] ZHYD-Task-7 exec @ 15:44:17.002
[3] ZHYD-Task-2 exec @ 15:44:17.002
[1] ZHYD-Task-1 exec @ 15:44:18.001
通过上面的结果可以看出,每个定时任务的执行线程都不在是固定的了。
总结
springboot中通过@EnableScheduling注解开启定时任务,@Scheduled支持cron、fixedDelay、fixedRate、initialDelay四种定时任务的表达式
springboot中通过实现SchedulingConfigurer接口并且重写configureTasks方法实现动态配置cron和多线程并行任务
作者:慕冬雪
链接:http://www.imooc.com/article/259921
来源:慕课网
本文首次发布于慕课网 ,转载请注明出处,谢谢合作