咱们好,我是贰师兄,好久不见。因为贰师兄最近开端写聊聊Mysql这个专栏,为避免传递一些错误的常识,误导咱们,所以在刷Mysql的源码。这个进程消耗的时刻比较多,自然更新的速度也会慢一些,请咱们见谅。
因为新的专栏还在准备中,本文咱们仍是聊一聊Spring的论题,本文咱们聊一下Spring守时使命调度。这个主题,信任咱们都比较了解,平时用的应该也比较多。不过说起完结原理,了解的小伙伴应该就不太多了,网上对应的文章尽管许多,不过根本都是运用教程。即便有少部分提及原理,说的也是含糊不清,更有甚者是胡说八道。
其实Spring的守时使命调度,并不神秘,也不是Spring特有的,实质仍是凭借JDK的才能完结的,仅仅在运用办法上更Spring一点,也便是更简练、更便捷一点。
关于JDK的守时使命,主要是凭借
ScheduledThreadPoolExecutor
完结的,感兴趣的小伙伴能够能够自行了解其完结原理。这儿咱们主要聊Spring的相关细节,关于JDK部分咱们不会详细介绍。当然,假如你们有需求,恰巧我也有时刻的话,也能够拿出来说一说,究竟说啥不是说呢,是吧。
1. Spring守时使命的类型
关于Spring守时使命的运用,小伙伴们应该都比较了解,便是直接在办法上加上@Scheduled
注解即可。Spring会依据你指定的频率,守时调度该办法的履行,这一点完全不用你关心。
运用自然是很简略的,这是Spring一向的风格。关于Spring是怎样做的,读过贰师兄前面文章的小伙伴应该也能够猜到,至少得先把这些标示了@Scheduled
注解的守时办法找出来,然后在想办法让它守时履行。当然前面咱们也说了,这部分是凭借JDK的才能来完结的。
不了解这个路数的小伙伴,能够参阅聊透Spring工作机制中@EventListener注解的解析和注册进程,套路是相同的。
可是在运用Spring守时调度的进程,也有一些细节需求先和小伙伴们介绍清楚。首要便是Spring支撑的三种类型使命的履行逻辑,这儿恐怕能说清楚的小伙伴们不多,尤其是在碰到单线程模型,咱们先整理一下:
Spring支撑CRON表达式类型、fixedDelay距离履行、fixedRate距离履行三种使命类型。关于这三种类型在使命履行上的差别,咱们一一介绍一下。
1.1 CRON表达式类型使命
关于CRON表达式含义,这儿咱们不再介绍,信任小伙伴们都比较了解,真实不了解自行查阅材料吧。
咱们要说的是,在单线程履行的状况下,假如CRON使命履行时刻过长,以至于下次履行的时刻都到了,可是上次使命还没有履行完毕,下次使命要怎样办。
这儿先给定论:抛弃
,也便是下一次使命履行就被抛弃了,也便是少履行了一次
。这儿拿使命设置为每五秒履行一次的表达式,阐明一下:
- 假定10:00:00s时,使命榜首次履行,可是使命履行时刻很长,履行了7s。
- 依据使命的履行方案,10:00:05s时,应该要履行第2次使命,可是此刻发现有使命在履行(上一次使命需求履行到10:00:07s),那么,
此次履行方案直接抛弃,也便是本次使命不履行了
。 - 依据使命的履行方案,10:00:10s时,应该要履行第三次使命,此刻发现没有使命履行,本次使命正常履行。
这儿咱们必定要留意,单线程模型下,因为榜首次使命履行时刻较长,导致第2次使命不履行,也便是少履行了一次。这儿或许会影响预期、从而产生事务影响。
1.2 fixedDelay距离类型使命
fixedDelay是最简略的一种办法模型,距离履行:也便是推迟指定的距离后,再次履行下次使命。核算公式为:下次履行时刻 = 上次使命履行完毕时刻 + 距离时刻
。相同的问题:假如使命履行时刻较长,下次履行时刻也会晚于预期。这儿以使命距离为五秒,阐明一下:
- 假定10:00:00s时,使命榜首次履行,使命履行时刻较长,履行了7s。
- 榜首次使命履行完毕后(10:00:07),等候5s后,再次履行下一次使命(10:00:12),第2次使命履行了3s。
- 第2次使命履行完毕后(10:00:15),等候5s后,再次履行下一次使命,依此类推。
这儿需求留意,单线程模型下,假如存在使命履行时刻较长,全体的履行方案都会往后顺延
。
1.3 fixedRate距离类型使命
fixedRate也是距离履行的办法,仅仅这个距离不是依照使命完毕时刻核算的,而是依照开端时刻。核算公式为:下次履行时刻 = 上次使命履行开端时刻 + 距离时刻
。当然,假如使命履行时刻较长,超过距离时刻,下次履行时刻也要顺延,究竟不能强暴的直接打断吧。
不过fixedRate会将距离会主动缩小,尽量追赶方案履行时刻,一旦赶上或许追平,持续依照指定距离履行。这儿仍是以距离为五秒的状况,阐明一下:
- 假定10:00:00s时,使命榜首次履行,使命履行时刻较长,履行了7s。
- 依据使命的履行方案,10:00:05s时,应该要履行第2次使命,可是此刻榜首次使命还在履行中,所以第2次履行时刻只能等候顺延。
- 榜首次使命履行完毕后(10:00:07),发现现已晚于第2次履行的方案时刻了。会追赶进展,所以第2次使命立即履行。
- 这儿假定第2次使命只需求履行2s,在10:00:09就履行完毕了。方案第三次履行时刻为:10:00:10,也便是第2次使命现已追平了,无需持续追赶,此刻会遵从方案,在10:00:10时,正常履行第三次使命。
这儿需求留意,fixedRate会主动调整距离,使使命尽快追平方案时刻,追平后遵从方案履行
。当然这儿评论的也是单线程模型下。
好了,关于守时使命的三种类型的评论就这么多。咱们留意在单线程模型下,上面的评论才有含义。咱们清楚不同使命类型,发生使命履行时刻过长,对下次履行时刻的影响即可。再次着重,是单线程模型下,假如是多线程履行,影响状况需求结合线程池装备剖析了,这儿咱们不具有评论条件。
这儿为什么执着的评论单线程模型,因为Spring默许的便是单线程模型,而往往咱们又不指定调度线程池。所以其实单线程模型才是最最常用的。
2. @Scheduled注解解析
经过上一章节对Spring三种守时使命类型的介绍,信任小伙伴们现已很清楚他们之间的区别了。在Spring中,守时使命都是由@Scheduled标识的,三种使命类型分别对应@Scheduled的三种特点,分别是cron
、fixedDelay
、fixedRate
,设置对应的值,即为敞开对应类型的使命。
咱们在上面也介绍过了,Spring要履行这些守时使命,榜首步便是需求先解析出来这些守时使命,然后才干交由JDK处理。那么本章节咱们就来看一下解析进程。
2.1 @EnableScheduling敞开使命调度功用
在探求解析流程之前,咱们先介绍一下@EnableScheduling
。咱们知道,在运用Spring的守时使命调度功用前,是需求在类上先增加@EnableScheduling敞开的,这究竟有什么用呢。
关于Spring的@EnableXXX
,一般届时敞开某种才能,比方EnableScheduling敞开守时使命调度、
@EnableAsync敞开异步调用等。其实原理也很简略,都是凭借@Import
才能导入某些BeanPostProcessor
(也有或许是其他类型的),这些BeanPostProcessor,会在bean的生命周期的各个流程发挥重要作用,从而使Spring具有强壮的才能。
要害这个进程完全是可插拔的,加入某个BeanPostProcessor,就具有响应才能了,拓宽才能极强。这也正是@EnableXXX
的原理,能够简略理解为:敞开某项功用。
BeanPostProcessor
是Spring留给咱们的一种拓宽办法,才能非常强悍,甚至许多Spring的中心才能,比方@AutoWired
和@Resource
特点注入,都是凭借它完结的。当然,@EnableXXX
+@Import
的组合不只仅能导入BeanPostProcessor,导入普通装备类也是能够的,并没有导入类型方面的限制。
下面咱们看一下@EnableScheduling的做法,完结和咱们上面说的相同的,他运用@Import
导入了SchedulingConfiguration
这个装备类,这个装备类中运用@Bean将ScheduledAnnotationBeanPostProcessor
目标放入到Spring容器中。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class) // 导入SchedulingConfiguration
@Documented
public @interface EnableScheduling {
}
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {
//运用@Bean将ScheduledAnnotationBeanPostProcessor目标实例放入Spring容器
@Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
return new ScheduledAnnotationBeanPostProcessor();
}
}
这儿的ScheduledAnnotationBeanPostProcessor
必定是Spring具有守时使命调度才能的要害。正如咱们前面提到的,它的确是承继了BeanPostProcessor
,也便是大名鼎鼎的Bean的后置处理器。的确,这个bean的后置处理器,负责了@Scheduled标示的守时办法的解析、封装、调度履行等全部功用。没有它,这些作业没有人做,的确就不具有守时调度才能了。
读过贰师兄聊透Spring依靠注入一文的小伙伴,必定立刻就能想到,支撑
@AutoWired
特点注入的AutowiredAnnotationBeanPostProcessor
,和支撑@Resource
特点注入的CommonAnnotationBeanPostProcessor
,不都是BeanPostProcessor
嘛。
这儿贰师兄在啰嗦一下,关于bean的后置处理器,咱们真的有必要去研究一下,其实Spring的许多强壮功用,都是依靠各个不同的BeanPostProcessor
构筑出来的。他作用于bean的生命周期中的各个阶段,对bean进行功用增强,从而使bean强壮无比,去聊透Spring bean的生命周期了解一下吧,求求你们了。
2.2 创立bean时解析@Scheduled注解办法
现在清楚了@EnableScheduling
的实质是将ScheduledAnnotationBeanPostProcessor
放到Spring容器中,它会负责@Scheduled标示的守时办法的解析、封装、调度履行等全部功用。那这些操作什么时分触发呢,又是怎样做的呢,是咱们接下来探求的要害。
关于@Scheduled解析流程,贰师兄翻开ScheduledAnnotationBeanPostProcessor
类,大致阅读了一下就找到是在postProcessAfterInitialization()
完结的。很明显,这儿上来便是反射查找@Scheduled的办法,不是他是谁,咱们先来简略看一下源码:
// ScheduledAnnotationBeanPostProcessor.java
public Object postProcessAfterInitialization(Object bean, String beanName) {
// 1:反射解析加了@Scheduled的办法
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);
});
// 2:处理加了@Scheduled的办法,(封装成调度使命)
annotatedMethods.forEach((method, scheduledMethods) ->
scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
// ...省略其他代码
}
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
// 将bean目标、办法信息封装为Runnable目标
Runnable runnable = createRunnable(bean, method);
// 处理cron表达式
String cron = scheduled.cron();
if (StringUtils.hasText(cron)) {
// 封装成ScheduledTask,保存到tasks中
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
}
// ...省略fixedDelay和fixedRate使命的解析
}
这儿的解析流程其实比较明晰,便是反射查找当前创立的bean,是否存在@Scheduled标示的守时使命办法
。假如存在,就对守时使命办法解析、封装处理。解析无非便是对@Scheduled字段的解析;关于为什么封装,这儿需求和小伙伴解释一下,后续守时调度这些办法是经过反射的办法,反射咱们比较了解,需求method和目标信息的,而守时使命履行时刻依靠@Scheduled注解信息,所以需求将三者封装成ScheduledTask
目标,先保存起来,供后续运用。
这儿之所以先保存起来,也是Spring一向的做法都是先解析暂存,后续再运用。之所以这样是因为Spring依靠联系杂乱,容器发动进程和bean创立进程中,每个流程都做了许多工作,一般没有办法在一个机遇做完某个功用所需的所有工作,所以会分散在各个流程里,这也是Spring源码杂乱最主要的原因。
关于@Scheduled解析机遇,解析办法都找到了,看一下调用联系就能够了,发现是在创立bean时,初始化回调时进行的,这也合理,创立bean时,解析一下bean中@Scheduled标示的守时办法。
3. @Scheduled守时使命调度履行
现在Spring现已将加了@Scheduled的调度使命解析出来了,那下一步便是调度使命的履行了。之前咱们也说过,这部分凭借的是JDK的守时使命调度才能,Spring仅仅做了交融翻译的作业,也便是把@Scheduled标示的守时办法,翻译成契合DJK规定的守时调度使命,再交由JDK的ScheduledThreadPoolExecutor履行
。
这儿能够看到,Spring所做的并不杂乱,不过仍是有一些细节需求留意,榜首个问题便是:履行使命的调度线程池从何而来
。咱们知道,对JDK守时使命调度而言,调度线程池至关重要,一般榜首步便是先创立一个ScheduledThreadPoolExecutor
,然后对这个调度线程池提交使命的。咱们先来看一下原生的做法:
这儿贰师兄没有自己写,而是从RocketMQ摘取了部分相关源码。rocketMQ中有很多的守时使命运用,也是运用JDK的才能,这儿咱们参阅一下:。
protected void initializeResources() {
// 1: 创立ScheduledThreadPoolExecutor
this.scheduledExecutorService = new ScheduledThreadPoolExecutor(1,
new ThreadFactoryImpl("BrokerControllerScheduledThread", true, getBrokerIdentity()));
}
protected void initializeBrokerScheduledTasks() {
// 2:提交使命到scheduledExecutorService,守时进行broker统计的使命
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
BrokerController.this.getBrokerStats().record();
} catch (Throwable e) {
LOG.error("BrokerController: failed to record broker stats", e);
}
}
}, initialDelay, period, TimeUnit.MILLISECONDS);
}
之所以挑选RocketMQ源码,主要是更具有代表性,免得咱们说我运用不标准。当然,别的一个原因也是贰师兄比较了解rocketMQ的源码,找起来比较快。
3.1 挑选适宜的线程池履行使命
相关于原生运用的直接创立调度线程池,Spring会有一些小小的麻烦,那便是挑选适宜的调度线程池
。假如用户指定了调度线程池还好,假如没有指定呢,否则履行了,仍是直接创立一个默许的,假如运用默许兜底,那线程数设置多少适宜呢,究竟线程数的设置要参阅:使命数和履行频率啊,这两个值每个项目又都不相同。
这儿贰师兄需求重点着重一下,给守时使命设置适宜的线程池非常重要,榜首章节就剖析过了,线程池设置的过小,会导致有些调度使命久久不能履行,从而影响数据的准确性,这一点小伙伴们必定要特别留意。
这儿Spring的做法是先看用户是否装备了调度线程池,假如装备了,运用用户装备的;假如没有装备,创立一个默许的,可是,Spring创立的默许调度线程池,是单线程的,是单线程,是单线程!!!
,重要的工作说三遍,贰师兄就在这个上面吃过亏,咱们一会看一下详细场景,加深一下小伙伴们的印象。
3.1.1 Spring查找调度线程池
咱们先来看一下Spring挑选调度线程池的逻辑,逻辑咱们现已说清楚了,直接在源码中验证一下。
private void finishRegistration() {
try {
// 2.1: 获取容器中装备的TaskScheduler,没有或存在多个,都会抛出反常
this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
}
catch (NoUniqueBeanDefinitionException ex) {
try {
//2.2 存在多个的话,再经过名称确认一个
this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true));
}
}
catch (NoSuchBeanDefinitionException ex) {
try {
// 2.3: 不存在TaskScheduler类型,获取ScheduledExecutorService类型
this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false));
}
catch (NoUniqueBeanDefinitionException ex2) {
try {
// 2.4: 获取多个ScheduledExecutorService,经过名字确认一个
this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, true));
}
catch (NoSuchBeanDefinitionException ex3) {
// 2.5: 没有打印日志即可
}
catch (NoSuchBeanDefinitionException ex2) {
// 2.5: 没有打印日志即可
}
}
// 调度使命履行,假如容器中不存在调度线程池,会创立默许线程池
this.registrar.afterPropertiesSet();
}
这部分的源码比较简略,经过resolveSchedulerBean()
去Spring容器查找特定类型的bean,查找不到会抛出NoSuchBeanDefinitionException
反常,查找到多个会抛出NoUniqueBeanDefinitionException
反常,这儿都进行了反常捕捉,再次处理对应的逻辑。
依照源码的逻辑,那便是:先查找TaskScheduler类型的bean
,假如不存在该类型的bean,再次尝试查找ScheduledExecutorService类型的bean
,真实查找不到,也便是打印一下了一下日志,而且仍是debug等级的。找到多个再次依据名称过滤一下,选定一个。
这儿能够看到,运用用户指定的调度线程池,是看容器中有没有,所以想要指定,直接将想要运用的调度线程池放入Spring容器即可。
还有一点需求解释一下,这一步并没有构建默许的线程池,构建默许线程池的流程在下一步哦。
3.1.2 Spring构建的默许调度线程池
在Spring容器中无法找到调度线程池时,Spring会创立默许的调度线程池,咱们也看一下这部分的逻辑。
protected void scheduleTasks() {
if (this.taskScheduler == null) {
// 重点:没有设置taskScheduler,默许才用单线程
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
}
// 构建默许单线程的调度线程池
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
// 构建corePoolSize为1的调度线程池
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
3.1.3 运用默许单线程的调度线程池,导致心跳丢掉的事例剖析
这儿简略介绍一下事务场景,项目有两个守时使命,一个是核算使命,守时凌晨2点履行,履行大约时刻5分钟;还有一个心跳发送的守时使命,每10s履行一下上报,服务端有检测逻辑,假如实例超过30s未上报心跳,进行实例摘除。其时的场景便是:项目没有设置调度线程池,故主动运用了Spring默许的单线程调度线程池。
这儿咱们用代码简略模仿一下:
@Component
public class ScheduledJob {
/** 核算使命,凌晨2点履行,耗时五分钟 */
@Scheduled(cron = "0 0 2 * * ?")
void calculation() throws InterruptedException {
System.out.println("使命1,"+Thread.currentThread().getName()+"开端履行:"+new Date());
Thread.sleep(5 * 60 * 1000);
System.out.println("使命1,"+Thread.currentThread().getName()+"履行完毕:"+new Date());
}
/** 心跳使命,每10s上报一次,耗时1s */
@Scheduled(cron = "*/5 * * * * ?")
void heartbeat() throws InterruptedException {
System.out.println("使命2,"+Thread.currentThread().getName()+"开端履行:"+new Date());
Thread.sleep(1000);
System.out.println("使命2,"+Thread.currentThread().getName()+"履行完毕:"+new Date());
}
}
代码如上所示,后来项目发现:每天夜里2:00-2:05期间,实例没有心跳上报了,时刻演远远超过30s,最终导致实例被摘除,从而导致其他一系列问题。后来排查下来,便是因为没有装备调度线程池,运用了Spring默许单线程调度线程池导致的。咱们剖析一下其时的场景:
- 在2:00前,只有心跳使命履行,因为履行时刻短,使命不会阻塞,每次心跳都能正常上报。
- 在2:00时左右,核算使命发动,仅有的线程资源被占用,履行需求持续五分钟,仅有的履行线程资源在2:05才干被开释。
- 2:00:10 心跳使命
应该被履行,可是因为没有能够线程,使命只能被抛弃
。直到2:05前,都是如此,导致近五分钟不能上报心跳
。 - 2:05时左右,核算使命完毕,心跳使命才有资源履行,持续上报心跳。
上述事例便是调度线程池设置的不合理,导致实例摘除的真实状况。经过这个事例,希望加深小伙伴们对设置适宜的调度线程池有多么重要的深入意识。赶紧检查一下自己项目调度线程池的设置吧,尤其是连设置都没有设置的小伙伴,更要当心了。
3.1.4 装备适宜的调度线程池
现在,咱们现已知道装备适宜的调度线程池有多么的重要。关于怎么创立,连Spring怎么查找的底裤都被咱们扒出来了,怎么装备还不是小菜一碟。为了完整性,咱们仍是给咱们展示一下吧。
@EnableScheduling
@Configuration
public class ScheduleConfig {
@Bean("threadPoolTaskScheduler")
public TaskScheduler threadPoolTaskScheduler(){
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10);
return scheduler;
}
}
是不是很简略,直接放入Spring容器中就能够了。关于你是想用@Bean
、仍是想用@Component
、或许想用@Import
亦或是自界说BeanPostProcessor
这些奇技淫巧,那就随你便了。
3.2 守时使命调度履行
现在守时使命也查找出来了,调度线程池也有了,可谓是万事俱备,只欠东风了。咱们也总算来到使命调度履行得最终一关了。关于调度履行,当然是先把之前解析封装出来ScheduledTask
使命取出来,转换翻译一下,交给JDK的调度线程池了。不过三种使命类型还不完全相同,咱们逐个来看一下。
3.2.1 cron表达式类型的使命履行
其实JDK的ScheduledThreadPoolExecutor自身是不支撑cron表达式类型的,这部分才能是Spring赋予的,当然,底层凭借的是ScheduledThreadPoolExecutor#schedule()单次使命调度,spring仅仅玩了一些小花样,从而使其具有了cron能够重复履行的才能。
这儿的详细完结是:Spring先进行cron表达式的解析,核算出下一次使命的详细履行时刻,然后交由ScheduledThreadPoolExecutor#schedule()进行下次调度。不过这仍是单次的啊,不具有重复履行的才能啊,这儿Spring的小把戏就来了,在履行时刻到,ScheduledThreadPoolExecutor履行了先前提交的使命后,会再次核算出下次使命的履行时刻,再次提交给ScheduledThreadPoolExecutor。哦,原来是经过本次履行后,会提交下次使命的办法
,使其具有了CRON重复履行的才能,不得不说,Spring很聪明。
咱们直接看一下源码:
public ScheduledTask scheduleCronTask(CronTask task) {
// 重点:2:使命调度履行阶段,将使命提交给调度线程池
if (this.taskScheduler != null) {
scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());
}
// 1:@Schedule解析机遇,taskScheduler为null,仅仅仅仅将使命包装保存起来即可
else {
addCronTask(task);
this.unresolvedTasks.put(task, scheduledTask);
}
}
// # ConcurrentTaskScheduler.java
public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
//封装ReschedulingRunnable,并调度使命
return new ReschedulingRunnable(task, trigger, this.scheduledExecutor, errorHandler).schedule();
}
咱们发现,在使命履行的时分,先将使命封装成了ReschedulingRunnable
,然后调用schedule()进行调用,貌似离中心秘密不远了,咱们持续盯梢一下。
public ScheduledFuture<?> schedule() {
synchronized (this.triggerContextMonitor) {
// 1:依据cron表达式,核算下次履行时刻
this.scheduledExecutionTime = this.trigger.nextExecutionTime(this.triggerContext);
if (this.scheduledExecutionTime == null) {
return null;
}
//2:核算下次履行还有多少时刻
long initialDelay = this.scheduledExecutionTime.getTime() - System.currentTimeMillis();
//3: 将自己作为使命提交给调度线程池履行。
this.currentFuture = this.executor.schedule(this, initialDelay, TimeUnit.MILLISECONDS);
return this;
}
}
这儿总算发现咱们想要的当地了,榜首步就解析cron表达式,核算出使命履行时刻,然后交给ScheduledThreadPoolExecutor#schedule()履行,这儿是榜首次履行使命。
关于ScheduledThreadPoolExecutor的调度原理,实质上是将守时使命,依照履行时刻,有序的保护在内部队列里,然后循环从队列获取履行时刻契合的使命,交由线程池履行。仅仅在正常线程池的使命履行的基础上,引入了时刻的概念,感兴趣的小伙伴能够自行查阅材料了解一下。
别的,关于把this
传递给schedule()
去履行,或许也有点懵,什么鬼,把自己提交给调度线程池了,这啥啊。小伙伴们冷静想一想,调度线程池实质是个线程池,依据JAVA标准,咱们向线程池提交的什么,是不是Runnable
实例,然后线程池履行的是Runnable#run()
。
那巧了,ReschedulingRunnable
就完结了Runnable
,所以,把自己提交曩昔,届时分履行的便是ReschedulingRunnable#run()
。这就需求咱们去看一下run()的详细完结,看一下完结逻辑是不是咱们之前剖析的:先反射履行@Schedule标示的守时办法
,然后再提交CRON表达式对应的下一次使命
。
public void run() {
Date actualExecutionTime = new Date();
//1: 履行咱们界说的@Schedule办法
super.run();
Date completionTime = new Date();
synchronized (this.triggerContextMonitor) {
Assert.state(this.scheduledExecutionTime != null, "No scheduled execution");
//2: 更新履行时刻信息
this.triggerContext.update(this.scheduledExecutionTime, actualExecutionTime, completionTime);
if (!obtainCurrentFuture().isCancelled()) {
//3:再次调用schedule办法,提交下一次使命
schedule();
}
}
}
果然不出所料,便是履行了咱们界说的@Schedule守时办法,然后提交一下车使命,不过还有一些其他的作业,比方记载一下履行时刻信息,目的是:方便进行下一次履行时刻的核算。
这儿就从源码的视点,带咱们窥探了Spring CRON表达式完结的奥妙,总结起来便是:仍是凭借ScheduledThreadPoolExecutor#schedule()完结的,关于其不支撑的循环履行的问题,Spring采用了履行完一次使命后,回调schedule(),核算下一次履行时刻,重新提交新的使命的办法,使其具有了循环调用的逻辑。
这儿有小伙伴关于super.run()是调用咱们界说的@Schedule守时办法有所不解,这儿之所以能够这样调用,是因为在使命解析时,现已将反射需求的method信息和目标信息封装成
ScheduledMethodRunnable
了,其对应的run()
便是反射履行该办法。在使命解析时,保存在了CronTask中。再创立ReschedulingRunnable时,又把ScheduledMethodRunnable传递了过来,最终在父类DelegatingErrorHandlingRunnable的run()调用了ScheduledMethodRunnable的run(),所以这儿的super.run(),其实便是ScheduledMethodRunnable#run(),也便是反射履行@Schedule标示的守时办法。
咱们总结一下cron使命的调度流程:
3.2.2 FixedDelay使命履行
关于FixedDelay使命的履行,则是直接凭借ScheduledThreadPoolExecutor#scheduleWithFixedDelay()
完结的,这儿咱们简略看一下:
public ScheduledTask scheduleFixedDelayTask(IntervalTask task) {
// 转换使命类型为FixedDelayTask
FixedDelayTask taskToUse = (task instanceof FixedDelayTask ? (FixedDelayTask) task :
new FixedDelayTask(task.getRunnable(), task.getInterval(), task.getInitialDelay()));
return scheduleFixedDelayTask(taskToUse);
}
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Date startTime, long delay) {
long initialDelay = startTime.getTime() - System.currentTimeMillis();
//直接运用ScheduledThreadPoolExecutor#scheduleWithFixedDelay()履行,
// 可是先构建提交的Runnable目标,构建的DelegatingErrorHandlingRunnable类型
return this.scheduledExecutor.scheduleWithFixedDelay(decorateTask(task, true), initialDelay, delay, TimeUnit.MILLISECONDS);
// ...省略非中心代码
}
这儿经过源码能够很清楚的看到,在核算了相关参数后,也是先构建了使命提交需求的Runnable目标,然后就直接交由ScheduledThreadPoolExecutor#scheduleWithFixedDelay()调度履行了。
这儿咱们就不看构建的DelegatingErrorHandlingRunnable的run()办法了,这儿也是直接反射履行@Schedule标示的守时办法
,并没有在做其他的工作。关于依据距离循环调度履行,ScheduledThreadPoolExecutor自身就支撑啊,这儿是不需求再做什么的。
3.2.3 FixedRate使命履行
关于FixedRate的履行,和FixedDelay完全相同,都是凭借ScheduledThreadPoolExecutor自身的才能完结的,这儿仅仅做转交罢了,仅仅FixedRate调用的是scheduleWithFixedRate()罢了。
别的关于FixedRate和FixedDelay,在单线程模型下,使命履行时刻过长,关于下次使命履行时刻的影响,自身也是JDK的才能和逻辑,和Spring自身无关哦。
3.3 守时使命触发履行的机遇
现在关于守时使命调度履行的细节,都现已说清楚了。还有一个问题在给小伙伴弥补下,便是这些守时使命是什么时分开端调度的。
其实这个问题不评论也完全无伤大雅,不过贰师兄之前在运用一向自研的、深度交融Spring的东西时,在碰到守时调度使命时,遇到了问题,后来排查下来发现:守时使命触发机遇,和自研东西初始化机遇重合,导致在守时使命中运用自研东西,呈现了问题。所以这儿和咱们简略介绍一下:
&emsp,首要咱们得先知道,Spring守时使命调度履行,是在接纳到ContextRefreshedEvent
工作后。而自研东西也是接纳到这个工作后做中心类创立,然后注入Spring容器中的。
因为是同一机遇触发的,先后完结状况无法保证,呈现了调度使命现已开端履行,可是中心东西类初始化还没有完结的状况,从而呈现了,在调度使命中,运用自研东西呈现空指针的状况。故这儿和咱们简略介绍一下,给咱们避个坑。
// 接纳ContextRefreshedEvent工作,调度守时使命
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (event.getApplicationContext() == this.applicationContext) {
// 查找调度线程池,提交调度使命
finishRegistration();
}
}
这儿需求留意,从代码上看,在所有bean实例化后回调阶段(也便是
afterSingletonsInstantiated()
被调用时),也有或许会触发守时调度使命的履行,因为代码中也会调用finishRegistration()
。不过机遇剖析下来,却没有在这个机遇调用,因为此刻
applicationContext
现已有值了,这就涉及到ApplicationContextAware
的回调机遇了,贰师兄在聊透Spring bean的生命周期文章有介绍,小伙伴们依据这篇文章,自行剖析一下吧,信任必定会有所收成的,这儿不再赘述了。