基于Nacos實現(xiàn)SpringBoot動態(tài)定時任務(wù)調(diào)度
背景
最近在項目開發(fā)上,有一個定時核對并清理的需求,定時規(guī)則較為簡單,每15分鐘運行一次,并且項目中暫未接入分布式定時任務(wù)調(diào)度框架;鑒于以上兩個原因,我決定直接用 Spring scheduling 開干。
回顧一下 SpringBoot 項目中定義定時任務(wù),其實就幾個步驟:
- 在啟動類上,或者任意一個配置類上添加
@EnableScheduling注解 - 在需要運行定時任務(wù)的方法上,添加
@Scheduled注解, 可以傳入cron、fixedDelay、fixedRate三個值之一cron: 傳入 Spring cron 表達(dá)式,推薦使用fixedDelay: 以調(diào)用完成時刻開始計算間隔時間,單位是毫秒fixedRate: 以調(diào)用開始時刻來計算間隔時間,單位是毫秒
- 在這個方法內(nèi)調(diào)用定時任務(wù)實際的執(zhí)行邏輯
我用的cron表達(dá)式很快就把這個需求實現(xiàn)了;但這時變數(shù)出現(xiàn)了,要求cron表達(dá)式支持 Nacos 動態(tài)配置,并且「動態(tài)配置后,不需要重啟服務(wù)也能生效」
讓我一時間犯了難,后面查詢相關(guān)資料后,也實現(xiàn)了這個新需求,接下來我分享一下解題過程
實現(xiàn)動態(tài)變更定時機制
配置化 cron 表達(dá)式
我們知道,如果一個 Java Bean 使用 @ConfigurationProperties 修飾后,當(dāng)它的配置值通過 Nacos 修改后,Nacos client 會收到變更事件,從而刷新對應(yīng)的屬性值,所以我們考慮把 cron 表達(dá)式放入一個 properties 類管理,并使用 @ConfigurationProperties 修飾
@Data
@Component
@ConfigurationProperties(prefix = "task-schedule")
public class TaskProperties {
private String cronExpression = "*/5 * * * * ?";
}
然后我們可以把這個 cron 表達(dá)式通過 placeholder 綁定到 @Schedule 的 cron 中
@Slf4j
@Component
public class SimpleTask {
@Scheduled(cron = "#{@taskProperties.cronExpression}")
public void simpleTask2() {
log.info("SimpleTask2 scheduled");
}
}
Spring schedule 調(diào)度規(guī)則
但是啟動項目之后發(fā)現(xiàn),無論怎么修改 Nacos 上的配置值,這個定時任務(wù)的執(zhí)行間隔都不會隨配置值更新,只會按照首次啟動時的 cron 表達(dá)式來進(jìn)行
翻看源碼 ScheduledAnnotationBeanPostProcessor 這個類的 postProcessAfterInitialization 方法
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
...
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
if (!this.nonAnnotatedClasses.contains(targetClass) &&
AnnotationUtils.isCandidateClass(targetClass, List.of(Scheduled.class, Schedules.class))) {
Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
(MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
Set<Scheduled> scheduledAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
method, Scheduled.class, Schedules.class);
return (!scheduledAnnotations.isEmpty() ? scheduledAnnotations : null);
});
// Non-empty set of methods
annotatedMethods.forEach((method, scheduledAnnotations) ->
scheduledAnnotations.forEach(scheduled -> processScheduled(scheduled, method, bean)));
...
}
return bean;
}
我們知道 BeanPostProcessor 中的 postProcessAfterInitialization 方法,在 Bean 初始化后執(zhí)行,看到它在逐一地調(diào)度這些定時任務(wù)
所以我們可以推斷,這些定時任務(wù)只會在項目啟動之后統(tǒng)一的調(diào)度,并不會在運行期間自動更新調(diào)度規(guī)則
追蹤定時任務(wù)調(diào)度
既然我們查看源碼后發(fā)現(xiàn) SpringBoot 幫我們調(diào)度只發(fā)生在項目啟動之后,那么如果我們希望響應(yīng) Nacos 變更而改變調(diào)度規(guī)則,我們需要把調(diào)度的主動權(quán)把握在手
我們繼續(xù)追蹤源碼,ScheduledAnnotationBeanPostProcessor 的 processScheduled 方法,調(diào)用 processScheduledSync
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
...
processScheduledSync(scheduled, method, bean);
}
private void processScheduledSync(Scheduled scheduled, Method method, Object bean) {
Runnable task;
try {
task = createRunnable(bean, method, scheduled.scheduler());
}
catch (IllegalArgumentException ex) {
...
}
processScheduledTask(scheduled, task, method, bean);
}
我們目前先看同步調(diào)用的情況,可以看到 processScheduledSync 方法中,做了兩件事:
- 創(chuàng)建一個
Runnable對象 - 調(diào)度這個
Runnable對象
可以得到一點啟發(fā):定時任務(wù)相當(dāng)于創(chuàng)建一個任務(wù),然后使用調(diào)度器按規(guī)則不停地調(diào)用,這里的調(diào)度器我們可以簡單理解為 Scheduled 線程池
如果我們可以拿到這個定時任務(wù)執(zhí)行的調(diào)度器,緩存起來,這樣調(diào)度的時機、調(diào)度的規(guī)則都可以由我們自己控制了;而創(chuàng)建一個 Runnable 對象其實也不難,可以簡單創(chuàng)建后直接把定時任務(wù)包裝起來
查看 processScheduledTask 方法,它確實是這么做的
private void processScheduledTask(Scheduled scheduled, Runnable runnable, Method method, Object bean) {
try {
...
// Determine initial delay
...
// Check cron expression
String cron = scheduled.cron();
if (StringUtils.hasText(cron)) {
String zone = scheduled.zone();
if (this.embeddedValueResolver != null) {
cron = this.embeddedValueResolver.resolveStringValue(cron);
zone = this.embeddedValueResolver.resolveStringValue(zone);
}
if (StringUtils.hasLength(cron)) {
Assert.isTrue(initialDelay.isNegative(), "'initialDelay' not supported for cron triggers");
processedSchedule = true;
if (!Scheduled.CRON_DISABLED.equals(cron)) {
CronTrigger trigger;
if (StringUtils.hasText(zone)) {
trigger = new CronTrigger(cron, StringUtils.parseTimeZoneString(zone));
}
else {
trigger = new CronTrigger(cron);
}
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, trigger)));
}
}
}
// Check fixed delay
...
// Check fixed rate
...
// Finally register the scheduled tasks
synchronized (this.scheduledTasks) {
Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
regTasks.addAll(tasks);
}
}
...
}
this.registrar.scheduleCronTask(new CronTask(runnable, trigger)),這個 this.registrar 的類型是 ScheduledTaskRegistrar,這一句我們可以知道 ScheduledTaskRegistrar 就是核心的調(diào)度器,負(fù)責(zé)調(diào)度各種定時任務(wù),比如:Cron、FixDelay 等等
它依靠它里面的 TaskScheduler 來負(fù)責(zé)執(zhí)行最終的任務(wù)調(diào)度,默認(rèn)的實現(xiàn)類是 ThreadPoolTaskScheduler,它依靠 ScheduledExecutorService 來最終實現(xiàn)定時任務(wù)的調(diào)度
掌握調(diào)度主動權(quán)
繼續(xù)翻閱源碼,我們發(fā)現(xiàn) SchedulingConfigurer 接口,它允許我們使用 ScheduledTaskRegistrar 來定制化定時任務(wù)的執(zhí)行,有了上面的一點鋪墊后,這個接口就很好理解了
@FunctionalInterface
public interface SchedulingConfigurer {
/**
* Callback allowing a {@link org.springframework.scheduling.TaskScheduler}
* and specific {@link org.springframework.scheduling.config.Task} instances
* to be registered against the given the {@link ScheduledTaskRegistrar}.
* @param taskRegistrar the registrar to be configured
*/
void configureTasks(ScheduledTaskRegistrar taskRegistrar);
}
我們可以拿到方法參數(shù)的 taskRegistrar,像源碼一樣 scheduleCronTask
這種方式和使用 @schedule 相比,更靈活,但是代碼也會更為復(fù)雜一點,屬于編程式調(diào)度
并且還可以使用一個成員屬性來接收這個 ScheduledTaskRegistrar,這樣可以隨時使用
protected ScheduledTaskRegistrar taskRegistrarHolder;
@Override
public void configureTasks(@Nonnull ScheduledTaskRegistrar taskRegistrar) {
// set ScheduledTaskRegistrar so that we can use it to schedule the timer task
taskRegistrarHolder = taskRegistrar;
}
訂閱 Nacos 配置更新事件
對于剛才定義的 Properties 類,即使 Nacos 可以幫我們刷新配置,但是不做任何的自定義的話,只是單純刷新對象屬性值也起不到動態(tài)的目的,所以我們需要訂閱并做出自定義的響應(yīng)邏輯
通過查閱資料找到了關(guān)鍵組件 NacosConfigManager,可以通過這樣的方式來訂閱配置變更,并且方法上還需要訂閱 ApplicationReadyEvent
@EventListener(ApplicationReadyEvent.class)
public void refreshConfig() throws Exception {
// add nacos config listener
nacosConfigManager.getConfigService().addListener("dynamic-schedule.yml", "DEFAULT_GROUP", new AbstractConfigChangeListener() {
@Override
public void receiveConfigChange(ConfigChangeEvent event) {
// receive config change event
...
}
});
log.info("added nacos config listener successfully, especially for cron-expression");
}
由于這種方式是訂閱整個配置文件的配置變更,每一個配置被更新都會執(zhí)行這個 refreshConfig 方法,所以在里面需要根據(jù)具體的配置項來監(jiān)聽
Collection<ConfigChangeItem> changeItems = event.getChangeItems();
for (ConfigChangeItem changeItem : changeItems) {
log.info("config changed item: {}", changeItem.getKey());
String key = changeItem.getKey();
if (!("task-schedule.cron-expression".equals(key) || "task-schedule.cronExpression".equals(key))) {
log.info("cron-expression has not been changed, doesn't response it");
return false;
}
log.info("schedule config changed, new cron-expression: {}, now refresh the timer task", changeItem.getNewValue());
return true;
}
至此,我們就實現(xiàn)了配置自定義監(jiān)聽,并且監(jiān)聽后我們還能做出其他后續(xù)的響應(yīng)邏輯
最終效果
有了上面自定義調(diào)度和自定義訂閱之后,我們只需要把他們整合一下,就能實現(xiàn)
另外針對任務(wù)暫停,這里也做簡單的演示
初始的 cron 表達(dá)式是
*/5 * * * * ?,項目啟動:
修改 Nacos 配置,改成
*/8 * * * * ?,并且不重啟服務(wù):
修改 Nacos 配置,改成
-,讓任務(wù)停止,并且不重啟服務(wù):
在 Spring schedule 中,如果 cron 表達(dá)式是一個
-也是表示停止
到此這篇關(guān)于基于Nacos實現(xiàn)SpringBoot動態(tài)定時任務(wù)調(diào)度的文章就介紹到這了,更多相關(guān)SpringBoot動態(tài)定時任務(wù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot實現(xiàn)動態(tài)定時任務(wù)
- SpringBoot 動態(tài)定時器的使用方法
- 淺談SpringBoot集成Quartz動態(tài)定時任務(wù)
- SpringBoot設(shè)置動態(tài)定時任務(wù)的方法詳解
- SpringBoot實現(xiàn)固定和動態(tài)定時任務(wù)的三種方法
- SpringBoot實現(xiàn)設(shè)置動態(tài)定時任務(wù)的方法詳解
- SpringBoot創(chuàng)建動態(tài)定時任務(wù)的幾種方式小結(jié)
- SpringBoot+Quartz實現(xiàn)動態(tài)定時任務(wù)
- Springboot實現(xiàn)動態(tài)定時任務(wù)流程詳解
- SpringBoot動態(tài)定時任務(wù)實現(xiàn)完整版
- Springboot實現(xiàn)動態(tài)定時任務(wù)管理的示例代碼
相關(guān)文章
springboot項目啟動指定對應(yīng)環(huán)境的方法
這篇文章主要介紹了springboot項目啟動指定對應(yīng)環(huán)境的方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-08-08
spring cloud gateway請求跨域問題解決方案
這篇文章主要介紹了spring cloud gateway請求跨域問題解決方案,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-01-01
搜索一文入門ElasticSearch(節(jié)點 分片 CRUD 倒排索引 分詞)
這篇文章主要為大家介紹了搜索一文入門ElasticSearch(節(jié)點 分片 CRUD 倒排索引 分詞)的基礎(chǔ)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03

