動態(tài)更改Spring定時任務(wù)Cron表達式的優(yōu)雅方案實例詳解
0x01 前言
在 SpringBoot 項目中,我們可以通過@EnableScheduling
注解開啟調(diào)度任務(wù)支持,并通過@Scheduled
注解快速地建立一系列定時任務(wù)。
@Scheduled
支持下面三種配置執(zhí)行時間的方式:
- cron(expression):根據(jù)
Cron
表達式來執(zhí)行。 - fixedDelay(period):固定間隔時間執(zhí)行,無論任務(wù)執(zhí)行長短,兩次任務(wù)執(zhí)行的間隔總是相同的。
- fixedRate(period):固定頻率執(zhí)行,從任務(wù)啟動之后,總是在固定的時刻執(zhí)行,如果因為執(zhí)行時間過長,造成錯過某個時刻的執(zhí)行(晚點),則任務(wù)會被立刻執(zhí)行。
最常用的應(yīng)該是第一種方式,基于Cron
表達式的執(zhí)行模式,因其相對來說更加靈活。
0x02 可變與不可變
默認情況下,@Scheduled
注解標記的定時任務(wù)方法在初始化之后,是不會再發(fā)生變化的。Spring 在初始化 bean 后,通過后處理器攔截所有帶有@Scheduled
注解的方法,并解析相應(yīng)的的注解參數(shù),放入相應(yīng)的定時任務(wù)列表等待后續(xù)統(tǒng)一執(zhí)行處理。到定時任務(wù)真正啟動之前,我們都有機會更改任務(wù)的執(zhí)行周期等參數(shù)。換言之,我們既可以通過application.properties
配置文件配合@Value
注解的方式指定任務(wù)的Cron
表達式,亦可以通過CronTrigger
從數(shù)據(jù)庫或者其他任意存儲中間件中加載并注冊定時任務(wù)。這是 Spring 提供給我們的可變的部分。
但是我們往往要得更多。能否在定時任務(wù)已經(jīng)在執(zhí)行過的情況下,去動態(tài)更改Cron
表達式,甚至禁用某個定時任務(wù)呢?很遺憾,默認情況下,這是做不到的,任務(wù)一旦被注冊和執(zhí)行,用于注冊的參數(shù)便被固定下來,這是不可變的部分。
0x03 創(chuàng)造與毀滅
既然創(chuàng)造之后不可變,那就毀滅之后再重建吧。于是乎,我們的思路便是,在注冊期間保留任務(wù)的關(guān)鍵信息,并通過另一個定時任務(wù)檢查配置是否發(fā)生變化,如果有變化,就把“前任”干掉,取而代之。如果沒有變化,就保持原樣。
先對任務(wù)做個簡單的抽象,方便統(tǒng)一的識別和管理:
public interface IPollableService { /** * 執(zhí)行方法 */ void poll(); /** * 獲取周期表達式 * * @return CronExpression */ default String getCronExpression() { return null; } /** * 獲取任務(wù)名稱 * * @return 任務(wù)名稱 */ default String getTaskName() { return this.getClass().getSimpleName(); } }
最重要的便是getCronExpression()
方法,每個定時服務(wù)實現(xiàn)可以自己控制自己的表達式,變與不變,自己說了算。至于從何處獲取,怎么獲取,請諸君自行發(fā)揮了。接下來,就是實現(xiàn)任務(wù)的動態(tài)注冊:
@Configuration @EnableAsync @EnableScheduling public class SchedulingConfiguration implements SchedulingConfigurer, ApplicationContextAware { private static final Logger log = LoggerFactory.getLogger(SchedulingConfiguration.class); private static ApplicationContext appCtx; private final ConcurrentMap<String, ScheduledTask> scheduledTaskHolder = new ConcurrentHashMap<>(16); private final ConcurrentMap<String, String> cronExpressionHolder = new ConcurrentHashMap<>(16); private ScheduledTaskRegistrar taskRegistrar; public static synchronized void setAppCtx(ApplicationContext appCtx) { SchedulingConfiguration.appCtx = appCtx; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { setAppCtx(applicationContext); } @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { this.taskRegistrar = taskRegistrar; } /** * 刷新定時任務(wù)表達式 */ public void refresh() { Map<String, IPollableService> beanMap = appCtx.getBeansOfType(IPollableService.class); if (beanMap.isEmpty() || taskRegistrar == null) { return; } beanMap.forEach((beanName, task) -> { String expression = task.getCronExpression(); String taskName = task.getTaskName(); if (null == expression) { log.warn("定時任務(wù)[{}]的任務(wù)表達式未配置或配置錯誤,請檢查配置", taskName); return; } // 如果策略執(zhí)行時間發(fā)生了變化,則取消當前策略的任務(wù),并重新注冊任務(wù) boolean unmodified = scheduledTaskHolder.containsKey(beanName) && cronExpressionHolder.get(beanName).equals(expression); if (unmodified) { log.info("定時任務(wù)[{}]的任務(wù)表達式未發(fā)生變化,無需刷新", taskName); return; } Optional.ofNullable(scheduledTaskHolder.remove(beanName)).ifPresent(existTask -> { existTask.cancel(); cronExpressionHolder.remove(beanName); }); if (ScheduledTaskRegistrar.CRON_DISABLED.equals(expression)) { log.warn("定時任務(wù)[{}]的任務(wù)表達式配置為禁用,將被不會被調(diào)度執(zhí)行", taskName); return; } CronTask cronTask = new CronTask(task::poll, expression); ScheduledTask scheduledTask = taskRegistrar.scheduleCronTask(cronTask); if (scheduledTask != null) { log.info("定時任務(wù)[{}]已加載,當前任務(wù)表達式為[{}]", taskName, expression); scheduledTaskHolder.put(beanName, scheduledTask); cronExpressionHolder.put(beanName, expression); } }); } }
重點是保存ScheduledTask
對象的引用,它是控制任務(wù)啟停的關(guān)鍵。而表達式“-”則作為一個特殊的標記,用于禁用某個定時任務(wù)。當然,禁用后的任務(wù)通過重新賦予新的 Cron 表達式,是可以“復(fù)活”的。完成了上面這些,我們還需要一個定時任務(wù)來動態(tài)監(jiān)控和刷新定時任務(wù)配置:
@Component public class CronTaskLoader implements ApplicationRunner { private static final Logger log = LoggerFactory.getLogger(CronTaskLoader.class); private final SchedulingConfiguration schedulingConfiguration; private final AtomicBoolean appStarted = new AtomicBoolean(false); private final AtomicBoolean initializing = new AtomicBoolean(false); public CronTaskLoader(SchedulingConfiguration schedulingConfiguration) { this.schedulingConfiguration = schedulingConfiguration; } /** * 定時任務(wù)配置刷新 */ @Scheduled(fixedDelay = 5000) public void cronTaskConfigRefresh() { if (appStarted.get() && initializing.compareAndSet(false, true)) { log.info("定時調(diào)度任務(wù)動態(tài)加載開始>>>>>>"); try { schedulingConfiguration.refresh(); } finally { initializing.set(false); } log.info("定時調(diào)度任務(wù)動態(tài)加載結(jié)束<<<<<<"); } } @Override public void run(ApplicationArguments args) { if (appStarted.compareAndSet(false, true)) { cronTaskConfigRefresh(); } } }
當然,也可以把這部分代碼直接整合到SchedulingConfiguration
中,但是為了方便擴展,這里還是將執(zhí)行與觸發(fā)分離了。畢竟除了通過定時任務(wù)觸發(fā)刷新,還可以在界面上通過按鈕手動觸發(fā)刷新,或者通過消息機制回調(diào)刷新。這一部分就請大家根據(jù)實際業(yè)務(wù)情況來自由發(fā)揮了。
0x04 驗證
我們創(chuàng)建一個原型工程和三個簡單的定時任務(wù)來驗證下,第一個任務(wù)是執(zhí)行周期固定的任務(wù),假設(shè)它的Cron
表達式永遠不會發(fā)生變化,像這樣:
@Service public class CronTaskBar implements IPollableService { @Override public void poll() { System.out.println("Say Bar"); } @Override public String getCronExpression() { return "0/1 * * * * ?"; } }
第二個任務(wù)是一個經(jīng)常更換執(zhí)行周期的任務(wù),我們用一個隨機數(shù)發(fā)生器來模擬它的善變:
@Service public class CronTaskFoo implements IPollableService { private static final Random random = new SecureRandom(); @Override public void poll() { System.out.println("Say Foo"); } @Override public String getCronExpression() { return "0/" + (random.nextInt(9) + 1) + " * * * * ?"; } }
第三個任務(wù)就厲害了,它仿佛就像一個電燈的開關(guān),在啟用和禁用中反復(fù)橫跳:
@Service public class CronTaskUnavailable implements IPollableService { private String cronExpression = "-"; private static final Map<String, String> map = new HashMap<>(); static { map.put("-", "0/1 * * * * ?"); map.put("0/1 * * * * ?", "-"); } @Override public void poll() { System.out.println("Say Unavailable"); } @Override public String getCronExpression() { return (cronExpression = map.get(cronExpression)); } }
如果上面的步驟都做對了,日志里應(yīng)該能看到類似這樣的輸出:
定時調(diào)度任務(wù)動態(tài)加載開始>>>>>>
定時任務(wù)[CronTaskBar]的任務(wù)表達式未發(fā)生變化,無需刷新
定時任務(wù)[CronTaskFoo]已加載,當前任務(wù)表達式為[0/6 * * * * ?]
定時任務(wù)[CronTaskUnavailable]的任務(wù)表達式配置為禁用,將被不會被調(diào)度執(zhí)行
定時調(diào)度任務(wù)動態(tài)加載結(jié)束<<<<<<
Say Bar
Say Bar
Say Foo
Say Bar
Say Bar
Say Bar
定時調(diào)度任務(wù)動態(tài)加載開始>>>>>>
定時任務(wù)[CronTaskBar]的任務(wù)表達式未發(fā)生變化,無需刷新
定時任務(wù)[CronTaskFoo]已加載,當前任務(wù)表達式為[0/3 * * * * ?]
定時任務(wù)[CronTaskUnavailable]已加載,當前任務(wù)表達式為[0/1 * * * * ?]
定時調(diào)度任務(wù)動態(tài)加載結(jié)束<<<<<<
Say Unavailable
Say Bar
Say Unavailable
Say Bar
Say Foo
Say Unavailable
Say Bar
Say Unavailable
Say Bar
Say Unavailable
Say Bar
0x05 小結(jié)
我們在上文通過定時刷新和重建任務(wù)的方式來實現(xiàn)了動態(tài)更改Cron
表達式的需求,能夠滿足大部分的項目場景,而且沒有引入quartzs
等額外的中間件,可以說是十分的輕量和優(yōu)雅了。
到此這篇關(guān)于動態(tài)更改Spring定時任務(wù)Cron表達式的優(yōu)雅方案的文章就介紹到這了,更多相關(guān)動態(tài)更改Spring定時任務(wù)Cron表達式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot+SpringCache實現(xiàn)兩級緩存(Redis+Caffeine)
這篇文章主要介紹了SpringBoot+SpringCache實現(xiàn)兩級緩存(Redis+Caffeine),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04Java服務(wù)器主機信息監(jiān)控工具類的示例代碼
這篇文章主要介紹了Java服務(wù)器主機信息監(jiān)控工具類的示例代碼,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-04-04如何解決getReader() has already been called&
這篇文章主要介紹了如何解決getReader() has already been called for this request問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05