从之前的 Xxl Job Helloworld 中学会了简单的使用 Xxl-Job
进行分步式任务调度。并且可以知道当我使用 Xxl-Job
时,我们核心基本需要以下三个步骤:
经过以上的三个步骤,然后添加的任务就可以执行了。下面我们就来从源码的角度分析一下:上面 3 个步骤都做了什么事,使得任务可以定时调度的。所以我决定分为三篇 blog 来分析 xxl-job 的源码实现。使用的 xxl-job 是最新版本 2.2.0-SNAPSHOT
。
下面我们就来看一下在 xxl-job
启动器(业务中的任务)启动会做哪些事情。当我们需要使用 xxl-job 的时候需要引用 xxl-job-core 并且会配置执行器组件(XxlJobSpringExecutor),其实大家也猜到核心就是这个类。下在我们来分析一下这个类主要做了哪些事。因为这个对象我们配置成 spring bean,并且它实现了 InitializingBean
,所以在这个对象初始化的时候会调用 XxlJobSpringExecutor#afterPropertiesSet
。
XxlJobSpringExecutor#afterPropertiesSet
public void afterPropertiesSet() throws Exception {
// init JobHandler Repository
initJobHandlerRepository(applicationContext);
// init JobHandler Repository (for method)
initJobHandlerMethodRepository(applicationContext);
// refresh GlueFactory
GlueFactory.refreshInstance(1);
// super start
super.start();
}
前两步分别把本地需要执行的任务添加到当前项目的内存中,使用 ConcurrentHashMap 保存。使用 spring bean 的 id 为 key,具体的任务实例对象为 value。第一步是注册是类上标注 @JobHandler 注解的 bean,@JobHandler 注解标注为 @Deprecated 后续会不支持该种方式;第二步是注册 spring bean 方法上标注了 @XxlJob 的方法。然后是刷新 GlueFactory(glue执行工厂),把它刷新为 SpringGlueFactory,在执行 glue 模式的任务时使用 spring 来加载相应实例。最后它会调用执行器的核心 com.xxl.job.core.executor.XxlJobExecutor#start
下面我们来看一下这个方法。
public void start() throws Exception {
// init logpath
XxlJobFileAppender.initLogPath(logPath);
// init invoker, admin-client
initAdminBizList(adminAddresses, accessToken);
// init JobLogFileCleanThread
JobLogFileCleanThread.getInstance().start(logRetentionDays);
// init TriggerCallbackThread
TriggerCallbackThread.getInstance().start();
// init executor-server
port = port>0?port: NetUtil.findAvailablePort(9999);
ip = (ip!=null&&ip.trim().length()>0)?ip: IpUtil.getIp();
initRpcProvider(ip, port, appName, accessToken);
}
public static void initLogPath(String logPath){
// init
if (logPath!=null && logPath.trim().length()>0) {
logBasePath = logPath;
}
// mk base dir
File logPathDir = new File(logBasePath);
if (!logPathDir.exists()) {
logPathDir.mkdirs();
}
logBasePath = logPathDir.getPath();
// mk glue dir
File glueBaseDir = new File(logPathDir, "gluesource");
if (!glueBaseDir.exists()) {
glueBaseDir.mkdirs();
}
glueSrcPath = glueBaseDir.getPath();
}
它会根据在执行器配置中定义的日志保存路径,看这个日志路径是否存在。如果不存在则会创建任务执行的日志目录。
private void initAdminBizList(String adminAddresses, String accessToken) throws Exception {
if (adminAddresses!=null && adminAddresses.trim().length()>0) {
for (String address: adminAddresses.trim().split(",")) {
if (address!=null && address.trim().length()>0) {
AdminBiz adminBiz = new AdminBizClient(address.trim(), accessToken);
if (adminBizList == null) {
adminBizList = new ArrayList();
}
adminBizList.add(adminBiz);
}
}
}
}
初始化 AdminBizClient 这个类包含callback(回调)、registry(注册)以及registryRemove(注册移除)这三个方法。当执行器启动的时候会把执行器执行地址注册到调度中心里面。
这个线程的作用是清除掉传入日期之前的日志。比如今天是 15 号,传入日志清除天数是 7。那么它就会清除掉 8 号之前的日志文件。
JobLogFileCleanThread#start
public void start(final long logRetentionDays){
// limit min value
if (logRetentionDays < 3 ) {
return;
}
localThread = new Thread(new Runnable() {
@Override
public void run() {
while (!toStop) {
try {
// clean log dir, over logRetentionDays
File[] childDirs = new File(XxlJobFileAppender.getLogPath()).listFiles();
if (childDirs!=null && childDirs.length>0) {
// today
Calendar todayCal = Calendar.getInstance();
todayCal.set(Calendar.HOUR_OF_DAY,0);
todayCal.set(Calendar.MINUTE,0);
todayCal.set(Calendar.SECOND,0);
todayCal.set(Calendar.MILLISECOND,0);
Date todayDate = todayCal.getTime();
for (File childFile: childDirs) {
// valid
if (!childFile.isDirectory()) {
continue;
}
if (childFile.getName().indexOf("-") == -1) {
continue;
}
// file create date
Date logFileCreateDate = null;
try {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
logFileCreateDate = simpleDateFormat.parse(childFile.getName());
} catch (ParseException e) {
logger.error(e.getMessage(), e);
}
if (logFileCreateDate == null) {
continue;
}
if ((todayDate.getTime()-logFileCreateDate.getTime()) >= logRetentionDays * (24 * 60 * 60 * 1000) ) {
FileUtil.deleteRecursively(childFile);
}
}
}
} catch (Exception e) {
if (!toStop) {
logger.error(e.getMessage(), e);
}
}
try {
TimeUnit.DAYS.sleep(1);
} catch (InterruptedException e) {
if (!toStop) {
logger.error(e.getMessage(), e);
}
}
}
logger.info(">>>>>>>>>>> xxl-job, executor JobLogFileCleanThread thread destory.");
}
});
localThread.setDaemon(true);
localThread.setName("xxl-job, executor JobLogFileCleanThread");
localThread.start();
}
它的作用就是初始化两个线程:一个是任务 Trigger 执行线程,一个是任务 Trigger 重试线程。然后通过 AdminBizClient 的 callback 方法把任务的执行日志以及任务执行的重试日志发送到调度中心。在调度中心可以展示任务的执行日志。
port = port>0?port: NetUtil.findAvailablePort(9999);
ip = (ip!=null&&ip.trim().length()>0)?ip: IpUtil.getIp();
这里是用于初始化 rpc 调用的服务 IP 地址与端口。所以在配置文件中如果要填写端口,那么一定不要写服务启动的端口冲突。如果冲突的会这个执行器在初始化 RPC 调用的时候就不会成功。这样调度中心在调度执行器也就不会成功了(笔者在第一次配置的时候就犯了这个错误)。
调度器的 RPC 调用其实它是使用自调的 xxl-rpc,功能实现相对简单。我之前有对 dubbo 这个 rpc 框架的源码分析。大家感兴趣可以看一下我之前的 blog。下面我就来简单的分析一下 xxl-rpc 是如何暴露服务给 xxl-job-admin 来远程调用执行器的。
XxlJobExecutor#initRpcProvider
private void initRpcProvider(String ip, int port, String appName, String accessToken) throws Exception {
// init, provider factory
String address = IpUtil.getIpPort(ip, port);
Map serviceRegistryParam = new HashMap();
serviceRegistryParam.put("appName", appName);
serviceRegistryParam.put("address", address);
xxlRpcProviderFactory = new XxlRpcProviderFactory();
xxlRpcProviderFactory.setServer(NettyHttpServer.class);
xxlRpcProviderFactory.setSerializer(HessianSerializer.class);
xxlRpcProviderFactory.setCorePoolSize(20);
xxlRpcProviderFactory.setMaxPoolSize(200);
xxlRpcProviderFactory.setIp(ip);
xxlRpcProviderFactory.setPort(port);
xxlRpcProviderFactory.setAccessToken(accessToken);
xxlRpcProviderFactory.setServiceRegistry(ExecutorServiceRegistry.class);
xxlRpcProviderFactory.setServiceRegistryParam(serviceRegistryParam);
// add services
xxlRpcProviderFactory.addService(ExecutorBiz.class.getName(), null, new ExecutorBizImpl());
// start
xxlRpcProviderFactory.start();
}
这个方法的上面都是为 XxlRpcProviderFactory ,就是 RPC 服务提供者工厂初始化信息。主要是暴露 ExecutorBiz 调度业务的实现类 ExecutorBizImpl。我们来看一下 ExecutorBiz 的接口定义:
public interface ExecutorBiz {
public ReturnT beat();
public ReturnT idleBeat(int jobId);
public ReturnT kill(int jobId);
public ReturnT log(long logDateTim, long logId, int fromLineNum);
public ReturnT run(TriggerParam triggerParam);
}
它主要是定义的任务运行、删除、日志以及心跳检测相关的方法。我们继续回到 XxlRpcProviderFactory#start 方法。
public void start() throws Exception {
// valid
if (this.server == null) {
throw new XxlRpcException("xxl-rpc provider server missing.");
}
if (this.serializer==null) {
throw new XxlRpcException("xxl-rpc provider serializer missing.");
}
if (!(this.corePoolSize>0 && this.maxPoolSize>0 && this.maxPoolSize>=this.corePoolSize)) {
this.corePoolSize = 60;
this.maxPoolSize = 300;
}
if (this.ip == null) {
this.ip = IpUtil.getIp();
}
if (this.port <= 0) {
this.port = 7080;
}
if (NetUtil.isPortUsed(this.port)) {
throw new XxlRpcException("xxl-rpc provider port["+ this.port +"] is used.");
}
// init serializerInstance
this.serializerInstance = serializer.newInstance();
// start server
serviceAddress = IpUtil.getIpPort(this.ip, port);
serverInstance = server.newInstance();
serverInstance.setStartedCallback(new BaseCallback() { // serviceRegistry started
@Override
public void run() throws Exception {
// start registry
if (serviceRegistry != null) {
serviceRegistryInstance = serviceRegistry.newInstance();
serviceRegistryInstance.start(serviceRegistryParam);
if (serviceData.size() > 0) {
serviceRegistryInstance.registry(serviceData.keySet(), serviceAddress);
}
}
}
});
serverInstance.setStopedCallback(new BaseCallback() { // serviceRegistry stoped
@Override
public void run() {
// stop registry
if (serviceRegistryInstance != null) {
if (serviceData.size() > 0) {
serviceRegistryInstance.remove(serviceData.keySet(), serviceAddress);
}
serviceRegistryInstance.stop();
serviceRegistryInstance = null;
}
}
});
serverInstance.start(this);
}
它其实主要作了两件事:
1、注册当前执行器的地址到调度中心
serviceRegistryInstance.registry(serviceData.keySet(), serviceAddress);
在初始化 XxlRpcProviderFactory 传入的注册中心实例是 XxlJobExecutor 的内部类 ExecutorServiceRegistry。我们主要关注它的 start 方法与 stop 方法。
然后它会调用 ExecutorRegistryThread#start 方法,默认情况下 toStop 是 false,所以它会把执行器的地址注册到调度中心。如果调用 toStop 方法,它就会移除注册中心的执行器地址。做到服务的自动上线与下线。
2、注册一个钩子程序,移除注册中心的执行器地址
serverInstance.setStopedCallback(new BaseCallback() { // serviceRegistry stoped
@Override
public void run() {
// stop registry
if (serviceRegistryInstance != null) {
if (serviceData.size() > 0) {
serviceRegistryInstance.remove(serviceData.keySet(), serviceAddress);
}
serviceRegistryInstance.stop();
serviceRegistryInstance = null;
}
}
});
它其实就是注册一个服务停止的回调函数,当服务停止的时候会调用 ServiceRegistry#stop 方法。最终调用
ExecutorRegistryThread#toStop 方法把 toStrop 设置成 true。这样就会调用调度中心的移除执行器地址的方法。
3、暴露一个任务调度的 RPC 服务提供给调度中心调用。
最终它会调用 NettyHttpServer#start 方法启动一下 http 服务,绑定你传入设置执行器的端口。通过 NettyHttpServerHandler 来处理调度中心调度执行器中的任务。下一篇我就来分析一下在调度中心添加任务并且执行任务调度的全过程。