前言
日常并不是需要所有的服务是都是立即需要返回给客户端的响应的,有一些任务是需要定时定点的去执行,这个时候我们想到的就是定时器的用处了,定时器的编写是很简单的。但是有的时候不了解其中的原理是很容易发生一些小问题(例如他们的线程池共用)。
解析过程
本地准备了一个小demo
。里面有个简单的定时器,要求是每一分钟执行一次。然后启动类上需要加上@EnableScheduling
注解信息。
-
代码部分
@Component public class TimeScheduled { @Scheduled(cron = "* 0/1 * * * ?") public void test(){ System.out.println("yyy"); } }
-
主要注意的是
@EnableScheduling
注解,里面引入了一个后处理器ScheduledAnnotationBeanPostProcessor
。这里就是解析Bean
将其任务封装。
主要方法:public Object postProcessAfterInitialization(Object bean, String beanName) { if (!(bean instanceof AopInfrastructureBean) && !(bean instanceof TaskScheduler) && !(bean instanceof ScheduledExecutorService)) { // 不属于组件bean方可进入 Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean); // 获取最终的bean(可能在此之前被代理了) if (!this.nonAnnotatedClasses.contains(targetClass)) { // 获取Scheduled方法集合 Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass, (method) -> { Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(method, Scheduled.class, Schedules.class); return !scheduledMethods.isEmpty() ? scheduledMethods : null; }); if (annotatedMethods.isEmpty()) { this.nonAnnotatedClasses.add(targetClass); if (this.logger.isTraceEnabled()) { this.logger.trace("No @Scheduled annotations found on bean class: " + targetClass); } } else { // 循环遍历解析表达式 并将定时任务注册到ScheduledTaskRegistrar中 annotatedMethods.forEach((method, scheduledMethods) -> { scheduledMethods.forEach((scheduled) -> { this.processScheduled(scheduled, method, bean); }); }); if (this.logger.isTraceEnabled()) { this.logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName + "': " + annotatedMethods); } } } return bean; } else { return bean; }
-
解析之后会将对应的定时方法根据类型封装到
org.springframework.scheduling.config.ScheduledTaskRegistrar
中,里面有以下几类任务集合。private List<TriggerTask> triggerTasks; // 触发器任务 本次未使用 private List<CronTask> cronTasks; // 含有cron表达式的任务 private List<IntervalTask> fixedRateTasks; // 含有fixedRate的任务 private List<IntervalTask> fixedDelayTasks; // 含有fixedDelay的任务 private final Map<Task, ScheduledTask> unresolvedTasks = new HashMap(16); // 作为全集任务集合,添加完上面的任务之后会往这个集合中丢入 private final Set<ScheduledTask> scheduledTasks = new LinkedHashSet(16); // 执行之后的任务集合
- 将任务添加到集合中,那么什么时候会进行任务的执行呢?我们可以看到这个
bean
后处理器还实现了ApplicationListener
接口。然后深入之后发现他调用了ScheduledTaskRegistrar
的afterPropertiesSet
方法,该方法里面调用了对应的执行过程。 - 之后
ScheduledExecutorService
再根据org.springframework.scheduling.support.CronTrigger#nextExecutionTime
计算出来的下次时间进行任务执行。
常见问题
-
多个任务定时器有交叉时间,却发现有些任务排队到时间未执行(采用默认线程池配置)
这块问题跟着源码进去就能发现,在
refresh
阶段创建taskScheduler
这个Bean
的时候,org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
里面默认的线程数大小为1,一旦任务执行开始的时候,按照线程池执行过程,剩下的任务就会丢到队列中。
补充疑惑
看了看定时器里面的代码,可能认知不够深,对此还是有些疑问的。
-
这里不知道小伙伴有没有疑问(我是有点疑问的)?因为
ScheduledTaskRegistrar
类实现了InitializingBean
接口,那为何不在容器执行该步骤的时候自动调用,还为何还利用上下文事件监听然后再进行方法调用?看到框架中该
ScheduledTaskRegistrar
类不是容器Bean
,虽然实现了对应的接口,但是不在Spring
启动解析中调用。如果你单独的将该类进行Bean
化,那么此时的Bean
是容器内部的,而不是与定时相关联的,那么执行的时候因为没有元素而不会进行执行。同时再解析InitializingBean
以及执行里面的方法的时候,对应的Bean
还未完成初始化操作(一个是finishBeanFactoryInitialization
步骤执行,一个是在finishRefresh
执行)。(个人观点其实可以直接去掉该InitializingBean
实现,毕竟用不到,让该类看起来更专一点)
总结
这段时间看了许多源码,感慨下有些东西还是需要你下去看看的,下面的世界更精彩,或许你都能回答出面试官等等的问题,但是一旦追问,你是否知道底下怎么实现?为何这么设计? 让你设计是如何做? (条条大路通罗马,这是我目前的路~)