这篇文章主要是解析执行器项目七大配置项作用和原理。我们以示例执行器xxl-job-executor-sample-springboot项目为例。

工程结构介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
├─java
│ └─com
│ └─xxl
│ └─job
│ └─executor
│ │ XxlJobExecutorApplication.java ------启动类
│ │
│ ├─core
│ │ └─config
│ │ XxlJobConfig.java ------XxlJobSpringExecutor配置类
│ │
│ ├─mvc
│ │ └─controller
│ │ IndexController.java ------空文件
│ │
│ └─service
│ └─jobhandler
│ SampleXxlJob.java ------示例执行器类

└─resources
application.properties ------项目配置文件
logback.xml ------日志配置文件

com.xxl.job.executor.core.config.XxlJobConfig配置类解读

由于是SpringBoot项目,该类采用@Configuration注解添加配置。

成员变量简介

该配置类一共包含八个成员变量。

日志对象

1
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

slf4j的日志对象,用来打印关键日志。

七大核心属性变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;

@Value("${xxl.job.executor.appname}")
private String appName;

@Value("${xxl.job.executor.ip}")
private String ip;

@Value("${xxl.job.executor.port}")
private int port;

@Value("${xxl.job.accessToken}")
private String accessToken;

@Value("${xxl.job.executor.logpath}")
private String logPath;

@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;

使用Spring提供的@Value注解来注入配置文件application.properties中的配置信息。

我们打开resources目录下的application.properties文件查看,默认配置信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin

### xxl-job executor address
xxl.job.executor.appname=xxl-job-executor-sample
xxl.job.executor.ip=
xxl.job.executor.port=9999

### xxl-job, access token
xxl.job.accessToken=

### xxl-job log path
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### xxl-job log retention days
xxl.job.executor.logretentiondays=30

根据配置文件中的相关注释我们来解释七大核心属性的含义:

  • adminAddresses
    • 配置项:xxl.job.admin.addresses,选填。
    • 含义:调度中心部署根地址。
    • 作用:执行器会使用该地址进行“执行器心跳注册”和任务结果回调。
    • 注意事项:如调度中心集群部署,存在多个根地址,则用逗号分隔。为空不填则关闭自动注册功能。
  • appName
    • 配置项:xxl.job.executor.appname,选填。
    • 含义:是每个执行器集群的唯一标示AppName。
    • 作用:执行器心跳注册分组依据。
    • 注意事项:为空不填表示关闭自动注册功能。
  • ip
    • 配置项:xxl.job.executor.ip,选填。
    • 含义:执行器IP
    • 作用:适用于多网卡时手动设置指定IP,该IP不会绑定Host仅作为通讯使用;用于“执行器注册”和“调度中心请求并触发任务”。
    • 注意事项:为空不填表示自动获取IP
  • port
    • 配置项:xxl.job.executor.port,选填。
    • 含义:执行器端口号。执行器实际是一个内嵌的Server,默认端口9999
    • 作用:用于“执行器注册”和“调度中心请求并触发任务”时通讯。
    • 注意事项:小于等于0时自动获取。单机部署多个执行器时,不同执行器端口不能相同。
  • accessToken
    • 配置项:xxl.job.accessToken,选填。
    • 含义:访问令牌。
    • 作用:为提升系统安全性,调度中心和执行器进行安全性校验,双方accessToken匹配才允许通讯。
    • 注意事项:正常通讯只有两种设置;
      • 设置一:调度中心和执行器均不设置accessToken,关闭访问令牌校验。
      • 设置二:调度中心和执行器设置相同的accessToken
  • logPath
    • 配置项:xxl.job.executor.logpath,选填。
    • 含义:执行器运行日志文件存储磁盘路径。
    • 作用:设置执行器运行日志文件存储磁盘路径。
    • 注意事项:需要对设置的路径拥有读写权限;为空则使用默认路径(/data/applogs/xxl-job/jobhandler)。
  • logRetentionDays
    • 配置项:xxl.job.executor.logretentiondays,选填。
    • 含义:执行器日志文件保存天数。
    • 作用:设置过期日志自动清理。
    • 注意事项:设置的值大于等于3时生效;否则日志自动清理功能关闭。

com.xxl.job.executor.core.config.XxlJobConfig#xxlJobExecutor方法作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppName(appName);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

return xxlJobSpringExecutor;
}

上一部分列举出的七大属性最终设置给了com.xxl.job.core.executor.impl.XxlJobSpringExecutor这个类的对象,并使用@Bean注解交由Spring进行管理。该方法中打印的日志信息 >>>>>>>>>>> xxl-job config init. 我们在启动执行器时控制台有输出。

七大属性配置详解

上一部分列举出了每个属性的注意事项等,这一部分我们去源码中验证上一部分的内容。

目标

我们要找到上述七大属性在设置给com.xxl.job.core.executor.impl.XxlJobSpringExecutor这个类的对象时是如何以及怎样进行条件限制的。

思考

com.xxl.job.executor.core.config.XxlJobConfig#xxlJobExecutor方法来看,这些配置从配置文件中读到值后是直接使用变异器(setter方法)设置给xxlJobSpringExecutor这个对象,然后就把这个对象交给Spring进行管理了,所以只有两种可能,第一是在变异器中进行了逻辑处理,第二是在Spring加载bean的过程中进行了逻辑处理。

实践

我们首先看一下com.xxl.job.core.executor.impl.XxlJobSpringExecutor这个类的定义:

1
2
public class XxlJobSpringExecutor extends XxlJobExecutor implements ApplicationContextAware, InitializingBean, DisposableBean {
......

该类继承了com.xxl.job.core.executor.XxlJobExecutor父类并实现了Spring的三个接口;我们在该类中未找属性对应的变异器,所以我们几乎可以断定这些属性是定义在父类中。

我们来看父类的代码

XxlJobExecutor变异器.png

变异器只是单纯的把传入的值设置给对象,所以排除了第一种可能,情况只能是第二种:在Spring加载bean的过程中进行了逻辑处理。

我们继续往下看父类XxlJobExecutor的代码,发现start方法中用到了这些属性,接下来我们来仔细阅读以下该方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ---------------------- start + stop ----------------------
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);
}

一共有七行核心代码,我们一行一行的来解读:

  1. XxlJobFileAppender.initLogPath(logPath);

从方法名来看显然是初始化日志存储路径,我们进入XxlJobFileAppender类查看initLogPath(logPath)方法,代码如下:

XxlJobFileAppender_initLogPath.png

我们可以看到静态成员变量logBasePath的值为/data/applogs/xxl-job/jobhandler,当logPath属性未配置时该路径即为日志存盘的默认路径。

该方法的实现思路如下:

  • 如果配置的logPath不为空则覆盖静态成员变量logBasePath的值。
  • 以静态成员变量logBasePath的值调用File类的mkdirs方法创建多级文件目录。
  • 调用File类的getPath方法将创建好的文件夹的路径重新赋值给静态成员变量logBasePath

以上就是属性logPath的配置方式。

  1. initAdminBizList(adminAddresses, accessToken);

从方法名来看该方法的作用是初始化调度中心部署根地址集合,其参数是配置项adminAddressesaccessToken的值。我们来看该方法的具体实现:

initAdminBizList.png

该方法的实现思路如下:

  • 判断传入的配置项adminAddresses的值是否为null并且去除两端空格后的长度是否大于零。
  • 如果不满足第一步的条件则什么也不做,即关闭自动注册功能;如果满足第一步的条件,则去除两端空格后调用split方法以英文逗号,分割成字符串数组进行遍历,遍历的第一步是限制数组中的单个地址值不为null并且去除两端空格后的长度大于零,这一步是为了防止,http://127.0.0.1:8080/xxl-job-admin,等类似误配置。
  • 一切限制条件通过后开始创建com.xxl.job.core.biz.client.AdminBizClient类的对象,并添加至静态成员变量adminBizList集合中。这里是在第一次循环时才使用new关键字创建ArrayList集合对象,其思想是“懒加载”,用时才去创建对象。由于Springbean的默认作用域是单例的,所以保证了该初始化方法只会执行一次。

我们来看一下com.xxl.job.core.biz.client.AdminBizClient类的构造方法:

AdminBizClient_AdminBizClient.png

构造方法中对传入的addressUrl进行了简单的valid校验,如果不是以/结尾则将/拼接至末尾。此处可看出我们的adminAddresses配置实际会变成类似http://127.0.0.1:8080/xxl-job-admin/这样的字符串。至于该类对象什么时候使用我们暂时没有线索,大可先不关注这个。

  1. JobLogFileCleanThread.getInstance().start(logRetentionDays);

调用JobLogFileCleanThread类的getInstance方法取得该类对象再调用其start(final long logRetentionDays)方法。

从类名来看是清除日志文件的线程。

我们前往com.xxl.job.core.thread.JobLogFileCleanThread类看一下getInstance方法,会发现有如下两行关键代码:

1
2
3
4
private static JobLogFileCleanThread instance = new JobLogFileCleanThread();
public static JobLogFileCleanThread getInstance() {
return instance;
}

这是“饿汉式”单例模式的写法,创建了单例的JobLogFileCleanThread对象。

接下来看一下start方法:

JobLogFileCleanThread_start.png

首先第一步检查设置的logRetentionDays属性是否小于3,小于则直接return;这里说明了配置文件中的值设置小于3时关闭日志自动清理功能。

该方法中实例化了成员变量private Thread localThread;,调用了setDaemon(true)方法将该线程设置为守护线程,并调用setName方法将线程名设为了xxl-job, executor JobLogFileCleanThread

线程中运行的run方法逻辑大致如下:

  • 调用XxlJobFileAppender.getLogPath()方法获取设置的日志存盘路径
  • 遍历该路径下所有文件夹,判断当前时间与文件夹的创建时间之差是否大于等于设置的logRetentionDays天数,如果大于则调用工具类com.xxl.job.core.util.FileUtildeleteRecursively方法递归删除文件夹下的所有文件。
  • 删除逻辑完成后,有这样一行代码:TimeUnit.DAYS.sleep(1);,线程睡眠一天。这里调用并发包下的TimeUnit类的sleep方法让代码可读性更高,如果直接使用传统Thread.sleep()方法,传给sleep方法的值的单位是毫秒,即需传入24*60*60*1000,代码可读性不高。
  1. TriggerCallbackThread.getInstance().start();

从类名TriggerCallbackThread来看,这是执行器回调线程,由于未使用到配置参数,这篇文章不对其进行展开解读。

  1. port = port>0?port: NetUtil.findAvailablePort(9999);

初始化port端口号。

如果配置文件中设置的端口号大于零,则使用配置文件中的值;

否则执行代码NetUtil.findAvailablePort(9999);,我们进入NetUtil类查看findAvailablePort方法,发现这个类属于com.xxl.rpc.util包,可见,xxl-job依赖了xxl-rpc(这是作者许雪里开源的rpc框架)。

从方法名来看作用是“寻找可用端口”,我们来看一下findAvailablePort方法的具体实现逻辑:

NetUtil_findAvailablePort.png

  • 传入的9999作为默认端口,首先循环999965534端口,逐个进行!isPortUsed(portTmp)判断,如果返回true则表示当前端口号未被使用,返回赋值给属性port
  • 如果上述循环未找到可使用的端口,则再循环99991端口,同样逐个进行!isPortUsed(portTmp)判断,如果还未找到可用端口,则抛出XxlRpcException异常,异常信息为no available port.

我们来看一下isPortUsed方法是如何判断端口是否被使用的:

NetUtil_isPortUsed.png

实际是尝试去创建一个ServerSocket客户端并与传入的端口号进行绑定,如端口被占用则绑定时会抛出IOException,由此来确定端口是否被使用,从而在未配置端口号时选出一个可用端口。最终finally代码块中调用了close方法关闭资源。

  1. ip = (ip!=null&&ip.trim().length()>0)?ip: IpUtil.getIp();

作用:确定ip地址。示例执行器的配置中是未进行配置的,所以会执行IpUtil.getIp()方法,该方法中最后会调用java.net.InetAddress#getHostAddress方法,该方法返回null,所以属性ip会被设置成null

  1. initRpcProvider(ip, port, appName, accessToken);

传入了四个参数:ipportappNameaccessToken用来初始化Rpc服务提供者。这部分属于xxl-rpc的内容,目前我们可以简单看看,大致内容是创建出XxlRpcProviderFactory类的对象,给该对象设置相关属性,添加com.xxl.job.core.biz.impl.ExecutorBizImpl服务至rpc服务提供者map容器中,最后调用start方法启动服务提供者(这里实际是一个NettyServer)。

现在我们大可不必去关注xxl-rpc是怎么实现的,这篇文章的目的是搞清楚七大核心配置的工作原理。

父类XxlJobExecutorstart方法我们看完了,那么它是什么时机执行的呢?

我们注意到XxlJobSpringExecutor类实现了SpringInitializingBean接口,该接口提供了afterPropertiesSet方法供子类实现,在bean加载过程中会执行该方法。

接下来我们来看一下XxlJobSpringExecutor类中重写的afterPropertiesSet方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// start
@Override
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();
}

前三行代码是初始化一些东西,暂时不去关注;最后一行super.start()是我们的关键,调用了父类的start方法。

总结

至此,我们知道了七大配置项的基本原理,对我们使用xxl-job有了一些帮助。例如配置项logretentiondays不能小于3,否则日志文件不会自动清理等。