前言
對(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)行如下兩步操作:
-
使用注解
@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
注解方法焦蘑。 -
為需要定期執(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)容如下圖所示:
以下我們只對(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í)間涮雷。可用在于fixedRate
和fixedDelay
的定時(shí)任務(wù)轻局。
注:initialDelay
只有在第一次運(yùn)行定時(shí)任務(wù)前有效洪鸭,不會(huì)對(duì)后續(xù)定時(shí)任務(wù)有影響。
注:以上屬性對(duì)應(yīng)的字符串屬性(比如嗽交,fixedRate
對(duì)應(yīng)的字符串屬性為fixedRateString
)卿嘲,其作用是一樣的,只是字符串屬性可以從外部文件中進(jìn)行配置夫壁,比如可以把定時(shí)任務(wù)寫到配置文件中拾枣,然后在代碼中使用:
- 配置文件
Application.yml
:
scheduler:
fixedRate:
timeInMilliseconds: 3000
- 代碼中引入配置文件配置:
@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í)行即可管削,如下所示:
- 開啟異步任務(wù)支持:
@Configuration
@EnableAsync // 開啟異步任務(wù)
public class AsyncConfigure implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
return Executors.newCachedThreadPool();
}
}
- 為定時(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
方法只支持TaskScheduler
和ScheduledExecutorService
,因此這里不是采用前文的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ā)ScheduledAnnotationBeanPostProcessor
的onApplicationEvent
方法(由于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 大步驟:
- 如果
ScheduledAnnotationBeanPostProcessor
本身設(shè)置了調(diào)度器壳繁,則將該調(diào)度器設(shè)置給ScheduledTaskRegistrar
,具體代碼如下所示:
if (this.scheduler != null) {
this.registrar.setScheduler(this.scheduler);
}
- 如果用戶配置了定時(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);
}
}
-
這是 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). 如果
TaskScheduler
和ScheduledExecutorService
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è)名稱為
taskScheduler
的ScheduledThreadPoolExecutor
(ScheduledThreadPoolExecutor
實(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)度器taskScheduler
的scheduleAtFixedRate
來(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ù)。
-