從之前的 Xxl Job Helloworld 中學會了簡單的使用 Xxl-Job
進行分步式任務調度。並且可以知道當我使用 Xxl-Job
時,我們核心基本需要以下三個步驟:
- 啓動調度中心(xxl-job-admin)
- 啓動執行器(引用xxl-job-core 的業務代碼)
- 調度中心添加任務並執行任務
經過以上的三個步驟,然後添加的任務就可以執行了。下面我們就來從源碼的角度分析一下:上面 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);
}
1、初始化日誌路徑
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();
}
它會根據在執行器配置中定義的日誌保存路徑,看這個日誌路徑是否存在。如果不存在則會創建任務執行的日誌目錄。
2、初始化 AdminBiz
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<AdminBiz>();
}
adminBizList.add(adminBiz);
}
}
}
}
初始化 AdminBizClient 這個類包含callback(回調)、registry(註冊)以及registryRemove(註冊移除)這三個方法。當執行器啓動的時候會把執行器執行地址註冊到調度中心裏面。
3、初始化日誌清除線程
這個線程的作用是清除掉傳入日期之前的日誌。比如今天是 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();
}
4、初始化 Trigger 回調線程
它的作用就是初始化兩個線程:一個是任務 Trigger 執行線程,一個是任務 Trigger 重試線程。然後通過 AdminBizClient 的 callback 方法把任務的執行日誌以及任務執行的重試日誌發送到調度中心。在調度中心可以展示任務的執行日誌。
5、獲取當前服務信息
port = port>0?port: NetUtil.findAvailablePort(9999);
ip = (ip!=null&&ip.trim().length()>0)?ip: IpUtil.getIp();
這裏是用於初始化 rpc 調用的服務 IP 地址與端口。所以在配置文件中如果要填寫端口,那麼一定不要寫服務啓動的端口衝突。如果衝突的會這個執行器在初始化 RPC 調用的時候就不會成功。這樣調度中心在調度執行器也就不會成功了(筆者在第一次配置的時候就犯了這個錯誤)。
6、調度器 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<String, String> serviceRegistryParam = new HashMap<String, String>();
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<String> beat();
public ReturnT<String> idleBeat(int jobId);
public ReturnT<String> kill(int jobId);
public ReturnT<LogResult> log(long logDateTim, long logId, int fromLineNum);
public ReturnT<String> 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 來處理調度中心調度執行器中的任務。下一篇我就來分析一下在調度中心添加任務並且執行任務調度的全過程。