Spring Boot - 定時(shí)任務(wù)

前言

對(duì)于一些周期性的工作刊橘,使用定時(shí)任務(wù)來(lái)執(zhí)行是非常合適的操作冠句。

Spring 3.0 時(shí)對(duì)定時(shí)任務(wù)提供了支持馒稍,其提供了一個(gè)接口TaskScheduler作為定時(shí)任務(wù)的抽象专酗,并且提供了一個(gè)默認(rèn)的實(shí)現(xiàn)ThreadPoolTaskScheduler,該實(shí)現(xiàn)是對(duì) JDK ScheduledExecutorService的包裝并增加了一些擴(kuò)展觸發(fā)功能牙咏。

本文主要介紹下在 Spring Boot 中使用定時(shí)任務(wù)。

基本使用

在 Spring Boot 中嘹裂,要使用定時(shí)任務(wù)妄壶,只需進(jìn)行如下兩步操作:

  1. 使用注解@EnableScheduling開啟定時(shí)任務(wù):

    @SpringBootApplication
    @ComponentScan("com.yn.scheduled")
    @EnableScheduling // 開啟定時(shí)任務(wù)
    public class Application {
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
    

    EnableScheduling注解會(huì)注冊(cè)一個(gè)ScheduledAnnotationBeanPostProcessor,從而使能掃描 Spring IOC 容器中對(duì)象的@Scheduled注解方法焦蘑。

  2. 為需要定期執(zhí)行的方法添加@Scheduled注解:

    @Component // 添加到 IOC 容器中
    public class ScheduledTask {
        private Logger logger = LoggerFactory.getLogger(this.getClass());
    
        @Scheduled(fixedRate = 1000)
        public void scheduledTask() {
            logger.info("定時(shí)任務(wù)(每秒一次):{}",new Date()) ;
        }
    }
    

    上述@Scheduled注解定義了一個(gè)每 1 秒執(zhí)行一次的定時(shí)任務(wù)scheduledTask盯拱。

以上,我們就完成了一個(gè)定時(shí)任務(wù)scheduledTask例嘱,此時(shí)運(yùn)行程序狡逢,就可以在控制臺(tái)看到定時(shí)任務(wù)scheduledTask每 1 秒都會(huì)得到執(zhí)行。

:被@Scheduled注解的定時(shí)任務(wù)有如下限制:

  • 定時(shí)方法必須是無(wú)參函數(shù)
  • 定時(shí)方法通常返回void拼卵,如果設(shè)置了返回其他類型的數(shù)據(jù)奢浑,則返回值會(huì)被忽略。

@Scheduled 簡(jiǎn)解

通過(guò)上面內(nèi)容腋腮,我們可以知道雀彼,在 Spring 中壤蚜,定時(shí)任務(wù)的設(shè)置主要通過(guò)注解@Scheduled來(lái)定義,其具體內(nèi)容如下圖所示:

Scheduled

以下我們只對(duì)@Scheduled注解常用的屬性進(jìn)行介紹:

  • cron:表示以 Unix 的 cron 方式定義時(shí)間徊哑。
    cron的定時(shí)設(shè)置功能十分靈活強(qiáng)大袜刷,具體的設(shè)置方式可參考 附錄 - cron 表達(dá)式

  • fixedRate:表示以固定間隔執(zhí)行定時(shí)任務(wù)莺丑。這里的間隔指的是:每次調(diào)用的時(shí)候就開始計(jì)時(shí)著蟹,到指定間隔時(shí)間再調(diào)用下一個(gè)定時(shí)任務(wù)。

  • fixedDelay:表示以固定間隔執(zhí)行定時(shí)任務(wù)梢莽。這里的間隔指的是:上一次定時(shí)任務(wù)完成后萧豆,才開始計(jì)時(shí),到指定間隔時(shí)間再調(diào)用下一個(gè)定時(shí)任務(wù)昏名。

  • initialDelay:表示首次運(yùn)行定時(shí)任務(wù)前的延時(shí)時(shí)間涮雷。可用在于fixedRatefixedDelay的定時(shí)任務(wù)轻局。
    initialDelay只有在第一次運(yùn)行定時(shí)任務(wù)前有效洪鸭,不會(huì)對(duì)后續(xù)定時(shí)任務(wù)有影響。

:以上屬性對(duì)應(yīng)的字符串屬性(比如嗽交,fixedRate對(duì)應(yīng)的字符串屬性為fixedRateString)卿嘲,其作用是一樣的,只是字符串屬性可以從外部文件中進(jìn)行配置夫壁,比如可以把定時(shí)任務(wù)寫到配置文件中拾枣,然后在代碼中使用:

  1. 配置文件Application.yml
scheduler:
  fixedRate:
    timeInMilliseconds: 3000
  1. 代碼中引入配置文件配置:
@Scheduled(fixedRateString = "${scheduler.fixedRate.timeInMilliseconds}")
public void taskFromExternal(){
    logger.info("Thread[{}] - taskFromExternal:{}", Thread.currentThread().getName(), new Date());
}

并發(fā)調(diào)度定時(shí)任務(wù)

需要注意的一點(diǎn)是,默認(rèn)情況下盒让,定時(shí)任務(wù)是串行運(yùn)行的(具體原因可參考后文內(nèi)容:源碼分析)梅肤,因此,哪怕即使使用的是fixedRate邑茄,也有可能因?yàn)槎〞r(shí)任務(wù)內(nèi)部存在耗時(shí)操作等行為導(dǎo)致調(diào)度出現(xiàn)誤差姨蝴,比如當(dāng)該耗時(shí)操作時(shí)間超過(guò)定時(shí)任務(wù)間隔時(shí),就會(huì)導(dǎo)致系統(tǒng)中的定時(shí)任務(wù)調(diào)度間隔不準(zhǔn)確肺缕。比如如下例子:

@Scheduled(fixedRate = 2000)
public void task01() throws InterruptedException {
    // 模擬耗時(shí)操作
    Thread.sleep(3000);
    logger.info("Thread[{}] - task01:{}", Thread.currentThread().getName(),new Date());
}

@Scheduled(fixedRate = 5000)
public void task02() {
    logger.info("Thread[{}] - task02:{}", Thread.currentThread().getName(),new Date());
}

運(yùn)行程序左医,可以看到如下結(jié)果:

可以看到,對(duì)于task01同木,我們?cè)O(shè)置的調(diào)度間隔是 2 秒浮梢,時(shí)間的運(yùn)行間隔為 3 秒,原因就是定時(shí)任務(wù)task01內(nèi)部耗時(shí)操作超過(guò)了本身的調(diào)度時(shí)間間隔彤路,而又由于 Spring 定時(shí)任務(wù)默認(rèn)是串行運(yùn)行秕硝,從而也影響了系統(tǒng)中其他定時(shí)任務(wù),比如定時(shí)任務(wù)task02設(shè)置的調(diào)度時(shí)間為 5 秒洲尊,但實(shí)際運(yùn)行間隔卻為 6 秒远豺。

為了減少系統(tǒng)中的定時(shí)任務(wù)互相影響奈偏,最好讓定時(shí)任務(wù)都運(yùn)行在獨(dú)立的線程中,也即并發(fā)運(yùn)行定時(shí)任務(wù)躯护,這樣惊来,即使其中一個(gè)或多個(gè)定時(shí)任務(wù)出問(wèn)題,也不會(huì)影響到系統(tǒng)其他定時(shí)任務(wù)棺滞。

一個(gè)很方便的事是唁盏,剛好 Spring 也提供了異步功能支持,我們之前的文章也進(jìn)行了介紹:Spring Boot - 執(zhí)行異步任務(wù)检眯。

簡(jiǎn)單來(lái)講,對(duì)于定時(shí)任務(wù)昆淡,Spring 提供了TaskScheduler接口進(jìn)行抽象锰瘸,同時(shí)借助注解@EnableScheduling@Scheduled就可以開啟并設(shè)置定時(shí)任務(wù)。
而對(duì)于異步任務(wù)昂灵,Spring 同樣提供了一個(gè)接口TaskExecutor進(jìn)行抽象避凝,同時(shí)借助注解@EnableAsync@Async就可以開啟并設(shè)置異步任務(wù)。

所以要將定時(shí)任務(wù)設(shè)置為并發(fā)調(diào)度眨补,只需開啟異步任務(wù)并為其增添異步執(zhí)行即可管削,如下所示:

  1. 開啟異步任務(wù)支持:
@Configuration
@EnableAsync // 開啟異步任務(wù)
public class AsyncConfigure implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        return Executors.newCachedThreadPool();
    }
}
  1. 為定時(shí)任務(wù)增加異步執(zhí)行功能:
@Scheduled(fixedRate = 2000)
@Async // 異步定時(shí)任務(wù)
public void task01() throws InterruptedException {
    // 模擬耗時(shí)操作
    Thread.sleep(3000);
    logger.info("Thread[{}] - task01:{}", Thread.currentThread().getName(), new Date());
}

@Scheduled(fixedRate = 5000)
@Async // 異步定時(shí)任務(wù)
public void task02() {
    logger.info("Thread[{}] - task02:{}", Thread.currentThread().getName(), new Date());
}

此時(shí)運(yùn)行程序,結(jié)果如下圖所示:

可以看到撑螺,異步任務(wù)的調(diào)度間隔符合我們的設(shè)置含思,即使對(duì)于自身內(nèi)部運(yùn)行超過(guò)定時(shí)間隔的任務(wù)時(shí),也會(huì)新開一條線程執(zhí)行新的定時(shí)任務(wù)甘晤,不會(huì)由于上一個(gè)任務(wù)的超時(shí)而導(dǎo)致系統(tǒng)中其他定時(shí)任務(wù)受到影響含潘。

:Spring 對(duì)異步任務(wù)和定時(shí)任務(wù)的抽象和實(shí)現(xiàn)十分類似,比如對(duì)于異步任務(wù)线婚,使用TaskExecutor進(jìn)行抽象遏弱,且只需提供@EnableAsync@Async就可以開啟并設(shè)置異步任務(wù),而對(duì)于定時(shí)任務(wù)塞弊,也是同樣的套路漱逸,使用TaskScheduler進(jìn)行抽象,且只需提供@EnableScheduling@Scheduled就可以開啟并設(shè)置定時(shí)任務(wù)...
在我們之前的文章(Spring Boot - 執(zhí)行異步任務(wù))中有提及到游沿,Spring 提供了一個(gè)接口AsyncConfigurer可以讓我們對(duì)異步任務(wù)進(jìn)行更加細(xì)粒度的設(shè)置饰抒,同樣的,對(duì)于定時(shí)任務(wù)奏候,Spring 也提供了一個(gè)類似的接口SchedulingConfigurer循集,可以讓我們對(duì)定時(shí)任務(wù)進(jìn)行更加細(xì)粒度的設(shè)置,有時(shí)候使用@Scheduled注解無(wú)法解決的問(wèn)題蔗草,比如動(dòng)態(tài)改變定時(shí)時(shí)間等咒彤,就可以通過(guò)SchedulingConfigurer進(jìn)行配置疆柔。這里,對(duì)于定時(shí)任務(wù)并發(fā)調(diào)度镶柱,我們也可以通過(guò)實(shí)現(xiàn)該接口進(jìn)行實(shí)現(xiàn)旷档,如下代碼所示:

@Configuration
@EnableScheduling
public class ScheduledConfigure implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(taskExecutor());
    }

    @Bean
    public Executor taskExecutor() {
        return Executors.newScheduledThreadPool(100);
    }
}

這樣配置后,系統(tǒng)中所有的定時(shí)任務(wù)無(wú)需添加@Async就會(huì)自動(dòng)運(yùn)行在線程池中歇拆。

:這里需要注意的一個(gè)點(diǎn)是鞋屈,ScheduledTaskRegistrar.setScheduler方法只支持TaskSchedulerScheduledExecutorService,因此這里不是采用前文的Executors.newCachedThreadPool故觅,而是使用Executors.newScheduledThreadPool厂庇,這兩者的效果有一點(diǎn)不同,具體運(yùn)行結(jié)果如下所示:

可以看到输吏,定時(shí)任務(wù)task01設(shè)置的調(diào)度間隔是 2 秒权旷,實(shí)際時(shí)間卻是 3 秒,出現(xiàn)這種狀況的原因是ScheduledExecutorService.scheduleAtFixedRate方法的時(shí)間間隔是上一次運(yùn)行成功后贯溅,才開始計(jì)時(shí)拄氯,也就是,ScheduledExecutorService的執(zhí)行方式是fixedDelay模式它浅,因此译柏,只有在前一個(gè)任務(wù)結(jié)束后,才會(huì)開啟下一個(gè)任務(wù)姐霍,所以就導(dǎo)致了上述現(xiàn)象鄙麦,即不同任務(wù)可以并發(fā)調(diào)度,但是同一個(gè)任務(wù)只能串行調(diào)度邮弹,所以如果這種效果不是你所期望的黔衡,則應(yīng)當(dāng)采用上文介紹的結(jié)合異步任務(wù)來(lái)完成定時(shí)任務(wù)并發(fā)調(diào)度。

源碼分析

這里我們簡(jiǎn)單對(duì)定時(shí)任務(wù)的整個(gè)過(guò)程做一個(gè)簡(jiǎn)要分析腌乡,如下所示:

首先盟劫,Spring 中的定時(shí)任務(wù)是通過(guò)注解@EnableScheduling開啟的,所以查看下該注解源碼:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {

}

很明顯看到与纽,@EnableScheduling注解的主要操作就是導(dǎo)入了一個(gè)配置類SchedulingConfiguration.class侣签,查看下該配置類源碼:

@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)建了一個(gè)ScheduledAnnotationBeanPostProcessor的 Bean 實(shí)例,其源碼如下所示:

public class ScheduledAnnotationBeanPostProcessor
        implements MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor,ApplicationListener<ContextRefreshedEvent>,... {

    // 構(gòu)造函數(shù)
    public ScheduledAnnotationBeanPostProcessor() {
        this.registrar = new ScheduledTaskRegistrar();
    }

    // 掃描 @Scheduled 注解并創(chuàng)建相應(yīng)的方法實(shí)例
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        ...
        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);
            });
        ...
        // Non-empty set of methods
        annotatedMethods.forEach((method, scheduledMethods) ->
                scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
        ...
    }

    // 解析 @Scheduled 并封裝定時(shí)任務(wù)到 ScheduledTaskRegistrar
    protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
        ...
        Set<ScheduledTask> tasks = new LinkedHashSet<>(4);

        // Determine initial delay
        long initialDelay = scheduled.initialDelay();
        String initialDelayString = scheduled.initialDelayString();
        ...
        // Check cron expression
        String cron = scheduled.cron();
        if (StringUtils.hasText(cron)) {
            ...
            tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
            }
        }
        ...
        // Check fixed delay
        long fixedDelay = scheduled.fixedDelay();
        if (fixedDelay >= 0) {
            ...
            tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
        }
        String fixedDelayString = scheduled.fixedDelayString();
        if (StringUtils.hasText(fixedDelayString)) {
            ...
            tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
            }
        }
        // Check fixed rate
        long fixedRate = scheduled.fixedRate();
        if (fixedRate >= 0) {
            ...
            tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
        }
        String fixedRateString = scheduled.fixedRateString();
        if (StringUtils.hasText(fixedRateString)) {
            ...
            tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
            }
        }

        // Check whether we had any attribute set
        Assert.isTrue(processedSchedule, errorMessage);

        // Finally register the scheduled tasks
        synchronized (this.scheduledTasks) {
            Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
            regTasks.addAll(tasks);
        }
        ...
    }
}

ScheduledAnnotationBeanPostProcessor創(chuàng)建的時(shí)候首先會(huì)構(gòu)造一個(gè)ScheduledTaskRegistrar實(shí)例急迂,由其內(nèi)部成員registrar持有影所。
然后,ScheduledAnnotationBeanPostProcessor類實(shí)現(xiàn)了BeanPostProcessor接口僚碎,其中猴娩,postProcessAfterInitialization方法會(huì)掃描@Scheduled注解并創(chuàng)建相應(yīng)的方法實(shí)例,該方法內(nèi)部是通過(guò)調(diào)用processScheduled方法對(duì)注解@Scheduled的內(nèi)容進(jìn)行解析,processScheduled解析完@Scheduled后卷中,會(huì)將其封裝到相應(yīng)的定時(shí)任務(wù)實(shí)例中矛双,并將這些定時(shí)任務(wù)添加到ScheduledTaskRegistrar實(shí)例中。

一個(gè)完整的流程是:當(dāng) Spring 啟動(dòng)時(shí)蟆豫,AbstractApplicationContext中的finishRefresh會(huì)觸發(fā)所有監(jiān)視者方法回調(diào)议忽,其中,publishEvent會(huì)觸發(fā)ScheduledAnnotationBeanPostProcessoronApplicationEvent方法(由于ScheduledAnnotationBeanPostProcessor實(shí)現(xiàn)了ApplicationListener十减,因此有該接口方法)栈幸,查看下該方法源碼:


public class ScheduledAnnotationBeanPostProcessor implements ApplicationListener<ContextRefreshedEvent>,...{
    ...
        @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();
            }
        }
    ...
}

可以看到,onApplicationEvent方法內(nèi)部調(diào)用了finishRegistration方法帮辟,finishRegistration主要用于查找并設(shè)置TaskScheduler(注冊(cè)調(diào)度器TaskScheduler)速址,也就是 Spring 對(duì)異步任務(wù)的抽象封裝類。其源碼如下所示:

private void finishRegistration() {
    if (this.scheduler != null) {
        this.registrar.setScheduler(this.scheduler);
    }

    if (this.beanFactory instanceof ListableBeanFactory) {
        // 如果配置了定時(shí)任務(wù) Bean: SchedulingConfigurer
        Map<String, SchedulingConfigurer> beans =
                ((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
        List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());
        AnnotationAwareOrderComparator.sort(configurers);
        for (SchedulingConfigurer configurer : configurers) {
            // 回調(diào)
            configurer.configureTasks(this.registrar);
        }
    }

    if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
        Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type");
        try {
            // Search for TaskScheduler bean...
            this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
        }
        catch (NoUniqueBeanDefinitionException ex) {
            logger.trace("Could not find unique TaskScheduler bean", ex);
            try {
                this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true));
            }
            catch (NoSuchBeanDefinitionException ex2) {
                ...
        }
        catch (NoSuchBeanDefinitionException ex) {
            logger.trace("Could not find default TaskScheduler bean", ex);
            // Search for ScheduledExecutorService bean next...
            try {
                this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false));
            }
            catch (NoUniqueBeanDefinitionException ex2) {
                logger.trace("Could not find unique ScheduledExecutorService bean", ex2);
                ...
            }
            catch (NoSuchBeanDefinitionException ex2) {
                logger.trace("Could not find default ScheduledExecutorService bean", ex2);
                // Giving up -> falling back to default scheduler within the registrar...
                logger.info("No TaskScheduler/ScheduledExecutorService bean found for scheduled processing");
            }
        }
    }

    this.registrar.afterPropertiesSet();
}

finishRegistration方法的邏輯很清晰由驹,它對(duì)定時(shí)任務(wù)調(diào)度器TaskScheduler的查找過(guò)程主要有 3 大步驟:

  1. 如果ScheduledAnnotationBeanPostProcessor本身設(shè)置了調(diào)度器壳繁,則將該調(diào)度器設(shè)置給ScheduledTaskRegistrar,具體代碼如下所示:
if (this.scheduler != null) {
    this.registrar.setScheduler(this.scheduler);
}
  1. 如果用戶配置了定時(shí)任務(wù)配置類SchedulingConfigurer荔棉,則回調(diào)配置類的configureTasks方法:
if (this.beanFactory instanceof ListableBeanFactory) {
    Map<String, SchedulingConfigurer> beans =
            ((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
    List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());
    AnnotationAwareOrderComparator.sort(configurers);
    for (SchedulingConfigurer configurer : configurers) {
        configurer.configureTasks(this.registrar);
    }
}
  1. 這是 Spring 默認(rèn)的搜索行為,其具體搜索邏輯如下:
    1). 首先全局搜索唯一的TaskScheduler類型 Bean 實(shí)例:

    // Search for TaskScheduler bean...
    this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
    

    2). 如果存在多個(gè)TaskScheduler類型 Bean蒿赢,則搜索名稱為taskScheduler的 Bean 實(shí)例:

    private void finishRegistration() {
        ...
        this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true));
        ...
    }
    
    public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler";
    private <T> T resolveSchedulerBean(BeanFactory beanFactory, Class<T> schedulerType, boolean byName) {
        if (byName) {
            T scheduler = beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, schedulerType);
            if (this.beanName != null && this.beanFactory instanceof ConfigurableBeanFactory) {
                ((ConfigurableBeanFactory) this.beanFactory).registerDependentBean(
                        DEFAULT_TASK_SCHEDULER_BEAN_NAME, this.beanName);
            }
            return scheduler;
        }
        ...
    }
    

    也就是說(shuō)润樱,如果存在多個(gè)TaskScheduler Bean 實(shí)例,則選擇名稱為taskScheduler的實(shí)例羡棵。

    3). 如果不存在TaskScheduler 類型 Bean 實(shí)例壹若,就降級(jí)轉(zhuǎn)而查找ScheduledExecutorService 類型 Bean 實(shí)例:

    this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false));
    

    4). 如果存在多個(gè)ScheduledExecutorService 類型的 Bean 實(shí)例,則查找名稱為taskScheduler的 Bean 實(shí)例:

    this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, true));
    

    5). 如果TaskSchedulerScheduledExecutorService Bean 實(shí)例都不存在皂冰,則結(jié)束查找店展。

    綜上,finishRegistration查找邏輯為:優(yōu)先查找全局唯一TaskScheduler Bean 實(shí)例秃流,存在多個(gè)則選擇名稱為taskScheduler的實(shí)例赂蕴,否則降級(jí)查找ScheduledExecutorService類型 Bean 實(shí)例,存在多個(gè)則選擇名稱為taskScheduler的那個(gè) Bean 實(shí)例舶胀。

    對(duì)定時(shí)任務(wù)調(diào)度器TaskScheduler的查找都是通過(guò)方法resolveSchedulerBean進(jìn)行的概说,事實(shí)上,默認(rèn)情況下嚣伐,Spring 在啟動(dòng)過(guò)程中糖赔,會(huì)自動(dòng)幫我們注入一個(gè)TaskScheduler Bean 實(shí)例,對(duì)應(yīng)的代碼如下所示:

    private void finishRegistration() {
        ...
        this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
        ...
    }    
    
    private <T> T resolveSchedulerBean(BeanFactory beanFactory, Class<T> schedulerType, boolean byName) {
        ...
        else if (beanFactory instanceof AutowireCapableBeanFactory) {
            NamedBeanHolder<T> holder = ((AutowireCapableBeanFactory) beanFactory).resolveNamedBean(schedulerType);
            if (this.beanName != null && beanFactory instanceof ConfigurableBeanFactory) {
                ((ConfigurableBeanFactory) beanFactory).registerDependentBean(holder.getBeanName(), this.beanName);
            }
            return holder.getBeanInstance();
        }
        ...
    }
    

    所以默認(rèn)情況下轩端,Spring 提供了一個(gè)默認(rèn)的TaskScheduler放典,其實(shí)我們前文也有提及,這個(gè)默認(rèn)的TaskScheduler就是ThreadPoolTaskScheduler,所以默認(rèn)情況下奋构,會(huì)將ThreadPoolTaskScheduler注冊(cè)給ScheduledTaskRegistrar壳影,注冊(cè)的方法如下所示:

    // ScheduledTaskRegistrar.java
    public void setTaskScheduler(TaskScheduler taskScheduler) {
        Assert.notNull(taskScheduler, "TaskScheduler must not be null");
        this.taskScheduler = taskScheduler;
    }
    

    我們對(duì)該方法進(jìn)行單步調(diào)式,就可以看到ThreadPoolTaskScheduler默認(rèn)的配置情況声怔,如下圖所示:

    taskScheduler

    可以看到态贤,默認(rèn)的定時(shí)任務(wù)調(diào)度器是一個(gè)名稱為taskSchedulerScheduledThreadPoolExecutorScheduledThreadPoolExecutor實(shí)現(xiàn)了ScheduledExecutorService)線程池實(shí)例,且其線程數(shù)為1醋火,線程前綴為scheduling-悠汽,所以默認(rèn)情況下,定時(shí)任務(wù)都是串行運(yùn)行的芥驳,且其線程名稱都為scheduling-1柿冲,另一個(gè)需要注意的點(diǎn)是,默認(rèn)的調(diào)度器其deamon = false兆旬,說(shuō)明其是一個(gè)后臺(tái)任務(wù)假抄,即使應(yīng)用主線程退出,定時(shí)任務(wù)仍然處于運(yùn)行之中丽猬。

最后宿饱,finishRegistration方法末尾還調(diào)用了afterPropertiesSet方法,如下所示:

// ScheduledAnnotationBeanPostProcessor.java
private void finishRegistration() {
    ...
    this.registrar.afterPropertiesSet();
}

// ScheduledTaskRegistrar.java
@Override
public void afterPropertiesSet() {
    scheduleTasks();
}

所以afterPropertiesSet最終是調(diào)用的是scheduleTasks脚祟,見(jiàn)名知意谬以,該方法用于調(diào)度已注冊(cè)的定時(shí)任務(wù),其源碼如下所示:

// ScheduledTaskRegistrar.java
@SuppressWarnings("deprecation")
protected void scheduleTasks() {
    if (this.taskScheduler == null) {
        this.localExecutor = Executors.newSingleThreadScheduledExecutor();
        this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
    }
    if (this.triggerTasks != null) {
        for (TriggerTask task : this.triggerTasks) {
            addScheduledTask(scheduleTriggerTask(task));
        }
    }
    if (this.cronTasks != null) {
        for (CronTask task : this.cronTasks) {
            addScheduledTask(scheduleCronTask(task));
        }
    }
    if (this.fixedRateTasks != null) {
        for (IntervalTask task : this.fixedRateTasks) {
            addScheduledTask(scheduleFixedRateTask(task));
        }
    }
    if (this.fixedDelayTasks != null) {
        for (IntervalTask task : this.fixedDelayTasks) {
            addScheduledTask(scheduleFixedDelayTask(task));
        }
    }
}

private final Set<ScheduledTask> scheduledTasks = new LinkedHashSet<>(16);

private void addScheduledTask(@Nullable ScheduledTask task) {
    if (task != null) {
        this.scheduledTasks.add(task);
    }
}

可以看到由桌,scheduledTasks方法對(duì)注冊(cè)的不同的定時(shí)任務(wù)分別進(jìn)行調(diào)度为黎,調(diào)度的方法為scheduleXXXTask,比如行您,對(duì)于fixedRate的定時(shí)任務(wù)铭乾,其對(duì)應(yīng)的調(diào)用方法為scheduleFixedRateTask,每次調(diào)度完成一個(gè)方法后娃循,都會(huì)將調(diào)度結(jié)果通過(guò)方法addScheduledTask添加到一個(gè)集合中scheduledTasks炕檩。
這里我們主要對(duì)調(diào)度方法進(jìn)行分析,就分析一下scheduleFixedRateTask捌斧,其余的調(diào)度方法與之類似:

@Deprecated
@Nullable
public ScheduledTask scheduleFixedRateTask(IntervalTask task) {
    FixedRateTask taskToUse = (task instanceof FixedRateTask ? (FixedRateTask) task :
            new FixedRateTask(task.getRunnable(), task.getInterval(), task.getInitialDelay()));
    return scheduleFixedRateTask(taskToUse);
}

scheduleFixedRateTask(IntervalTask)方法內(nèi)部最終是通過(guò)調(diào)用重載函數(shù)scheduleFixedRateTask(FixedRateTask)來(lái)完成調(diào)度捧书,其源碼如下:

@Nullable
public ScheduledTask scheduleFixedRateTask(FixedRateTask task) {
    ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
    ...
    scheduledTask.future =
            this.taskScheduler.scheduleAtFixedRate(task.getRunnable(), task.getInterval());
    ...
    return (newTask ? scheduledTask : null);
}

這里就可以看到了,最終是通過(guò)定時(shí)任務(wù)調(diào)度器taskSchedulerscheduleAtFixedRate來(lái)完成定時(shí)任務(wù)調(diào)度骤星。

到此经瓷,源碼分析基本已完成。

附錄

  • cron 表達(dá)式:cron 表達(dá)式的語(yǔ)法如下所示:

    <秒> <分> <小時(shí)> <日> <月> <周> [年]

    其中洞难,各字段允許的值和特殊符號(hào)如下表所示:

    字段 允許值 允許的特殊字符
    0~59 , - * /
    0~59 , - * /
    小時(shí) 0~23 , - * /
    日期 0~31 , - * ? / L W C
    月份 1~12 或者 JAN~DEC , - * /
    星期 1~7 或者 SUN~SAT , - * ? / L C #
    年(可選) 留空舆吮,1970~2099 , - * /

    上表中的特殊符號(hào)釋義如下:

    • ,:表示枚舉值。比如:假設(shè)小時(shí)為10,14,16,則表示上午 10 時(shí)色冀,下午 2 時(shí) 以及 下午 4 時(shí)各觸發(fā)一次潭袱。
    • -:表示間隔時(shí)間。比如:假設(shè)小時(shí)為8-12锋恬,則表示上午 8 時(shí)到中午 12 時(shí)每個(gè)小時(shí)時(shí)間段都進(jìn)行觸發(fā)屯换。
    • *:表示匹配所有可能值。
    • /:表示增量与学。比如:假設(shè)分鐘為3/20彤悔,則表示從第 3 分鐘開始,以后每隔 20 分鐘觸發(fā)一次索守。
    • ?:僅被用于月和周兩個(gè)子表達(dá)式晕窑,表示不指定值。
    • L:僅被用于月和周兩個(gè)子表達(dá)式卵佛,它是單詞“l(fā)ast”的縮寫杨赤。如果在“L”前有具體的內(nèi)容,它就具有其他的含義了截汪,比如:假設(shè)星期字段為6L疾牲,則表示每個(gè)月的倒數(shù)第 6 天。
    • W:表示工作日(Mon-Fri)衙解,并且僅能用于日域中说敏。
    • C:表示日期(Calendar)意思。比如:5C表示日歷 5 日以后的第一天丢郊,1C在星期字段相當(dāng)于星期日后的第一天。

    以下是一些常用的 cron 表達(dá)式:

    • 每隔5秒執(zhí)行一次:*/5 * * * * ?
    • 每隔1分鐘執(zhí)行一次:0 */1 * * * ?
    • 每天上午10點(diǎn)医咨,下午2點(diǎn)枫匾,4點(diǎn):0 0 10,14,16 * * ?
    • 朝九晚五工作時(shí)間內(nèi)每半小時(shí):0 0/30 9-17 * * ?
    • 表示每個(gè)星期三中午12點(diǎn):0 0 12 ? * WED
    • 每天中午12點(diǎn)觸發(fā):0 0 12 * * ?
    • 每天上午10:15觸發(fā):0 15 10 ? * *
    • 每天上午10:15觸發(fā):0 15 10 * * ?
    • 每天上午10:15觸發(fā):0 15 10 * * ?
    • 2005 2005年的每天上午10:15觸發(fā):0 15 10 * * ?
    • 在每天下午2點(diǎn)到下午2:59期間的每1分鐘觸發(fā):0 * 14 * * ?
    • 在每天下午2點(diǎn)到下午2:55期間的每5分鐘觸發(fā):0 0/5 14 * * ?
    • 在每天下午2點(diǎn)到2:55期間和下午6點(diǎn)到6:55期間的每5分鐘觸發(fā):0 0/5 14,18 * * ?
    • 在每天下午2點(diǎn)到下午2:05期間的每1分鐘觸發(fā):0 0-5 14 * * ?
    • 每年三月的星期三的下午2:10和2:44觸發(fā):0 10,44 14 ? 3 WED
    • 周一至周五的上午10:15觸發(fā):0 15 10 ? * MON-FRI
    • 每月15日上午10:15觸發(fā):0 15 10 15 * ?
    • 每月最后一日的上午10:15觸發(fā):0 15 10 L * ?
    • 每月的最后一個(gè)星期五上午10:15觸發(fā):0 15 10 ? * 6L
    • 2002年至2005年的每月的最后一個(gè)星期五上午10:15觸發(fā):0 15 10 ? * 6L 2002-2005
    • 每月的第三個(gè)星期五上午10:15觸發(fā):0 15 10 ? * 6#3

    比如在代碼中配置如下:

    // 每隔 5s 執(zhí)行一次
    @Scheduled(cron = "*/5 * * * * ?")
    public void cronTask(){
        logger.info("Thread[{}] - cronTask:{}", Thread.currentThread().getName(), new Date());
    }
    

    以上就通過(guò) cron 表達(dá)式就設(shè)置了一個(gè)每隔 5 秒運(yùn)行的定時(shí)任務(wù)。

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末拟淮,一起剝皮案震驚了整個(gè)濱河市干茉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌很泊,老刑警劉巖角虫,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異委造,居然都是意外死亡戳鹅,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門昏兆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)枫虏,“玉大人,你說(shuō)我怎么就攤上這事×フ” “怎么了腾它?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)死讹。 經(jīng)常有香客問(wèn)我瞒滴,道長(zhǎng),這世上最難降的妖魔是什么赞警? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任妓忍,我火速辦了婚禮,結(jié)果婚禮上仅颇,老公的妹妹穿的比我還像新娘单默。我一直安慰自己,他們只是感情好忘瓦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布搁廓。 她就那樣靜靜地躺著,像睡著了一般耕皮。 火紅的嫁衣襯著肌膚如雪境蜕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天凌停,我揣著相機(jī)與錄音粱年,去河邊找鬼。 笑死罚拟,一個(gè)胖子當(dāng)著我的面吹牛台诗,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播赐俗,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼拉队,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了阻逮?” 一聲冷哼從身側(cè)響起粱快,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎叔扼,沒(méi)想到半個(gè)月后事哭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瓜富,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年鳍咱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片与柑。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡伶唯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出薄辅,到底是詐尸還是另有隱情驹饺,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏妹蔽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一挠将、第九天 我趴在偏房一處隱蔽的房頂上張望胳岂。 院中可真熱鬧,春花似錦舔稀、人聲如沸乳丰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)产园。三九已至,卻和暖如春夜郁,著一層夾襖步出監(jiān)牢的瞬間什燕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工竞端, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留屎即,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓事富,卻偏偏與公主長(zhǎng)得像技俐,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子统台,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354