通过源码分析Java开源任务调度框架Quartz的主要流程

从使用效果、调用链路跟踪、E-R图、循环调度逻辑几个方面分析Quartz。

github项目地址: https://github.com/tanliwei/spring-quartz-cluster-sample , 补充了SQL输出

系统说明:

IDE: IntelliJ

JDK:1.8

Quartz:2.2.1

使用效果

1.从github项目https://github.com/tanliwei/spring-quartz-cluster-sample中,拉取项目到本地,导入IDEA。

相信读者都有一定工作经验,这些细节不赘述。

2.本文采用Mysql数据库。

请执行 resources/scripts/tables_mysql_innodb.sql

3.修改jdbc.properties中数据库配置

4.通过IDEA, Edit Configurations -> Add Tomcat Server, 部署到Tomcat

暴露的Restful 接口 /say-hello.do 以及添加好任务后的调用效果:

添加任务

在tomcat启动成功后,在首页点击“添加任务”,添加如下任务:

代码执行逻辑在SyncJobFactory类中,从Output中可以看到执行的输出信息,

调用链跟踪的最后会回到这个类来。

现在开始跟踪调用链路。

IDEA 快捷键:
进入方法:  Ctrl + 鼠标左键
光标前进/后退: Ctrl + Shirt + 右方向键/左方向键
 
 
一、 调用链路跟踪

从配置文件applicationContext.xml配置中找到任务调度核心类SchedulerFactoryBean

resources/applicationContext.xml

<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
...
</bean>

使用IDEA快捷键,点击进入SchedulerFactoryBean类,它实现了InitializingBean接口,

在Spring中凡是实现了InitializingBean接口的Bean,都会在Bean属性都设置完成后调用afterPropertiesSet()方法.

SchedulerFactoryBean.java

//---------------------------------------------------------------------
// Implementation of InitializingBean interface
// 实现 InitializingBean 接口
//---------------------------------------------------------------------
public void afterPropertiesSet() throws Exception {
//...
// Create SchedulerFactory instance.
// 创建 SchedulerFactory 调度器工厂实例
SchedulerFactory schedulerFactory = (SchedulerFactory)
BeanUtils.instantiateClass(this.schedulerFactoryClass);
initSchedulerFactory(schedulerFactory);
//...
// Get Scheduler instance from SchedulerFactory.
// 通过调度器工厂 获取 调度器实例
try {
this.scheduler = createScheduler(schedulerFactory, this.schedulerName);
//...
}

SchedulerFactoryBean.java

/**
* Create the Scheduler instance for the given factory and scheduler name.
* 通过制定工厂和调度器名称创建调度器实例
* Called by {@link #afterPropertiesSet}.
* <p>The default implementation invokes SchedulerFactory's <code>getScheduler</code>
* method. Can be overridden for custom Scheduler creation.
*/
protected Scheduler createScheduler(SchedulerFactory schedulerFactory, String schedulerName)
throws SchedulerException {
//...
try {
SchedulerRepository repository = SchedulerRepository.getInstance();
synchronized (repository) {
Scheduler existingScheduler = (schedulerName != null ? repository.lookup(schedulerName) : null);
Scheduler newScheduler = schedulerFactory.getScheduler();
if (newScheduler == existingScheduler) {
throw new IllegalStateException("Active Scheduler of name '" + schedulerName + "' already registered " +
"in Quartz SchedulerRepository. Cannot create a new Spring-managed Scheduler of the same name!");
}
//...
}

这个项目走的逻辑是 StdSchedulerFactory.getScheduler()方法,可自行debug。

StdSchedulerFactory.java

/**
* Returns a handle to the Scheduler produced by this factory.
* 返回该工厂创造的调度器的句柄
*/
public Scheduler getScheduler() throws SchedulerException {
if (cfg == null) {
initialize();
} SchedulerRepository schedRep = SchedulerRepository.getInstance(); Scheduler sched = schedRep.lookup(getSchedulerName());
//...
sched = instantiate();
return sched;
}

StdSchedulerFactory.java

private Scheduler instantiate() throws SchedulerException {
//...
//大量的配置初始化、实例化代码
//...
//第1298行代码
qs = new QuartzScheduler(rsrcs, idleWaitTime, dbFailureRetry);
//...
}

QuartzScheduler.java

/**
* Create a <code>QuartzScheduler</code> with the given configuration
* 根据给定的配置 创建Quartz调度器
*/
public QuartzScheduler(QuartzSchedulerResources resources, long idleWaitTime, @Deprecated long dbRetryInterval)
throws SchedulerException {
this.resources = resources;
if (resources.getJobStore() instanceof JobListener) {
addInternalJobListener((JobListener)resources.getJobStore());
}
//private QuartzSchedulerThread schedThread;
this.schedThread = new QuartzSchedulerThread(this, resources);
ThreadExecutor schedThreadExecutor = resources.getThreadExecutor();
//通过线程池执行 Quartz调度器线程
schedThreadExecutor.execute(this.schedThread);
//...
}

QuartzSchedulerThread.java

/**
* <p>
* The main processing loop of the <code>QuartzSchedulerThread</code>.
* Quartz调度器线程的主循环逻辑
* </p>
*/
@Override
public void run() {
//while循环执行,只要调度器为被暂停
while(!halted.get()){
JobRunShell shell = null;
try {
shell = qsRsrcs.getJobRunShellFactory().createJobRunShell(bndle);
shell.initialize(qs);
}
if (qsRsrcs.getThreadPool().runInThread(shell) == false){} }
}

JobRunShell.java

public void run() {
//...
Job job = jec.getJobInstance();
//...
try {
log.debug("Calling execute on job " + jobDetail.getKey());
//执行
job.execute(jec);
endTime = System.currentTimeMillis();
}
//...
//更新Trigger触发器状态,删除FIRED_TRIGGERS触发记录
instCode = trigger.executionComplete(jec, jobExEx);
//...
}

QuartzJobBean.java

/**
* This implementation applies the passed-in job data map as bean property
* values, and delegates to <code>executeInternal</code> afterwards.
* 这个实现 把传入的map数据作为bean属性值,然后委托给 executeInternal 方法
*/
public final void execute(JobExecutionContext context) throws JobExecutionException {
try {
//执行
executeInternal(context);
}

SyncJobFactory.java

//回到了我们的业务类SyncJobFactory的executeInternal方法,
//里面执行我们的业务代码
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
try {
LOG.info("SyncJobFactory execute" + IPAddressKowalski.getIpAddressAndPort() + " port:"+IPAddressKowalski.getTomcatPort());
}
//...
System.out.println("jobName:" + scheduleJob.getJobName() + " " + scheduleJob);
//...
}

二、E-R图

梳理6张主要的Quartz表:


 
QRTZ_TRIGGERS 触发器表

SCHED_NAME,调度器名称,集群时为常量值:“ClusterScheduler”。 联合主键,QRTZ_JOB_DETAILS表SCHED_NAME外键

JOB_NAME,任务名。自定义值。 联合主键,QRTZ_JOB_DETAILS表JOB_NAME外键

JOB_GROUP,任务组。 自定义值。联合主键,QRTZ_JOB_DETAILS表JOB_GROUP外键

TRIGGER_STATE,触发器状态: WAITING , ACQUIRED, BLOCKING

NEXT_FIRE_TIME, 下次触发时间:

MISFIRE_INSTR,执行失败后的指令,

非失败策略 MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY = -1;

失败策略 MISFIRE_INSTRUCTION_SMART_POLICY = 0;

TRIGGER_TYPE, 触发器类型,例如CRON,cron表达式类型的触发器

PRIORITY,优先级

QRTZ_CRON_TRIGGERS cron类型触发器表

SCHED_NAME,调度器名称,集群时为常量值:“ClusterScheduler”。 联合主键,QRTZ_TRIGGERS表SCHED_NAME外键

JOB_NAME,任务名。自定义值。 联合主键,QRTZ_TRIGGERS表JOB_NAME外键

JOB_GROUP,任务组。 自定义值。联合主键,QRTZ_TRIGGERS表JOB_GROUP外键

CRON_EXPRESSION, cron表达式, 例如每30秒执行一次, 0/30 * * * * ?

QRTZ_JOB_DETAILS 任务详细表

SCHED_NAME,调度器名称,集群时为常量值:“ClusterScheduler”。联合主键

JOB_NAME,任务名。自定义值。 联合主键

JOB_GROUP,任务组。 自定义值。联合主键

JOB_DATA,blob类型,任务参数

QRTZ_FIRED_TRIGGERS 任务触发表

SCHED_NAME,调度器名称,集群时为常量值:“ClusterScheduler”。联合主键

ENTRY_ID,entry id,联合主键

JOB_NAME,任务名。自定义值。

JOB_GROUP,任务组。 自定义值。

FIRED_TIME, 任务触发时间

STATE,状态

INSTANCE_NAME, 服务器实例名

PRIORITY,优先级

QRTZ_SCHEDULER_STATE

SCHED_NAME,调度器名称,集群时为常量值:“ClusterScheduler”。联合主键

INSTANCE_NAME,服务器实例名。联合主键

LAST_CHECKIN_TIME,上次检查时间

CHECKIN_INTERVAL,检查间隔

QRTZ_LOCKS 全局锁

SCHED_NAME,调度器名称,集群时为常量值:“ClusterScheduler”。联合主键

LOCK_NAME,锁名称,例如,TRIGGER_ACCESS。联合主键

三、循环调度逻辑

主要流程如下:

源码如下:

QuartzSchedulerThread.java

 public void run() {
//...
while (!halted.get()) {
try {
//合理休眠
//...
//获取接下来的触发器
//1.状态为WAITING
//2.触发时间在30秒内
//3.不是错过执行的或者错过了但是时间不超过两分钟
triggers = qsRsrcs.getJobStore().acquireNextTriggers(
now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow()); //...
//触发任务
List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
//...
JobRunShell shell = null;
//...
//执行代码
if (qsRsrcs.getThreadPool().runInThread(shell) == false) {
//...
} // while (!halted)
//..
}

JobRunShell.java

    protected QuartzScheduler qs = null;

    public void run() {
qs.addInternalSchedulerListener(this);
try {
//...
do {
Job job = jec.getJobInstance();
// execute the job
try {
//执行任务代码
job.execute(jec);
//更新触发器,删除触发记录
qs.notifyJobStoreJobComplete(trigger, jobDetail, instCode);
break;
} while (true);
}
//...
}

四、扩展

除了对主线程 QuartzSchedulerThread 的分析

继续分析JobStoreSupport类的两个线程 ClusterManager 和 MisfireHandler 的分析, 它们维护触发器的MISFIRE_INSTR状态,和调度器状态QRTZ_SCHEDULER_STATE。

通过源码分析Java开源任务调度框架Quartz的主要流程的更多相关文章

  1. 通过源码分析MyBatis的缓存

    前方高能! 本文内容有点多,通过实际测试例子+源码分析的方式解剖MyBatis缓存的概念,对这方面有兴趣的小伙伴请继续看下去~ MyBatis缓存介绍 首先看一段wiki上关于MyBatis缓存的介绍 ...

  2. 通过源码浅析Java中的资源加载

    前提 最近在做一个基础组件项目刚好需要用到JDK中的资源加载,这里说到的资源包括类文件和其他静态资源,刚好需要重新补充一下类加载器和资源加载的相关知识,整理成一篇文章. 理解类的工作原理 这一节主要分 ...

  3. 通过源码了解Java的自动装箱拆箱

    什么叫装箱 & 拆箱? 将int基本类型转换为Integer包装类型的过程叫做装箱,反之叫拆箱. 首先看一段代码 public static void main(String[] args) ...

  4. 通过源码学Java基础:InputStream、OutputStream、FileInputStream和FileOutputStream

    1. InputStream 1.1 说明 InputStream是一个抽象类,具体来讲: This abstract class is the superclass of all classes r ...

  5. 通过源码学Java基础:BufferedReader和BufferedWriter

    准备写一系列Java基础文章,先拿Java.io下手,今天聊一聊BufferedReader和BufferedWriter BufferedReader BufferedReader继承Writer, ...

  6. 通过源码分析View的测量

    要理解View的测量,首先要了解MeasureSpec,系统在测量view的宽高时,要先确定MeasureSpec. MeasureSpec(32为int值)由两部分组成: SpecMode(高2位) ...

  7. [源码分析] 定时任务调度框架 Quartz 之 故障切换

    [源码分析] 定时任务调度框架 Quartz 之 故障切换 目录 [源码分析] 定时任务调度框架 Quartz 之 故障切换 0x00 摘要 0x01 基础概念 1.1 分布式 1.1.1 功能方面 ...

  8. 追源索骥:透过源码看懂Flink核心框架的执行流程

    li,ol.inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-bottom:20px}dt, ...

  9. 通过源码了解ASP.NET MVC 几种Filter的执行过程

    一.前言 之前也阅读过MVC的源码,并了解过各个模块的运行原理和执行过程,但都没有形成文章(所以也忘得特别快),总感觉分析源码是大神的工作,而且很多人觉得平时根本不需要知道这些,会用就行了.其实阅读源 ...

随机推荐

  1. CSS(二)- 选择器 - 一定要搞懂的选择器优先级和权重问题

    css的优先级之前一直没怎么注意没当回事,总以为对同一元素靠后的渲染会覆盖前面的渲染,要是覆盖不了那就来个!important嘛.直到我那在学前端基础的后端伙伴拿一个问题问住了我,我才意识到这是重点中 ...

  2. java 面向对象(四十三):反射(七)反射应用四:动态代理

    1.代理模式的原理:使用一个代理将对象包装起来, 然后用该代理对象取代原始对象.任何对原始对象的调用都要通过代理.代理对象决定是否以及何时将方法调用转到原始对象上. 2.静态代理2.1 举例:实现Ru ...

  3. web 部署专题(一):Gunicorn运行与配置方法

    Gunicorn“绿色独角兽”是一个被广泛使用的高性能的Python WSGI UNIX HTTP服务器,移植自Ruby的独角兽(Unicorn )项目,使用pre-fork worker模式,具有使 ...

  4. Unity-内存

    editor 和runtime的内存管理分开的 unity检测不到native内存容量 如c++,lua 一个asset一个ab的问题在于 每个asset都有对应的文件头,并不划算 IL2CPP抛弃了 ...

  5. 从JIT到类加载再到实现原理解式Lambda编译慢的问题

    问题回顾 描述的话不多说,直接上图: 看到输出结果了吗?为什么第一次和第二次的时间相差如此之多?咱们一起琢磨琢磨,也可以先去看看结论再回过头看分析 注:并非仅第二次快,而是除了第一次,之后的每一次都很 ...

  6. 题解 洛谷 P4189 【[CTSC2010]星际旅行】

    一个比较直接的想法就是对每个点进行拆点,拆成入点和出点,限制放在入点和出点相连的边上,然后跑最大费用最大流即可. 但是这样复杂度无法接受,所以考虑模拟费用流来解决本题. 发现 \(H\) 都大于等于该 ...

  7. three.js 数学方法之Matrix3

    今天郭先生来说一说three.js的三维矩阵,这块知识需要结合线性代数的一些知识,毕业时间有点长,线性代数的知识大部分都还给了老师.于是一起简单的复习了一下.所有的计算都是使用列优先顺序进行的.然而, ...

  8. Presto 函数开发

    0. 写在前面 Presto Functions 并不能像 Hive UDF 一样动态加载,需要根据 Function 的类型,实现 Presto 内部定义的不同接口,在 Presto 服务启动时进行 ...

  9. Flutter学习笔记(41)--自定义Dialog实现版本更新弹窗

    如需转载,请注明出处:Flutter学习笔记(41)--自定义Dialog实现版本更新弹窗 功能点: 1.更新弹窗UI 2.强更与非强更且别控制 3.屏蔽物理返回键(因为强更的时候点击返回键,弹窗会消 ...

  10. 华东师范大学p163页,用闭区间套定理证明数列的可惜收敛准则,被网友解决了。