文章目录
写在前面
首先附上xxl job项目源码地址:https://github.com/xuxueli/xxl-job
本文章比较长,主要讲解思路为:
- XXL JOB项目源码整体概括
- xxl-job-admin的源码分析
- 执行器代码分析
- XXL Job需要改进的地方
一.XXL JOB项目源码整体概括
1. 源码整体概括说明
这个项目是作为工程开发的同学们很值得学习的一个开源项目。代码整体风格比较好,模块化清晰。代码逻辑遵行Web的MVC架构,采用Spring boot + Mybatis的框架组合来组织代码。
代码总体分为三部分:
一.xxl-job-core: 这是公共服务模块,比如提供RPC远程调度,线程管理等。从业务角度去分析这个模块是没有意义的,很容易一脑雾水,因为这个模块不是独立的服务,它只是为xxl-job-admin和xxl-job-executors-sample提供了功能模块。
二. xxl-job-admin: web交互的后台引擎,这里称为调度中心。主要负责下面几件事情:
- 负责web端交互:作为Web后台引擎,提供了登录权限管理,任务增删改查操作,执行器组管理,GLUE任务在线编辑,日志管理等
- 与MySQL数据库交互,把数据持久化。
- 提供RPC接口,供执行器注册,维持和执行器的心跳。
- 与quartz交互,把任务调度的事情交给quartz去做。
三. xxl-job-executors-sample。主要做以下两件事情:
- 执行器初始化,并且主动注册到调度中心那里去。
- bean的方式注入我们线下编辑好的任务。
整体架构图如下。后续章节会对细节进行展开阐述。
图1. 代码整体逻辑架构图
图2. xxl job的Web UI真实界面
2.分析该项目源码时一些必须的知识
磨刀不误砍材工,在正式深入分析这个项目之前,有些知识有必要预知下:
1.quartz的用法。
2.freemarker渲染前端界面的原理和用法。
3.java基本功,以及spring boot和mybatis相关框架知识。
2.1 quartz简单介绍
xxl job的任务调度是依赖于quartz的。 quartz可用于创建执行数十,数百甚至数十万个作业的简单或复杂的计划; 任务定义为标准Java组件的任务,可以执行任何可以对其进行编程的任何内容。我们先从quartz官网的一个例子说起:
// 第一步,定义任务类。这个class必须要实现Job接口的execute方法。
public class HelloJob implements Job {
private static Logger _log = LoggerFactory.getLogger(HelloJob.class);
public HelloJob() {
}
public void execute(JobExecutionContext context)
throws JobExecutionException {
_log.info("Hello World! - " + new Date());
}
}
//2. 定义任务的执行逻辑,将任务和触发器绑定起来。
public class SimpleExample {
public void run() throws Exception {
log.info("------- 初始化----------------------");
// 首先,我们得到一个scheduler实例
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler sched = sf.getScheduler();
log.info("------- 初始化完成 -----------");
// computer a time that is on the next round minute
Date runTime = evenMinuteDate(new Date());
log.info("------- 调度任务 -------------------");
// define the job and tie it to our HelloJob class
JobDetail job = JobBuilder.newJob(HelloJob.class).withIdentity("job1", "group1").build();
// Trigger the job to run on the next round minute
Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "group1").startAt(runTime).build();
// 告诉quartz利用trigger触发器来调度job
sched.scheduleJob(job, trigger);
log.info(job.getKey() + " will run at: " + runTime);
// Start up the scheduler (nothing can actually run until the
// scheduler has been started)
sched.start();
log.info("------- 任务已经已经启动了 -----------------");
// wait long enough so that the scheduler as an opportunity to
// run the job!
log.info("------- Waiting 65 seconds... -------------");
try {
// wait 65 seconds to show job
Thread.sleep(65L * 1000L);
// executing...
} catch (Exception e) {
//
}
// shut down the scheduler
log.info("------- 调度关闭 ---------------------");
sched.shutdown(true);
log.info("------- 关闭完成 -----------------");
}
public static void main(String[] args) throws Exception {
SimpleExample example = new SimpleExample();
example.run();
}
}
从上面的demo可以看出quartz的关键API:
- Scheduler - 进行作业调度的主要接口.
- Job - 作业接口,编写自己的作业需要实现,如例子中的HelloJob
- JobDetail - 作业的详细信息,除了包含作业本身,还包含一些额外的数据。
- Trigger - 作业计划的组件-作业何时执行,执行次数,频率等。
- JobBuilder - 建造者模式创建 JobDetail实例.
- TriggerBuilder - 建造者模式创建 Trigger 实例.
- QuartzSchedulerThread 继承Thread 主要的执行任务线程
从上面的几个接口,可以看到quartz设计非常精妙,将作业和触发器分开设计,同时调度器完成对作业的调度。
整个执行过程可以概括如下:
- 从StdSchedulerFactory获取scheduler
- 创建JobDetail
- 创建Trigger
- scheduler.scheduleJob()将任务和触发器绑定起来
所以quartz的核心元素可以表示为如下图:
图3. quartz内部核心模块关系图
quartz不是以定时器的方式去执行任务的,而是通过线程池去完成。配置文件quartz.properties配置了线程池相关的参数。在quartz中,有两类线程,Scheduler调度线程和任务执行线程,其中任务执行线程通常使用一个线程池维护一组线程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bT3CYQD3-1571059534531)(https://cdn.nlark.com/yuque/0/2018/png/171710/1540885992917-894fa2a9-f022-4a35-80fb-18a84507f89c.png “”)]
图4. quartz的线程视图
调度线程主要有两个:执行常规调度的线程,和执行misfiredtrigger的线程。常规调度线程轮询存储的所有trigger,如果有需要触发的trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该trigger关联的任务。Misfire线程是扫描所有的trigger,查看是否有misfiredtrigger,如果有的话根据misfire的策略分别处理(fire now 或者 wait for the next fire)。
quartz内部的数据是存入数据库的,总共有12张表。Quartz集群中,独立的Quartz节点并不与另一其的节点或是管理节点通信,而是通过相同的数据库表来感知到另一Quartz应用的。 到此,我认为quartz的核心要点应该介绍完了。
2.2 freemarker前端渲染模板简介
freemarker是一个java模板引擎。是一种基于模板和要改变的数据,并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。类似于JSP,volecity。这里不细说,有这个概念就好了。
2.3 java基本功修炼
xxl job的源码阅读,需要一定的java工程功底。特别要熟悉下spring boot, mybatis框架。
二. xxl-job-admin的源码分析
xxl-job-admin是项目的核心,称为调度中心,也是一个典型的web项目架构。通常对于一个web程序来说,我们分析时,主要是关注两件事情:第一,这个程序在初始化(也就是程序启动的时候)干了哪些事情;第二,程序的Restful接口分析,这个是Web项目最大的主线。下面的分析我们也主要是从这两点分别展开。
1. 调度中心初始化
JVM执行一个java程序时,会经历编译,加载,分配内存和执行等过程。spring boot采用了的bean方式初始化了一些对象,这些对象包括了数据库连接池,前端界面渲染的引擎,配置文件读取,quartz调度引擎,拦截器等等,这些对象一旦初始化,就会从JVM的方法区里实例化到堆内存里面去,可以供进程后续的调用。这里有个和我们业务直接相关的bean初始化,代码如下:
<!--classpath:applicationcontext-xxl-job-admin-xml-->
<bean id="xxlJobDynamicScheduler" class="com.xxl.job.admin.core.schedule.XxlJobDynamicScheduler" init-method="init" destory-method="destory">
<property name="scheduler" ref="quartzScheduler" />
<property name="accessToken" value="${xxl.job.accessToken" />
</bean>
这个XxlJobDynamicScheduler类在初始化化时,执行了init方法。我们来重点分析下这个init方法干了哪些事情。
public void init() throws Exception {
// 1. 调度中心注册守护线程,就是一直守护着执行器的注册,维持着和执行器之间的心跳
JobRegistryMonitor.getInstance.start();
// 2. 任务失败处理的守护线程
JobFailMonitorHelper.getInstance().start();
// 3. 初始化本地调度中心服务
NetComServerFactory.putService(AdminBiz.class, XxlJobDynamicScheduler.adminBiz);
NetComServerFactory.setAcessToken(accessToken);
// 4.国际化
initI18n();
Assert.notNull(scheduler, "quartz scheduler is null");
logger.info(">>>>>>> init xxl-job admin success");
}
- JobRegistryMonitor.getInstance.start()是开启了一个单独的线程,这个线程每30s去轮训一下数据库。如果某个执行器的注册信号(也叫作心跳)在近90s内没有写入数据库表XXL_JOB_QRTZ_TRIGGER_REGISTRY,那么调度中心就认为这个执行器已经死掉。然后会更新数据库表XXL_JOB_QRTZ_TRIGGER_GROUP表,使每个执行器组,只保留活着的执行器。这里的执行器组是根据调度中心来区分的。每个执行器组(这个有可能是一台,也有可能是一个集群)都有一个唯一的appName,执行器向调度中心注册时就是通过这个appName标志来区分是属于哪个执行器组的。
- JobFailMonitorHelper.getInstance().start()是一个失败任务处理的守护线程。这个线程是每隔10秒执行一下逻辑。数据库表XXL_JOB_QRTZ_TRIGGER_LOG里存着每个任务每次的执行记录,这里面记录着任务的执行状态。如果某条日志记录的处理状态码为500,那么这条执行记录是以失败告终的。那么失败守护线程就会根据这个任务的executorFailRetryCount(失败重试次数)是否大于零(这个参数是前端新增任务时配置的),如果大于零,会去尝试再执行下这个任务。并且相应地在数据库里把该条执行日志里的executorFailRetryCount值减1。最后发出失败告警。
- 初始化本地的调度中心的服务Map,以及accessToken值。调度中心实例用HaspMap对象存了起来。
- 国际化。支持中文和英文展示。
所以总的来说,这里主要是初始化了两个守护线程。一个是维持和执行器之间心跳的线程,一个是任务执行失败重试的线程。
2. Web MVC逻辑分析
Controller层是我们理解后台逻辑的入口,com.xxl.job.admin.controller包里面中共包含了六大模块:权限登录模块,调度中心和执行器通信的RPC模块,GLUE任务编辑模块,执行器管理模块,任务操作模块和任务日志管理模块。从用户正常的交互角度分析,这些模块是有先后顺序的。用户首先是通过账户密码登录系统,然后查看调度中心里有没有已经自动注册上的执行器,如果没有,那么需要手动添加执行器。后续就可以创建任务了。任务创建时,GLUE类型的任务可以在线编辑任务逻辑代码的。任务确认创建好了之后,可以手动即席执行,还可以配置cron表达式进行周期调度执行。最后通过日志界面,查看每个任务的执行逻辑。所以本节也会根据这个先后顺序来介绍每个模块的具体逻辑。
2.1. 权限登录模块
程序的配置文件里配置了初始化的username为admin,password为123456。spring boot的xml配置文件里配置了两个拦截器。具体如下:
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**" />
<bean class="com.xxl.job.admin.co