Spring Scheduling本地任務(wù)調(diào)度設(shè)計(jì)與實(shí)現(xiàn)方式
一、Spring Boot 集成 Scheduling
對(duì)于不涉及分布式計(jì)算又關(guān)于時(shí)間的任務(wù)處理,也就是本地任務(wù)調(diào)度(Task Schduling)既是 Spring Framework 的集成功能,也是 Spring Boot 的重要特性。
在 Spring Boot 中,任務(wù)調(diào)度的使用得到了極大簡(jiǎn)化。
1、簡(jiǎn)單任務(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)使用一個(gè)線程,可使用 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è)計(jì)說明
Spring Boot 對(duì) TaskExecution and Scheduling 做了簡(jiǎn)要使用說明,深入了解本地任務(wù)調(diào)度可以參考 Spring Framework 對(duì) TaskExecution and Scheduling 的介紹。
下面就以 Spring Boot 3.1.2 以及 Spring Framework 6.0.11 版本為例,重點(diǎn)分析 Spring 對(duì)本地任務(wù)調(diào)度的實(shí)現(xiàn)方法。
首先,打開 @EnableScheduling 注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {
}這個(gè)注解寫了大段的注釋用于解釋任務(wù)調(diào)度的用法,從注解中我們可以了解到本地調(diào)度的設(shè)計(jì)說明,這是后面分析源碼、擴(kuò)展實(shí)現(xiàn)、最佳實(shí)踐的基礎(chǔ):
- 簡(jiǎn)單的任務(wù)調(diào)度可以通過在方法上標(biāo)記 @Scheduled 實(shí)現(xiàn),Spring 提供了三種調(diào)度模式 cron、fixedRate 和 fixedDelay,也就是 cron 表達(dá)式執(zhí)行、固定頻率執(zhí)行與固定延遲執(zhí)行。
- Spring 容器會(huì)先掃描 org.springframework.scheduling.TaskScheduler 和 java.util.concurrent.ScheduledExecutorService 兩個(gè) bean,如果都沒有注入的話會(huì)創(chuàng)建一個(gè)默認(rèn)的單線程任務(wù)調(diào)度器。
- 如果 @Scheduled 無法滿足任務(wù)調(diào)度配置的話,或者需要運(yùn)行時(shí)配置 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)閉時(shí)也能夠正確關(guān)閉。
- 最重要的,EnableScheduling 僅適用于本地應(yīng)用程序上下文,也就是我們說的本地任務(wù)調(diào)度。分布式任務(wù)調(diào)度會(huì)有其他解決方案。
三、Spring Scheduling 關(guān)鍵類初探
1、SchedulingConfiguration
使用 @EnableScheduling 就會(huì)自動(dòng)引入這個(gè)注解,它只做了一件事就是注入了一個(gè) 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 注解自動(dòng)注冊(cè),作為 bean 的后置處理器實(shí)現(xiàn)了大量接口。核心功能是提供 cron、fixedRate 和 fixedDelay 三種調(diào)度模式,識(shí)別 @Scheduled 標(biāo)記的方法,并轉(zhuǎn)換成 TaskScheduler 可調(diào)度的任務(wù)。以及,識(shí)別所有 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é)束時(shí)間與下一次執(zhí)行任務(wù)的開始時(shí)間之間有 n 毫秒的延遲。當(dāng)我們需要確保只有一個(gè)任務(wù)實(shí)例一直在運(yùn)行時(shí),該屬性特別有用。
而 fixedRate 屬性是每 n 毫秒運(yùn)行一次計(jì)劃任務(wù)。它不會(huì)檢查任務(wù)之前的執(zhí)行情況。如果任務(wù)的所有執(zhí)行都是獨(dú)立的,這一點(diǎn)就很有用。如果我們不希望超出內(nèi)存和線程池的大小,那么 fixedRate 就會(huì)非常方便。不過,如果進(jìn)入的任務(wù)不能快速完成,就有可能出現(xiàn)內(nèi)存不足異常。
(3)finishRegistration
這個(gè)方法完成任務(wù)調(diào)度器注冊(cè)。提供自定義調(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)建的時(shí)候二話沒說,初始化就創(chuàng)建了一個(gè) ScheduledTaskRegistrar。
ScheduledTaskRegistrar 用于輔助任務(wù)注冊(cè)到 TaskScheduler 中,尤其是結(jié)合 @EnableAsync 注解和 SchedulingConfigurer 的回調(diào)方法時(shí)。這個(gè) bean 在創(chuàng)建時(shí)會(huì)先判斷是否存在 TaskScheduler,沒有就會(huì)創(chuàng)建一個(gè)單線程任務(wù)調(diào)度池,然后逐一把調(diào)度任務(wù)添加到任務(wù)隊(duì)列中。
(1)scheduleTasks
在被 ScheduledAnnotationBeanPostProcessor#finishRegistration 調(diào)用時(shí),就是完成任務(wù)標(biāo)準(zhǔn)化,afterPropertiesSet 只完成了一個(gè)方法完成,也就是 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
使用適配器模式將各個(gè) Task 轉(zhuǎn)換成 ScheduledTask,成為標(biāo)準(zhǔn)化任務(wù)執(zhí)行。
其他幾個(gè)方法 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)用時(shí)序
@EnableScheduling 的引入使得 Spring 容器開啟了對(duì)本地調(diào)度任務(wù)掃描,并自動(dòng)裝配了 SchedulingConfiguration,實(shí)際上是引入了 ScheduledAnnotationBeanPostProcessor。
ScheduledAnnotationBeanPostProcessor 實(shí)現(xiàn)了 postProcessAfterInitialization 首先被執(zhí)行,在 bean 初始化完成后對(duì) @Scheduled 注解進(jìn)行掃描并轉(zhuǎn)換成標(biāo)準(zhǔn)化本地調(diào)度任務(wù) ScheduledTask。
任務(wù)轉(zhuǎn)換完成后就會(huì)異步執(zhí)行任務(wù),但是需要等待主線程對(duì)線程池的初始化。ScheduledAnnotationBeanPostProcessor 實(shí)現(xiàn)了 onApplicationEvent 完成對(duì)任務(wù)注冊(cè)的初始化,包括自定義調(diào)度任務(wù)配置
SchedulingConfigurer 的所有實(shí)現(xiàn)的掃描,以及對(duì)調(diào)度任務(wù)執(zhí)行者 TaskScheduler 的初始化,如果都沒有注入的話會(huì)創(chuàng)建一個(gè)默認(rèn)的單線程任務(wù)調(diào)度器。
最后,準(zhǔn)備工作完成后,由 ScheduledTaskRegistrar 的各類型調(diào)度任務(wù)分別調(diào)用 ScheduledFuture 的 schedule 方法完成任務(wù)。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- Spring中@EnableScheduling實(shí)現(xiàn)定時(shí)任務(wù)代碼實(shí)例
- Spring中的@EnableScheduling定時(shí)任務(wù)注解
- SpringBoot注解@EnableScheduling定時(shí)任務(wù)詳細(xì)解析
- SpringBoot使用Scheduling實(shí)現(xiàn)定時(shí)任務(wù)的示例代碼
- springboot通過SchedulingConfigurer實(shí)現(xiàn)多定時(shí)任務(wù)注冊(cè)及動(dòng)態(tài)修改執(zhí)行周期(示例詳解)
- Spring定時(shí)任務(wù)關(guān)于@EnableScheduling的用法解析
- springboot項(xiàng)目使用SchedulingConfigurer實(shí)現(xiàn)多個(gè)定時(shí)任務(wù)的案例代碼
- SpringBoot使用SchedulingConfigurer實(shí)現(xiàn)多個(gè)定時(shí)任務(wù)多機(jī)器部署問題(推薦)
相關(guān)文章
如何修改json字符串中某個(gè)key對(duì)應(yīng)的value值
這篇文章主要介紹了如何修改json字符串中某個(gè)key對(duì)應(yīng)的value值操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-11-11
Java回調(diào)函數(shù)原理實(shí)例與代理模式的區(qū)別講解
今天小編就為大家分享一篇關(guān)于Java回調(diào)函數(shù)原理實(shí)例與代理模式的區(qū)別講解,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-02-02
java中使用try-catch-finally一些值得注意的事(必看)
下面小編就為大家?guī)硪黄猨ava中使用try-catch-finally一些值得注意的事(必看)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-08-08
解決springboot項(xiàng)目上傳文件出現(xiàn)臨時(shí)文件目錄為空的問題
這篇文章主要介紹了解決springboot項(xiàng)目上傳文件出現(xiàn)臨時(shí)文件目錄為空的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09
Java中StringBuffer和StringBuilder_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
StringBuffer、StringBuilder和String一樣,也用來代表字符串。String類是不可變類,StringBuffer則是可變類,任何對(duì)它所指代的字符串的改變都不會(huì)產(chǎn)生新的對(duì)象。本文重點(diǎn)給大家介紹String、StringBuffer、StringBuilder區(qū)別,感興趣的朋友一起看看吧2017-04-04
SpringBoot登錄攔截配置詳解(實(shí)測(cè)可用)
這篇文章主要介紹了SpringBoot登錄攔截配置詳解(實(shí)測(cè)可用),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
Spring Cloud Alibaba教程之Sentinel的使用
這篇文章主要介紹了Spring Cloud Alibaba教程之Sentinel的使用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09
java 多態(tài)性詳解及簡(jiǎn)單實(shí)例
這篇文章主要介紹了java 多態(tài)性詳解及簡(jiǎn)單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-02-02

