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等定時調度組件。

原文鏈接:http://wuwenliang.net/2019/12/03/SpringBoot動態定時任務開發指南/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章