Spring Scheduling本地任務(wù)調(diào)度設(shè)計與實(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 表達(dá)式,它的使用在這篇文章中做了介紹。
Spring Boot 做了增強(qiáng),可以添加默認(rèn)值,優(yōu)先從配置文件中讀取 hello.schedule.cron,如果為空則使用后面默認(rèn)的值,也就是每十秒鐘執(zhí)行一次 sayHello 方法。
線程池默認(rèn)使用一個線程,可使用 spring.task.scheduling 命名空間進(jìn)行如下微調(diào):
properties復(fù)制代碼spring.task.scheduling.thread-name-prefix=scheduling- spring.task.scheduling.pool.size=2
2、自定義任務(wù)調(diào)度
如果需要擴(kuò)展任務(wù)調(diào)度,可以實(shí)現(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 版本為例,重點(diǎn)分析 Spring 對本地任務(wù)調(diào)度的實(shí)現(xiàn)方法。
首先,打開 @EnableScheduling 注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {
}這個注解寫了大段的注釋用于解釋任務(wù)調(diào)度的用法,從注解中我們可以了解到本地調(diào)度的設(shè)計說明,這是后面分析源碼、擴(kuò)展實(shí)現(xiàn)、最佳實(shí)踐的基礎(chǔ):
- 簡單的任務(wù)調(diào)度可以通過在方法上標(biāo)記 @Scheduled 實(shí)現(xiàn),Spring 提供了三種調(diào)度模式 cron、fixedRate 和 fixedDelay,也就是 cron 表達(dá)式執(zhí)行、固定頻率執(zhí)行與固定延遲執(zhí)行。
- Spring 容器會先掃描 org.springframework.scheduling.TaskScheduler 和 java.util.concurrent.ScheduledExecutorService 兩個 bean,如果都沒有注入的話會創(chuàng)建一個默認(rèn)的單線程任務(wù)調(diào)度器。
- 如果 @Scheduled 無法滿足任務(wù)調(diào)度配置的話,或者需要運(yùn)行時配置 fixRate 與 fixDelay,可以通過標(biāo)記 @Configuration 來實(shí)現(xiàn) SchedulingConfigurer,這樣就可以訪問底層的 ScheduledTaskRegistrar 實(shí)例,實(shí)現(xiàn)細(xì)粒度的任務(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 的后置處理器實(shí)現(xiàn)了大量接口。核心功能是提供 cron、fixedRate 和 fixedDelay 三種調(diào)度模式,識別 @Scheduled 標(biāo)記的方法,并轉(zhuǎn)換成 TaskScheduler 可調(diào)度的任務(wù)。以及,識別所有 SchedulingConfigurer 的實(shí)例,允許自定義使用調(diào)度任務(wù)或?qū)θ蝿?wù)進(jìn)行細(xì)粒度的控制。
(1)postProcessAfterInitialization
忽略掉細(xì)枝末節(jié),方法的核心功能是掃描帶有 @Scheduled 注解的方法,并處理成標(biāo)準(zhǔn)化任務(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)換成標(biāo)準(zhǔ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ù)實(shí)例一直在運(yùn)行時,該屬性特別有用。
而 fixedRate 屬性是每 n 毫秒運(yùn)行一次計劃任務(wù)。它不會檢查任務(wù)之前的執(zhí)行情況。如果任務(wù)的所有執(zhí)行都是獨(dú)立的,這一點(diǎn)就很有用。如果我們不希望超出內(nèi)存和線程池的大小,那么 fixedRate 就會非常方便。不過,如果進(jìn)入的任務(wù)不能快速完成,就有可能出現(xiàn)內(nèi)存不足異常。
(3)finishRegistration
這個方法完成任務(wù)調(diào)度器注冊。提供自定義調(diào)度任務(wù)擴(kuò)展點(diǎn)的 SchedulingConfigurer 接口,并被全部掃描進(jìn)來。
最后,查找 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ù)標(biāo)準(zhǔn)化,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,成為標(biāo)準(zhǔn)化任務(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,實(shí)際上是引入了 ScheduledAnnotationBeanPostProcessor。
ScheduledAnnotationBeanPostProcessor 實(shí)現(xiàn)了 postProcessAfterInitialization 首先被執(zhí)行,在 bean 初始化完成后對 @Scheduled 注解進(jìn)行掃描并轉(zhuǎn)換成標(biāo)準(zhǔn)化本地調(diào)度任務(wù) ScheduledTask。
任務(wù)轉(zhuǎn)換完成后就會異步執(zhí)行任務(wù),但是需要等待主線程對線程池的初始化。ScheduledAnnotationBeanPostProcessor 實(shí)現(xiàn)了 onApplicationEvent 完成對任務(wù)注冊的初始化,包括自定義調(diào)度任務(wù)配置
SchedulingConfigurer 的所有實(shí)現(xiàn)的掃描,以及對調(diào)度任務(wù)執(zhí)行者 TaskScheduler 的初始化,如果都沒有注入的話會創(chuàng)建一個默認(rèn)的單線程任務(wù)調(diào)度器。
最后,準(zhǔn)備工作完成后,由 ScheduledTaskRegistrar 的各類型調(diào)度任務(wù)分別調(diào)用 ScheduledFuture 的 schedule 方法完成任務(wù)。
總結(jié)
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
- Spring中@EnableScheduling實(shí)現(xiàn)定時任務(wù)代碼實(shí)例
- Spring中的@EnableScheduling定時任務(wù)注解
- SpringBoot注解@EnableScheduling定時任務(wù)詳細(xì)解析
- SpringBoot使用Scheduling實(shí)現(xiàn)定時任務(wù)的示例代碼
- springboot通過SchedulingConfigurer實(shí)現(xiàn)多定時任務(wù)注冊及動態(tài)修改執(zhí)行周期(示例詳解)
- Spring定時任務(wù)關(guān)于@EnableScheduling的用法解析
- springboot項目使用SchedulingConfigurer實(shí)現(xiàn)多個定時任務(wù)的案例代碼
- SpringBoot使用SchedulingConfigurer實(shí)現(xiàn)多個定時任務(wù)多機(jī)器部署問題(推薦)
相關(guān)文章
java線程池prestartCoreThread prestartAllCoreThreads的預(yù)熱源碼解讀
這篇文章主要介紹了java線程池prestartCoreThread prestartAllCoreThreads的預(yù)熱源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
Spring?Boot項目使用WebClient調(diào)用第三方接口的詳細(xì)教程(附實(shí)例代碼)
WebClient是Spring WebFlux模塊提供的一個非阻塞的基于響應(yīng)式編程的進(jìn)行Http請求的客戶端工具,從Spring5.0開始提供,這篇文章主要介紹了Spring?Boot項目使用WebClient調(diào)用第三方接口的相關(guān)資料,需要的朋友可以參考下2025-09-09
java線程Thread.sleep()對比對象的wait示例解析
這篇文章主要為大家介紹了java線程Thread.sleep()對比對象的wait示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
基于Java實(shí)現(xiàn)多線程下載并允許斷點(diǎn)續(xù)傳
這篇文章主要介紹了基于Java實(shí)現(xiàn)多線程下載并允許斷點(diǎn)續(xù)傳,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-03-03

