SpringBoot @Schedule的使用注意與原理分析
簡介
之前使用@Schedule一直沒有遇到什么問題,那種拿來就用的感覺還挺好,最近使用@Schedule遇到一點問題,才仔細(xì)的研究了一下@Schedule的一些細(xì)節(jié)和原理問題。
這篇文章就將分享一下,使用@Schedule一些可能被忽略的問題。
注意事項
@Schedule默認(rèn)線程池大小
我相信@Schedule默認(rèn)線程池大小的問題肯定是被很多拿來就用的朋友忽略的問題,默認(rèn)情況下@Schedule使用線程池的大小為1。
一般情況下沒有什么問題,但是如果有多個定時任務(wù),每個定時任務(wù)執(zhí)行時間可能不短的情況下,那么有的定時任務(wù)可能一直沒有機會執(zhí)行。
有興趣的朋友,可以試一下:
@Component
public class BrigeTask {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Scheduled(cron = "*/5 * * * * ?")
private void cron() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "-cron:" + LocalDateTime.now().format(FORMATTER));
TimeUnit.SECONDS.sleep(6);
}
@Scheduled(fixedDelay = 5000)
private void fixedDelay() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "-fixedDelay:" + LocalDateTime.now().format(FORMATTER));
TimeUnit.SECONDS.sleep(6);
}
@Scheduled(fixedRate = 5000)
private void fixedRate() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "-fixedRate:" + LocalDateTime.now().format(FORMATTER));
TimeUnit.SECONDS.sleep(6);
}
}上面的任務(wù)中,fixedDelay與cron,可能很久都不會被執(zhí)行。

要解決上面的問題,可以把執(zhí)行任務(wù)的線程池設(shè)置大一點,怎樣設(shè)置通過實現(xiàn)SchedulingConfigurer接口,在configureTasks方法中配置,這種方式參見后面的代碼,這里可以直接注入一個TaskScheduler來解決問題。
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(5);
return taskScheduler;
}當(dāng)然也可以使用ScheduledExecutorService:
@Bean
public ScheduledExecutorService scheduledExecutorService() {
return Executors.newScheduledThreadPool(10);
}為啥這樣有效,請參考后面@Schedule原理。
固定延遲與固定速率
@Schedule的三種方式cron、fixedDelay、fixedRate不管線程夠不夠都會阻塞到上一次執(zhí)行完成,才會執(zhí)行下一次。
如果任務(wù)方法執(zhí)行時間非常短,上面三種方式其實基本沒有太多的區(qū)別。
如果,任務(wù)方法執(zhí)行時間比較長,大于了設(shè)置的執(zhí)行周期,那么就有很大的區(qū)別。例如,假設(shè)執(zhí)行任務(wù)的線程足夠,執(zhí)行周期是5s,任務(wù)方法會執(zhí)行6s。
- cron的執(zhí)行方式是,任務(wù)方法執(zhí)行完,遇到下一次匹配的時間再次執(zhí)行,基本就會10s執(zhí)行一次,因為執(zhí)行任務(wù)方法的時間區(qū)間會錯過一次匹配。
- fixedDelay的執(zhí)行方式是,方法執(zhí)行了6s,然后會再等5s再執(zhí)行下一次,在上面的條件下,基本就是每11s執(zhí)行一次。
- fixedRate的執(zhí)行方式就變成了每隔6s執(zhí)行一次,因為按固定區(qū)間執(zhí)行它沒5s就應(yīng)該執(zhí)行一次,但是任務(wù)方法執(zhí)行了6s,沒辦法,只好6s執(zhí)行一次。
上面的結(jié)論都可以通過,最上面的示例驗證,有興趣的朋友可以調(diào)整一下休眠時間測試一下。
SpringBoot @Schedule原理
在SpringBoot中,我們使用@EnableScheduling來啟用@Schedule。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {
}EnableScheduling注解沒什么特殊,需要注意import了SchedulingConfiguration。
SchedulingConfiguration一看名字就知道是一個配置類,肯定是為了添加相應(yīng)的依賴類。
@Configuration
@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();
}
}我們可以看到在SchedulingConfiguration創(chuàng)建了一個ScheduledAnnotationBeanPostProcessor。
看樣子SpringBoot定時任務(wù)的核心就是ScheduledAnnotationBeanPostProcessor類了,下面我們來看一看ScheduledAnnotationBeanPostProcessor類。
ScheduledAnnotationBeanPostProcessor
public class ScheduledAnnotationBeanPostProcessor
implements ScheduledTaskHolder, MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor,
Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware,
SmartInitializingSingleton, ApplicationListener<ContextRefreshedEvent>, DisposableBean {
}ScheduledAnnotationBeanPostProcessor實現(xiàn)了很多接口,這里重點關(guān)注2個,ApplicationListener和DestructionAwareBeanPostProcessor。
DestructionAwareBeanPostProcessor封裝任務(wù)
DestructionAwareBeanPostProcessor繼承了BeanPostProcessor。
BeanPostProcessor相信大家已經(jīng)非常熟悉了,就是在Bean創(chuàng)建執(zhí)行setter之后,在自定義的afterPropertiesSet和init-method前后提供攔截點,大致執(zhí)行的先后順序是:
Bean實例化 -> setter -> BeanPostProcessor#postProcessBeforeInitialization -> -> InitializingBean#afterPropertiesSet -> init-method -> BeanPostProcessor#postProcessAfterInitialization
我們看一下ScheduledAnnotationBeanPostProcessor的postProcessAfterInitialization方法:
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||
bean instanceof ScheduledExecutorService) {
// Ignore AOP infrastructure such as scoped proxies.
return bean;
}
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
if (!this.nonAnnotatedClasses.contains(targetClass) &&
AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) {
Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
(MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
method, Scheduled.class, Schedules.class);
return (!scheduledMethods.isEmpty() ? scheduledMethods : null);
});
if (annotatedMethods.isEmpty()) {
this.nonAnnotatedClasses.add(targetClass);
if (logger.isTraceEnabled()) {
logger.trace("No @Scheduled annotations found on bean class: " + targetClass);
}
}
else {
// Non-empty set of methods
annotatedMethods.forEach((method, scheduledMethods) ->
scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
if (logger.isTraceEnabled()) {
logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
"': " + annotatedMethods);
}
}
}
return bean;
}簡單說一下流程:
找到所有的Schedule方法,把它封裝為ScheduledMethodRunnable類(ScheduledMethodRunnable類實現(xiàn)了Runnable接口),并把其做為一個任務(wù)注冊到ScheduledTaskRegistrar中。
如果對具體的邏輯感興趣,可以從postProcessAfterInitialization方法順著processScheduled方法一次debug。
ApplicationListener執(zhí)行任務(wù)
前面我們介紹通過BeanPostProcessor解析出了所有的任務(wù),接下來要做的事情就是提交任務(wù)了。
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (event.getApplicationContext() == this.applicationContext) {
// Running in an ApplicationContext -> register tasks this late...
// giving other ContextRefreshedEvent listeners a chance to perform
// their work at the same time (e.g. Spring Batch's job registration).
finishRegistration();
}
}ScheduledAnnotationBeanPostProcessor監(jiān)聽的事件是ContextRefreshedEvent,就是在容器初始化,或者刷新的時候被調(diào)用。
監(jiān)聽到ContextRefreshedEvent事件之后,值調(diào)用了finishRegistration方法,這個方法的基本流程如下:
- 1.找到容器中的SchedulingConfigurer,并調(diào)用它的configureTasks,SchedulingConfigurer的作用主要就是配置ScheduledTaskRegistrar類,例如線程池等參數(shù),例如:
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.concurrent.Executors;
@Configuration
public class MyScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
}
}- 2.調(diào)用ScheduledTaskRegistrar的afterPropertiesSet方法執(zhí)行任務(wù),如果對具體的邏輯感興趣,可以閱讀ScheduledTaskRegistrar的scheduleTasks方法。
關(guān)于為啥直接在容器中注入一個TaskScheduler、ScheduledExecutorService也可以有效,也可以在finishRegistration方法中找到答案。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
簡單談?wù)刯ava中final,finally,finalize的區(qū)別
Java中final、finally、finalize的區(qū)別與用法,困擾了不少學(xué)習(xí)者,下面我們就這個問題進(jìn)行一些探討,希望對大家的學(xué)習(xí)有所幫助。2016-05-05
Java中synchronized關(guān)鍵字鎖的力度與位置示例詳解
synchronized是Java中最核心的線程同步機制,用于解決多線程環(huán)境下的數(shù)據(jù)競爭和可見性問題,這篇文章主要介紹了Java中synchronized關(guān)鍵字鎖的力度與位置的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2026-01-01
java利用easyexcel實現(xiàn)導(dǎo)入與導(dǎo)出功能
這篇文章主要介紹了java利用easyexcel實現(xiàn)導(dǎo)入與導(dǎo)出功能,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價值,感興趣的小伙伴可以參考一下,希望對你的學(xué)習(xí)有所幫助2022-09-09
SpringBoot使用AOP統(tǒng)一日志管理的方法詳解
這篇文章主要為大家分享一個干貨:超簡潔SpringBoot使用AOP統(tǒng)一日志管理,文中的示例代碼講解詳細(xì),感興趣的小伙伴快跟隨小編一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05

