一般情況下,如果想在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等定時調度組件。