SpringBoot动态定时任务开发指南
一般情况下,如果想在Spring Boot中使用定时任务,我们只需要@EnableScheduling开启定时任务支持,在需要调度的方法上添加@Scheduled注解。这样就能够在项目中开启定时调度功能了,并且这种方法支持通过cron表达式灵活的控制执行周期和频率。
上述的方式好处是快捷,轻量,缺点是周期一旦指定,想要更改必须要重启应用,如果我们想要动态的对定时任务的执行周期进行变更,甚至动态的增加定时调度任务则上述方式就不适用了。
本文我将讲解如何在Spring 定时任务的基础上进行扩展,实现动态定时任务。
需求
- 动态增加定时任务
- 热更新定时任务的执行周期(动态更新cron表达式)
方案1:仅实现动态变更任务周期
首先介绍的方案1能够实现动态变更已有任务的执行频率/周期。
首先建立一个Spring Boot应用,这里不再展开。
建立一个任务调度类,实现接口SchedulingConfigurer,标记为Spring的一个bean。注意一定要添加注解 @EnableScheduling 开启定时任务支持。
@EnableScheduling
@Component
public class DynamicCronHandler implements SchedulingConfigurer {
private static final String DEFAULT_CRON = "0/5 * * * * ?";
private String taskCron = DEFAULT_CRON;
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.addTriggerTask(()->{
LOGGER.info("执行任务");
}, triggerContext -> {
// 刷新cron
CronTrigger cronTrigger = new CronTrigger(taskCron);
Date nextExecDate = cronTrigger.nextExecutionTime(triggerContext);
return nextExecDate;
});
}
scheduledTaskRegistrar.addTriggerTask接受两个参数,分别为需要调度的任务实例(Runnable实例),Trigger实例,这里通过lambda方式注入,需要实现nextExecutionTime回调方法,返回下次执行时间。
通过该回调方法,在Runnable中执行业务逻辑代码,在Trigger修改定时任务的执行周期。
public DynamicCronHandler setTaskImplement(Runnable taskImplement) {
this.taskImplement = taskImplement;
return this;
}
public DynamicCronHandler setTaskCron(String taskCron) {
this.taskCron = taskCron;
return this;
}
public DynamicCronHandler taskCron(String taskCron) {
System.out.println("更新cron=" + taskCron);
this.taskCron = taskCron;
return this;
}
...省略getter...
}
编写一个测试类,进行测试:
@RequestMapping("execute")
@ResponseBody
public String executeTask(@RequestParam(value = "cron", defaultValue = "0/10 * * * * ?") String cron) {
LOGGER.info("cron={}", cron);
dynamicCronHandler.taskCron(cron);
return "success";
}
暴露一个http接口,接受参数cron,启动应用并访问/execute,首次传入参数cron=0/1 ?,表示每秒执行一次任务。日志如下:
2019-12-03 15:32:40.001 INFO 7196 --- [TaskScheduler-1] c.s.s.t.d.d.DynamicCronHandler : 执行任务
2019-12-03 15:32:41.001 INFO 7196 --- [TaskScheduler-1] c.s.s.t.d.d.DynamicCronHandler : 执行任务
2019-12-03 15:32:42.001 INFO 7196 --- [TaskScheduler-1] c.s.s.t.d.d.DynamicCronHandler : 执行任务
2019-12-03 15:32:43.001 INFO 7196 --- [TaskScheduler-1] c.s.s.t.d.d.DynamicCronHandler : 执行任务
2019-12-03 15:32:44.001 INFO 7196 --- [TaskScheduler-1] c.s.s.t.d.d.DynamicCronHandler : 执行任务
可以看到每秒执行一次。
更改cron的值为0/5 ?,观察到控制台输出发生变化:
更新cron=0/5 * * * * ?
2019-12-03 15:33:30.001 INFO 7196 --- [TaskScheduler-1] c.s.s.t.d.d.DynamicCronHandler : 执行任务
2019-12-03 15:33:35.001 INFO 7196 --- [TaskScheduler-1] c.s.s.t.d.d.DynamicCronHandler : 执行任务
2019-12-03 15:33:40.001 INFO 7196 --- [TaskScheduler-1] c.s.s.t.d.d.DynamicCronHandler : 执行任务
此时定时任务执行频率更新为5秒一次,表明通过SchedulingConfigurer.configureTasks回调,动态的更新了定时任务执行频率。
思考
到目前为止,实现了动态变更定时任务的执行频率,但是不能实现动态的提交定时任务。方案二就是为了解决这个疑问而实现的,
方案二:动态提交定时任务并更新任务执行频率
首先建立一个DynamicTaskScheduler类,内容如下:
@Scope(value = "singleton")
@Component
@EnableScheduling
public class DynamicTaskScheduler {
private ScheduledFuture<?> future;
@Autowired
private ThreadPoolTaskScheduler threadPoolTaskScheduler;
@Bean
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
return new ThreadPoolTaskScheduler();
}
public void startCron(Runnable task, String cron) {
stopCron();
future = threadPoolTaskScheduler.schedule(
task, new CronTrigger(cron)
);
}
public void stopCron() {
if (future != null) {
future.cancel(true);
System.out.println("stopCron()");
}
}
}
这里通过startCron提交一个新的任务,通过cron表达式进行调度,在开始之前进行判断是否关闭老的,必须关闭老的才能开启新的。
通过stopCron对老任务进行关闭。
编写一个测试方法测试该动态任务调度类。
@RequestMapping("execute1")
@ResponseBody
public String executeTask1(@RequestParam(value = "cron", defaultValue = "0/10 * * * * ?") String cron) {
LOGGER.info("cron={}", cron);
dynamicTaskScheduler.startCron(
() -> {
LOGGER.info("模拟执行作业,cron={}", cron);
},
cron
);
return "success";
}
启动方法中初始化一个 ThreadPoolTaskScheduler 实例。
@SpringBootApplication
public class SnowalkerTestDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SnowalkerTestDemoApplication.class, args);
}
@Bean
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
executor.setPoolSize(20);
executor.setThreadNamePrefix("taskExecutor-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
return executor;
}
}
运行启动类,访问测试接口/execute1,先传入cron=0/1 ?,表示每秒执行一次任务。日志如下:
更改cron的值为0/5 ?,观察到控制台输出发生变化:
可以看到这种方式同样实现了动态的变更定时任务执行频率,相比上述的方法,该方式更加灵活,能够动态的增加任务到线程池中进行调度,我们可以定义一个Map保存future,从而实现创建并维护多个定时任务,具体可以参考这篇文章 ThreadPoolTaskScheduler的使用,定时任务开启与关闭 ,思路如下:
- 自定义Task类,实现Runnable,定义属性name
- 定义一个ConcurrentHashMap,KEY=name,value=ScheduledFuture
- 通过 ScheduledFuture<?> schedule(Runnable task, Trigger trigger) 进行任务调度时,传入自定义Task,构造/setter 注入任务名称(全局唯一), 并将该task实现类设置到步骤2的map中,key=name,value=当前通过schedule调度返回的ScheduledFuture
- 停止该任务时,通过name在map中找到ScheduledFuture实例,调用scheduledFuture.cancel(true);方法停止任务即可
核心代码如下:
任务存储Map
public static ConcurrentHashMap<String, ScheduledFuture> map = new ConcurrentHashMap<>();
启动任务
@Component
@Scope("prototype")
public class DynamicTask {
@Autowired
private ThreadPoolTaskScheduler threadPoolTaskScheduler;
private ScheduledFuture future;
public void startCron() {
cron = "0/1 * * * * ?";
System.out.println(Thread.currentThread().getName());
String name = Thread.currentThread().getName();
future = threadPoolTaskScheduler.schedule(new myTask(name), new CronTrigger(cron));
App.map.put(name, future);
}
停止任务
public void stop() {
if (future != null) {
future.cancel(true);
}
}
}
自定义Task定义
public class MyTask implements Runnable {
private String name;
myTask(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("test" + name);
}
}
测试接口
@Autowired
private DynamicTask task;
@RequestMapping("/start")
public void test() throws Exception {
// 开启定时任务,对象注解Scope是多利的。
task.startCron();
}
@RequestMapping("/stop")
public void stop() throws Exception {
// 提前测试用来测试线程1进行对比是否关闭。
ScheduledFuture scheduledFuture = App.map.get("http-nio-8081-exec-2");
scheduledFuture.cancel(true);
// 查看任务是否在正常执行之前结束,正常返回true
boolean cancelled = scheduledFuture.isCancelled();
while (!cancelled) {
scheduledFuture.cancel(true);
}
}
小结
以上就是SpringBoot动态定时任务相关的讲解,这种方式在轻量级环境下能够很好的工作。如果我们的定时任务要求分布式,高可用,则需要引入额外的组件,如果有必要则需要引入如ejob,xxl-job,quartz等定时调度组件。
参考资料
SpringBoot中并发定时任务的实现、动态定时任务的实现
使用ThreadPoolTaskScheduler实现定时关单
ThreadPoolTaskScheduler的使用,定时任务开启与关闭
版权声明:
原创不易,洗文可耻。除非注明,本博文章均为原创,转载请以链接形式标明本文地址。