Springboot定时任务调度的实现原理

news2024/11/16 1:55:11

前言

        源码的世界是一片汪洋大海,springboot的源码更是如此,虽然用的时候似乎很简单,然而正是因为其内部的设计巧妙、复杂,才造就了其使用上的简单易上手。罗马不是一天建起来的,要完全理解它也并非一时的事,所以这里给大家分享一些我自己阅读源码时的一些体会,那就是不要因为一时看不懂而着急或放弃,慢慢来,一点一点来,早晚能弄明白,另外一点就是,带着问题去看,时刻要把握好自己的问题是什么,不要在源码中迷失了自己。

1. 问题

        关于Springboot调度任务的工作原理,实际就是两个问题:

        第一个问题,调度任务是如何被注册的?

        第二个问题,注册的调度任务是如何触发执行的?

2. 实现方法

        关于Springboot调度任务的具体实现方法已经在上一篇文章中详细介绍过,这里再作一下简单的梳理、归纳,主要两种方法:

        基于注解@Scheduled

        基于接口SchedulingConfigurer

        两种方法都需要使用@EnableScheduling(第一个核心关键类)来开启调度任务功能。

2.1 基于注解@Scheduled

        基于注解@Scheduled内的属性,可以分为三类调度任务:1、cron表达式;2、fixedDelay(fixedDelayString);3、fixedRate(fixedRateString)。

  1. cron表达式可以通过若干数字、空格、符号按一定的规则,组成一组字符串,定义调度任务的执行规则;
  2. fixedDelay以每次调度任务执行完成后间隔指定时间再开始下一次的调度任务,单位是毫秒;
  3. fixedRate以每次调度任务开始的时间间隔指定时间再开始下一次的调度任务,单位是毫秒;

2.2 基于接口SchedulingConfigurer

        实现org.springframework.scheduling.annotation.SchedulingConfigurer接口,并重写configureTasks()方法,在重写configureTasks()里,完成调度任务的注册;

3. 工作原理

        基于注解@Scheduled和基于接口SchedulingConfigurer接口,都需要使用@EnableScheduling来开启调度任务注册功能。进入@EnableScheduling注解内部观察一番,发现通过@Import引入了一个配置类SchedulingConfiguration.class(第二个核心关键类)

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

顺着SchedulingConfiguration.class进入其内部,又发现了一个大秘密:ScheduledAnnotationBeanPostProcessor(第三个核心关键类)

@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();
   }

}

        对Spring生命周期比较熟悉的话,一看到XxxxBeanPostProcessor,就能想到postProcessAfterInitialization()方法了。

BeanPostProcessor,spring的后置处理器,Spring重要的扩展点之一,可以在在Bean对象初始化前后回调BeanPostProcessor中定义的两个方法:

postProcessBeforeInitialization()方法会在每一个bean对象的初始化方法调用之前回调;postProcessAfterInitialization()方法会在每个bean对象的初始化方法调用之后被回调

基于注解@Scheduled与基于接口SchedulingConfigurer调度任务实现入口是一样的,其具体实现是不一样的。

3.1 基于注解@Scheduled

        还记得第一个问题是什么吗?(调度任务是如何被注册的?)基于注解@Scheduled调度任务的注册就是在中实现在org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#postProcessAfterInitialization()方法中实现的,具体的步骤是:

        在spring容器中找出被注解@Scheduled.class,@Schedules.class标记过的方法

接着遍历这些方法,而实际的调度任务注册逻辑也是从这里(processScheduled()方法)开始的

        从上面的实现方法梳理中已知,springboot的调度任务实际上可以分为三类,这里就以常用cron表达式类为例来说明其注册、执行过程。然后进入processScheduled()内,可以看到首先把@Scheduled标记的方法包装成一个Runnable任务(实现java多线程的方法之一就是实现java.lang.Runnable接口)

而在processScheduled()方法内也会根据不同类别的任务分别作处理,这里就以cron表达式类的调度任务为例看一下后续是怎么处理的。

        接着进入thsi.registrat.scheduleCronTask()方法内部(第五个核心关键类ScheduledTaskRegistrar),很多人认为下面就是触发开始执行调度任务的执行了;实际上这么认为是错的,因为这个时候Spring的容器还未启动完成,任务的调度器(this.taskScheduleer是null)还未实例化,所以这里只是完成调度任务的注册。

分析源码就是这样,得慢慢来,不要急,要牢牢把握住自己的问题,千万不要迷路了。下面开始分析第二个问题:注册的调度任务是如何触发执行的。

        任务注册上面说到了ScheduledAnnotationBeanPostProcessor#postProcessAfterInitialization()方法被触发执行,一直到执行到thsi.registrat.scheduleCronTask()只是完成了调度任务的注册,并没有开始执行。

        实际上注册完成的调度任务开始执行是在Spring容器启动完成后,会发布一个启动完成的事件(ContextRefreshedEvent),ScheduledAnnotationBeanPostProcessor实现了Spring的监听器接口(ApplicationListener),因此实际触发已注册调度任务的执行入口是在监听方法中(org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#onApplicationEvent);

在监听方法中实际又调用了finishRegistration(),那么在finishRegistration()中,分别作了哪些事呢?

        第一,调用org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#resolveSchedulerBean()查找任务调度器(TaskScheduler);

        第二,实际找到了ThreadPoolTaskScheduler作为实际的任务调度器,然后调用ScheduledTaskRegistrar#setTaskScheduler()完成任务调度器(TaskScheduler)的配置;

        第三,接着调用ScheduledTaskRegistrar#afterPropertiesSet()开始实际的任务触发执行;不同类型的调度任务是在org.springframework.scheduling.config.ScheduledTaskRegistrar#scheduleTasks()中完成判断,然后分别调用各自的方法执行的;以cron表达式类型的调度任务为例,实际上最后由ScheduledTaskRegistrar#scheduleCronTask()实际完成。

        至此,基于注解@Scheduled的调度任务实现原理基本分析完了,下面是我就调度任务的注册和执行两个时机为入口,绘制了整个过程的一个调用时序图,供大家参考学习:

3.2 基于接口SchedulingConfigurer

        基于接口SchedulingConfigurer的Springboot调度任务,与基于注解不同,其调度任务的注册、执行都是在Spring容器启动完成以后,发布ContextRefreshedEvent事件,实现了Srping事件监听器的接口(ApplicationListener)的ScheduledAnnotationBeanPostProcessor类的onApplicationEvent()被触发,然后才开始调度任务的注册和执行,下面具体分析一下:

        第一步,查找所有SchedulingConfigurer接口的实现类,然后遍历所有实现类并执行org.springframework.scheduling.annotation.SchedulingConfigurer#configureTasks,就这么朴实无华,完成了所有通过实现SchedulingConfigurer接口(第四个核心关键类)的调度任务注册;(第一个问题:调度任务是如何被注册的,到这已经有答案了)

        第二步,从org.springframework.scheduling.config.ScheduledTaskRegistrar#afterPropertiesSet()进入开始调度任务的触发执行阶段(第二个问题,注册的的调度任务是如何被执行的),afterPropertiesSet()中实际是调用了org.springframework.scheduling.config.ScheduledTaskRegistrar#scheduleTasks()方法;如果在实现SchedulingConfigurer接口,重写configureTasks(),没有显性的指定任务调度器(TaskScheduler),在scheduleTasks()里,会初始化一个默认的任务调度器,这里要注意,默认的使用的是单线程的线程池;

接下来就是根据实际注册的调度任务类型分别开始调度任务的实际执行了,在上一篇文章中,我注册的是TriggerTasks类型的任务,所以这里就会调用org.springframework.scheduling.config.ScheduledTaskRegistrar#scheduleTriggerTask()方法开始调度任务的执行。

至此,基于接口SchedulingConfigurer的Springboot调度任务的工作任务也基本分析完了,下面是整个过程的调用时序图,大家可以参考一下:

4. 核心类回顾

@EnableScheduling,开启Springboot任务调度功能的标识注解;

SchedulingConfiguration,Springboot任务调度功能的自动配置类,作用是实例化ScheduledAnnotationBeanPostProcessor;

ScheduledAnnotationBeanPostProcessor,调用任务的注册、执行的触发入口;

SchedulingConfigurer,调度任务的扩展接口,允许用户自定义调度任务的注册;

ScheduledTaskRegistrar,调度任务注册中心,调用任务的实际管理者;

5.总结

        通过分析Springboot两种调度任务的实现方法的工作原理,有什么收获呢?

        第一,默认情况下,使用单线程的线程池来执行调度任务,性能上不会太高,适用场景有限;

        第二,即便显性的任务调度器配置了拥用较多线程的线程池,与现有其他业务同处一个工程,也会挤占其他业务的服务器资源;

        所以,在实际使用过程中,应根据实际场景和资源配置进行选择。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/126166.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

webdriver的尝试:一 【webdriver自动打开浏览器与页面】

文章目录Webdriver尝试使用步骤1:安装类库2:安装驱动3:配置环境3:编写脚本4:执行脚本Webdriver 网站地址 Selenium webdriver 简单介绍:webdriver是一个api和协议。支持多种语言。主要功能,通…

大米新闻微信小程序和Springboot新闻管理系统项目源码

介绍 本项目分为大米news小程序端和springboot新闻管理系统后台项目。小程序主要用来新闻展示,后台管理系统用于提供相关新闻API。 项目源码 参考:https://www.bilibili.com/video/BV1TD4y1j7g3/?spm_id_from333.337.search-card.all.click&vd_s…

day08 常用API

1.API 1.1 API概述-帮助文档的使用 什么是API ​ API (Application Programming Interface) :应用程序编程接口 java中的API ​ 指的就是 JDK 中提供的各种功能的 Java类,这些类将底层的实现封装了起来,我们不需要关心这些类是如何实现的&a…

两个链表的第一个公共结点

今天为大家带来一道题目: 这个题目先来看看我自己写的错误版本 public class Solution {public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {ListNode tmp1pHead1;ListNode tmp2pHead2;while(pHead1!null&&pHead2!null){ListNode cur…

Axure8.0动态面板使用

Axure动态面板是最常使用的,今天我们就来详细介绍一下。 动态面板是Axure中一个非常强大的高级元件,用于实现多个状态的切换展示,可以将其看成一个容器,可以容纳多种不同状态,通过各种交互触发其状态发生变化。 通过以…

年终盘点丨2022边缘计算大事记

2022年进入尾声了,每年到了年底,边缘计算社区都会盘点过去一年边缘计算领域发生的值得您关注的事情。今年的边缘计算领域发生很多不一样的精彩:加强面向特定场景的边缘计算能力刷屏一整年,安波福43亿美元收购风河,全球…

C++图论 最短路问题总结

目录 最短路问题 图的存储 一、单源最短路 ① 朴素Dijkstra O(n^2) 练习题 代码 ② 堆优化Dijkstra O(mlogn) 练习题 代码 ③ Bellman_ford O(nm) 练习题 代码 ④ Spfa O(n) - O(nm) 练习题 ​代码 二、多源最短路 Floyd O(n^3) 练习题 代码 最短路问题 图…

C# 数据库访问方法

一 访问数据的两种基本方式 1 方式1:DataAdapter及DataSet ① 适合于“离线”处理; ② 自动建立Command对象; 方式2:DataReader ① 适合于只读数据,效率较高 它们都要使用Connection及Command 二 Connection对象…

Android解析服务器响应数据

文章目录Android解析服务器响应数据解析XML格式数据Pull解析方式SAX解析方式解析JSON数据使用JSONObject使用GSON的方式来解析JSON数据Android解析服务器响应数据 解析XML格式数据 通常情况下,每一个需要访问网络的应用程序都会有一个自己的服务器,我们可以向服务器提交自己的…

多线程——概念及线程安全

文章目录多线程概念进程vs线程多线程的优势/好处/使用场景线程的状态创建线程的方式线程的启动Thread中,start()和run()有什么区别?Thread类中的常用方法join()获取当前线程引用线程休眠线程中断线程的属性多线程效率局部变量在多线程中的使用线程安全问题1.什么情况会产生线程…

replit搭建

本文章用于快速搭建“出去”的节点,很简单 每个月只有100G流量中间可能会停止运行,需要手动进入项目开启 1、需要注册一个Replit账号 点击注册 支持Github登录,其他登录也行 2、使用这个模板项目 随便起个名字 3、运行 进行完第二步&am…

【开源项目】第三方登录框架JustAuth入门使用和源码分析

第三方登录框架JustAuth入门使用和源码分析 项目介绍 JustAuth,如你所见,它仅仅是一个第三方授权登录的工具类库,它可以让我们脱离繁琐的第三方登录 SDK,让登录变得So easy! JustAuth 集成了诸如:Github、Gitee、支付…

九、kubernetes中Namespace详解、实例

1、概述 Namespace是kubernetes系统中的一种非常重要资源,它的主要作用是用来实现多套环境的资源隔离或者多租户的资源隔离。 默认情况下,kubernetes集群中的所有的Pod都是可以相互访问的。但是在实际中,可能不想让两个Pod之间进行互相的访…

花费数小时,带你学透Java数组,这些常用方法你还记得吗?

推荐学习专栏:Java 编程进阶之路【从入门到精通】 文章目录1. 数组2. 一维数组2.1 声明2.2 初始化2.3 使用3. 二维数组3.1 声明3.2 初始化3.3 使用4. 数组在内存中的分布5. 数组常用的方法5.1 Arrays.toString方法5.2 Arrays.copyOf方法5.3 Arrays.copyOfRange方法5…

麦克斯韦(Maxwell)方程组的由来

美国著名物理学家理查德费曼(Richard Feynman)曾预言:“人类历史从长远看,好比说到一万年以后看回来,19世纪最举足轻重的毫无疑问就是麦克斯韦发现了电动力学定律。” 这个预言或许对吧。可是费曼也知道,麦…

疫情三年划上终止符,好易点却把个人健康写入了产品基因

作者 | 牧之 编辑 | 小沐 出品 | 智哪儿 zhinaer.cn随着12月26日国家卫健委发布的一纸公告,新冠肺炎正式更名为新冠感染。而从次年1月8日起,新冠将被实施「乙类乙管」。同时出入境也将采取开放性政策。这意味着,持续三年的「疫情时期」&#…

大数据技术——HBase简介

文章目录1. HBase定义2. HBase数据模型2.1 逻辑存储结构2.2 HBase 物理存储结构3. HBase基础架构1. HBase定义 HBase – Hadoop Database,是一个高可靠性、高性能、面向列、可伸缩的分布式存储系统,利用HBase技术可在廉价PC Server上搭建起大规模结构化存…

基于BP神经网络的电力负荷预测(Matlab代码实现)

💥💥💥💞💞💞欢迎来到本博客❤️❤️❤️💥💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑…

SpringBoot系列之数据库初始化-jpa配置方式

【DB系列】数据库初始化-jpa配置方式 | 一灰灰Blog 上一篇博文介绍如何使用spring.datasource来实现项目启动之后的数据库初始化,本文作为数据库初始化的第二篇,将主要介绍一下,如何使用spring.jpa的配置方式来实现相同的效果 I. 项目搭建 1…

qt windeployqt打包 带多个dll的可执行程序时 应用程序无法正常启动

前提: 我的工程中包含5个子项目,项目1生成 camer.exe 项目2生成 dll1.dll ,其中项目1 依赖后面的四个子项目。 但我在打包程序时,只运行了windeployqt F:\workspace\\bin-ne\camer.exe 将打包的程序放在纯净版本上时&#xff0…