Spring Scheduling本地任務(wù)調(diào)度設(shè)計與實現(xiàn)方式
一、Spring Boot 集成 Scheduling
對于不涉及分布式計算又關(guān)于時間的任務(wù)處理,也就是本地任務(wù)調(diào)度(Task Schduling)既是 Spring Framework 的集成功能,也是 Spring Boot 的重要特性。
在 Spring Boot 中,任務(wù)調(diào)度的使用得到了極大簡化。
1、簡單任務(wù)調(diào)度
從官方文檔介紹中,在任務(wù)調(diào)度類上聲明 EnableScheduling 和 @Configuration,并在調(diào)度方法添加 @Scheduled 就可以完成任務(wù)調(diào)度。
@Configuration @EnableScheduling public class SayHelloTask { @Scheduled(cron = "${hello.schedule.cron:*/10 * * * * *}") public void sayHello() { System.out.print("Hello,Schedule"); } }
@Scheduled 支持 cron 表達式,它的使用在這篇文章中做了介紹。
Spring Boot 做了增強,可以添加默認值,優(yōu)先從配置文件中讀取 hello.schedule.cron,如果為空則使用后面默認的值,也就是每十秒鐘執(zhí)行一次 sayHello 方法。
線程池默認使用一個線程,可使用 spring.task.scheduling 命名空間進行如下微調(diào):
properties復(fù)制代碼spring.task.scheduling.thread-name-prefix=scheduling- spring.task.scheduling.pool.size=2
2、自定義任務(wù)調(diào)度
如果需要擴展任務(wù)調(diào)度,可以實現(xiàn) SchedulingConfigurer 來完成。
@Component public class SayHiSchedulingConfigurer implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); threadPoolTaskScheduler.setPoolSize(4); threadPoolTaskScheduler.setThreadNamePrefix("helloschedules"); threadPoolTaskScheduler.initialize(); taskRegistrar.setScheduler(threadPoolTaskScheduler); } }
這樣,調(diào)整了任務(wù)調(diào)度的線程池大小,也修改了線程日志名稱,便于日志分析定位。
二、Spring Scheduling 設(shè)計說明
Spring Boot 對 TaskExecution and Scheduling 做了簡要使用說明,深入了解本地任務(wù)調(diào)度可以參考 Spring Framework 對 TaskExecution and Scheduling 的介紹。
下面就以 Spring Boot 3.1.2 以及 Spring Framework 6.0.11 版本為例,重點分析 Spring 對本地任務(wù)調(diào)度的實現(xiàn)方法。
首先,打開 @EnableScheduling 注解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(SchedulingConfiguration.class) @Documented public @interface EnableScheduling { }
這個注解寫了大段的注釋用于解釋任務(wù)調(diào)度的用法,從注解中我們可以了解到本地調(diào)度的設(shè)計說明,這是后面分析源碼、擴展實現(xiàn)、最佳實踐的基礎(chǔ):
- 簡單的任務(wù)調(diào)度可以通過在方法上標記 @Scheduled 實現(xiàn),Spring 提供了三種調(diào)度模式 cron、fixedRate 和 fixedDelay,也就是 cron 表達式執(zhí)行、固定頻率執(zhí)行與固定延遲執(zhí)行。
- Spring 容器會先掃描 org.springframework.scheduling.TaskScheduler 和 java.util.concurrent.ScheduledExecutorService 兩個 bean,如果都沒有注入的話會創(chuàng)建一個默認的單線程任務(wù)調(diào)度器。
- 如果 @Scheduled 無法滿足任務(wù)調(diào)度配置的話,或者需要運行時配置 fixRate 與 fixDelay,可以通過標記 @Configuration 來實現(xiàn) SchedulingConfigurer,這樣就可以訪問底層的 ScheduledTaskRegistrar 實例,實現(xiàn)細粒度的任務(wù)控制。注意使用類似 @Bean(destroyMethod="shutdown") 來保證自定義任務(wù)執(zhí)行器在 Spring 應(yīng)用程序上下文關(guān)閉時也能夠正確關(guān)閉。
- 最重要的,EnableScheduling 僅適用于本地應(yīng)用程序上下文,也就是我們說的本地任務(wù)調(diào)度。分布式任務(wù)調(diào)度會有其他解決方案。
三、Spring Scheduling 關(guān)鍵類初探
1、SchedulingConfiguration
使用 @EnableScheduling 就會自動引入這個注解,它只做了一件事就是注入了一個 bean ScheduledAnnotationBeanPostProcessor。
@Configuration(proxyBeanMethods = false) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class SchedulingConfiguration { @Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() { return new ScheduledAnnotationBeanPostProcessor(); } }
2、ScheduledAnnotationBeanPostProcessor
ScheduledAnnotationBeanPostProcessor 是任務(wù)調(diào)度的核心類,由 @EnableScheduling 注解自動注冊,作為 bean 的后置處理器實現(xiàn)了大量接口。核心功能是提供 cron、fixedRate 和 fixedDelay 三種調(diào)度模式,識別 @Scheduled 標記的方法,并轉(zhuǎn)換成 TaskScheduler 可調(diào)度的任務(wù)。以及,識別所有 SchedulingConfigurer 的實例,允許自定義使用調(diào)度任務(wù)或?qū)θ蝿?wù)進行細粒度的控制。
(1)postProcessAfterInitialization
忽略掉細枝末節(jié),方法的核心功能是掃描帶有 @Scheduled 注解的方法,并處理成標準化任務(wù)。
@Override public Object postProcessAfterInitialization(Object bean, String beanName) { // ... 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); }); // ... annotatedMethods.forEach((method, scheduledAnnotations) -> scheduledAnnotations.forEach(scheduled -> processScheduled(scheduled, method, bean))); // ... return bean; }
(2)processScheduled
將掃描到的所有調(diào)度配置借助 ScheduledTaskRegistrar 轉(zhuǎn)換成標準化調(diào)度任務(wù),直接被異步執(zhí)行,返回結(jié)果交給 ScheduledTask.future。
protected void processScheduled(Scheduled scheduled, Method method, Object bean) { try { Runnable runnable = createRunnable(bean, method); boolean processedSchedule = false; String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required"; Set<ScheduledTask> tasks = new LinkedHashSet<>(4); // ... // 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)) { TimeZone timeZone; if (StringUtils.hasText(zone)) { timeZone = StringUtils.parseTimeZoneString(zone); } else { timeZone = TimeZone.getDefault(); } tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone)))); } } } // ... // Finally register the scheduled tasks synchronized (this.scheduledTasks) { Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4)); regTasks.addAll(tasks); } // ... }
這里分析一下 fixedDelay 與 fixedRate 的區(qū)別:
fixedDelay 屬性可確保在執(zhí)行任務(wù)的結(jié)束時間與下一次執(zhí)行任務(wù)的開始時間之間有 n 毫秒的延遲。當(dāng)我們需要確保只有一個任務(wù)實例一直在運行時,該屬性特別有用。
而 fixedRate 屬性是每 n 毫秒運行一次計劃任務(wù)。它不會檢查任務(wù)之前的執(zhí)行情況。如果任務(wù)的所有執(zhí)行都是獨立的,這一點就很有用。如果我們不希望超出內(nèi)存和線程池的大小,那么 fixedRate 就會非常方便。不過,如果進入的任務(wù)不能快速完成,就有可能出現(xiàn)內(nèi)存不足異常。
(3)finishRegistration
這個方法完成任務(wù)調(diào)度器注冊。提供自定義調(diào)度任務(wù)擴展點的 SchedulingConfigurer 接口,并被全部掃描進來。
最后,查找 TaskScheduler,添加到 ScheduledTaskRegistrar 中。
private void finishRegistration() { if (this.scheduler != null) { this.registrar.setScheduler(this.scheduler); } if (this.beanFactory instanceof ListableBeanFactory lbf) { Map<String, SchedulingConfigurer> beans = lbf.getBeansOfType(SchedulingConfigurer.class); List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values()); AnnotationAwareOrderComparator.sort(configurers); for (SchedulingConfigurer configurer : configurers) { configurer.configureTasks(this.registrar); } } // ... this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true /false)); // ... this.registrar.afterPropertiesSet(); }
3、ScheduledTaskRegistrar
ScheduledAnnotationBeanPostProcessor 被 SchedulingConfiguration 創(chuàng)建的時候二話沒說,初始化就創(chuàng)建了一個 ScheduledTaskRegistrar。
ScheduledTaskRegistrar 用于輔助任務(wù)注冊到 TaskScheduler 中,尤其是結(jié)合 @EnableAsync 注解和 SchedulingConfigurer 的回調(diào)方法時。這個 bean 在創(chuàng)建時會先判斷是否存在 TaskScheduler,沒有就會創(chuàng)建一個單線程任務(wù)調(diào)度池,然后逐一把調(diào)度任務(wù)添加到任務(wù)隊列中。
(1)scheduleTasks
在被 ScheduledAnnotationBeanPostProcessor#finishRegistration 調(diào)用時,就是完成任務(wù)標準化,afterPropertiesSet 只完成了一個方法完成,也就是 scheduleTasks。
protected void scheduleTasks() { if (this.taskScheduler == null) { this.localExecutor = Executors.newSingleThreadScheduledExecutor(); this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor); } // ... if (this.cronTasks != null) { for (CronTask task : this.cronTasks) { addScheduledTask(scheduleCronTask(task)); } } // ... }
(2)scheduleCronTask
使用適配器模式將各個 Task 轉(zhuǎn)換成 ScheduledTask,成為標準化任務(wù)執(zhí)行。
其他幾個方法 scheduleTriggerTask,scheduleFixedRateTask,scheduleFixedDelayTask 都是類似的。
public ScheduledTask scheduleCronTask(CronTask task) { ScheduledTask scheduledTask = this.unresolvedTasks.remove(task); boolean newTask = false; if (scheduledTask == null) { scheduledTask = new ScheduledTask(task); newTask = true; } if (this.taskScheduler != null) { scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger()); } else { addCronTask(task); this.unresolvedTasks.put(task, scheduledTask); } return (newTask ? scheduledTask : null); }
四、Spring Scheduling 調(diào)用時序
@EnableScheduling 的引入使得 Spring 容器開啟了對本地調(diào)度任務(wù)掃描,并自動裝配了 SchedulingConfiguration,實際上是引入了 ScheduledAnnotationBeanPostProcessor。
ScheduledAnnotationBeanPostProcessor 實現(xiàn)了 postProcessAfterInitialization 首先被執(zhí)行,在 bean 初始化完成后對 @Scheduled 注解進行掃描并轉(zhuǎn)換成標準化本地調(diào)度任務(wù) ScheduledTask。
任務(wù)轉(zhuǎn)換完成后就會異步執(zhí)行任務(wù),但是需要等待主線程對線程池的初始化。ScheduledAnnotationBeanPostProcessor 實現(xiàn)了 onApplicationEvent 完成對任務(wù)注冊的初始化,包括自定義調(diào)度任務(wù)配置
SchedulingConfigurer 的所有實現(xiàn)的掃描,以及對調(diào)度任務(wù)執(zhí)行者 TaskScheduler 的初始化,如果都沒有注入的話會創(chuàng)建一個默認的單線程任務(wù)調(diào)度器。
最后,準備工作完成后,由 ScheduledTaskRegistrar 的各類型調(diào)度任務(wù)分別調(diào)用 ScheduledFuture 的 schedule 方法完成任務(wù)。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
- Spring中@EnableScheduling實現(xiàn)定時任務(wù)代碼實例
- Spring中的@EnableScheduling定時任務(wù)注解
- SpringBoot注解@EnableScheduling定時任務(wù)詳細解析
- SpringBoot使用Scheduling實現(xiàn)定時任務(wù)的示例代碼
- springboot通過SchedulingConfigurer實現(xiàn)多定時任務(wù)注冊及動態(tài)修改執(zhí)行周期(示例詳解)
- Spring定時任務(wù)關(guān)于@EnableScheduling的用法解析
- springboot項目使用SchedulingConfigurer實現(xiàn)多個定時任務(wù)的案例代碼
- SpringBoot使用SchedulingConfigurer實現(xiàn)多個定時任務(wù)多機器部署問題(推薦)
相關(guān)文章
如何修改json字符串中某個key對應(yīng)的value值
這篇文章主要介紹了如何修改json字符串中某個key對應(yīng)的value值操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11Java回調(diào)函數(shù)原理實例與代理模式的區(qū)別講解
今天小編就為大家分享一篇關(guān)于Java回調(diào)函數(shù)原理實例與代理模式的區(qū)別講解,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-02-02java中使用try-catch-finally一些值得注意的事(必看)
下面小編就為大家?guī)硪黄猨ava中使用try-catch-finally一些值得注意的事(必看)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-08-08解決springboot項目上傳文件出現(xiàn)臨時文件目錄為空的問題
這篇文章主要介紹了解決springboot項目上傳文件出現(xiàn)臨時文件目錄為空的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09Java中StringBuffer和StringBuilder_動力節(jié)點Java學(xué)院整理
StringBuffer、StringBuilder和String一樣,也用來代表字符串。String類是不可變類,StringBuffer則是可變類,任何對它所指代的字符串的改變都不會產(chǎn)生新的對象。本文重點給大家介紹String、StringBuffer、StringBuilder區(qū)別,感興趣的朋友一起看看吧2017-04-04Spring Cloud Alibaba教程之Sentinel的使用
這篇文章主要介紹了Spring Cloud Alibaba教程之Sentinel的使用,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09