目的:分析xxl-job執行器的註冊過程
流程:
- 獲取執行器中所有被註解(
@xxlJjob
)修飾的handler
- 執行器註冊過程
- 執行器中任務執行過程
版本:xxl-job 2.3.1
建議:下載xxl-job
源碼,按流程圖debug
調試,看堆棧信息並按文章內容理解執行流程。
完整流程圖:
查找Handler任務
部分流程圖:
首先啓動管理臺界面(服務XxlJobAdminApplication
),然後啓動項目中給的執行器實例(SpringBoot)
;
這個方法是掃描項目中使用@xxlJob
註解的所有handler方法。接着往下走
private void initJobHandlerMethodRepository(ApplicationContext applicationContext) {
if (applicationContext == null) {
return;
}
//獲取該項目中所有的bean,然後遍歷
String[] beanDefinitionNames = applicationContext.getBeanNamesForType(Object.class, false, true);
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = applicationContext.getBean(beanDefinitionName);
Map<Method, XxlJob> annotatedMethods = null; // referred to :org.springframework.context.event.EventListenerMethodProcessor.processBean
try {
annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(),
new MethodIntrospector.MetadataLookup<XxlJob>() {
//注意點★
@Override
public XxlJob inspect(Method method) {
return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
}
});
} catch (Throwable ex) {
logger.error("xxl-job method-jobhandler resolve error for bean[" + beanDefinitionName + "].", ex);
}
//沒有跳過本次循環繼續
if (annotatedMethods==null || annotatedMethods.isEmpty()) {
continue;
}
//獲取了當前執行器中所有@xxl-job的方法,獲取方法以及對應的初始化和銷燬方法
for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
Method executeMethod = methodXxlJobEntry.getKey();
XxlJob xxlJob = methodXxlJobEntry.getValue();
// regist
registJobHandler(xxlJob, bean, executeMethod);
}
}
}
在Spring
案例執行器中有5個handler
:
XxlJobExecutor.registJobHandler()中部分源碼
String name = xxlJob.value();
//make and simplify the variables since they'll be called several times later
Class<?> clazz = bean.getClass();
String methodName = executeMethod.getName();
if (name.trim().length() == 0) {
throw new RuntimeException("xxl-job method-jobhandler name invalid, for[" + clazz + "#" + methodName + "] .");
}
if (loadJobHandler(name) != null) {
throw new RuntimeException("xxl-job jobhandler[" + name + "] naming conflicts.");
}
然後進行遍歷註冊;開始進行名字判斷:
- 判斷bean名字是否爲空
- 判斷bean是否被註冊了(存在了)
loadJobHandler
校驗方式會去該方法中查找:當bean註冊完成後時存放到jobHandlerRepository
一個map
類型中;
private static ConcurrentMap<String, IJobHandler> jobHandlerRepository = new ConcurrentHashMap<String, IJobHandler>();
public static IJobHandler loadJobHandler(String name){
return jobHandlerRepository.get(name);
}
executeMethod.setAccessible(true);
它實現了修改對象訪問權限的功能,參數爲true,則表示允許調用方在使用反射時忽略Java語言的訪問控制檢查.
往後走會判斷該註解的生命週期方法(init和destroy
)
- 未設置生命週期,則直接開始註冊
//注意MethodJobHandler,後面會用到
registJobHandler(name, new MethodJobHandler(bean, executeMethod, initMethod, destroyMethod));
//添加執行器名字及對應的hob方法信息(當前類、方法、init和destroy屬性)
public static IJobHandler registJobHandler(String name, IJobHandler jobHandler){
logger.info(">>>>>>>>>>> xxl-job register jobhandler success, name:{}, jobHandler:{}", name, jobHandler);
return jobHandlerRepository.put(name, jobHandler);
}
- 有生命週期,設置init和destroy方法權限
if (xxlJob.init().trim().length() > 0) {
try {
initMethod = clazz.getDeclaredMethod(xxlJob.init());
initMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException("xxl-job method-jobhandler initMethod invalid, for[" + clazz + "#" + methodName + "] .");
}
}
if (xxlJob.destroy().trim().length() > 0) {
try {
destroyMethod = clazz.getDeclaredMethod(xxlJob.destroy());
destroyMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException("xxl-job method-jobhandler destroyMethod invalid, for[" + clazz + "#" + methodName + "] .");
}
}
首先檢查@XxlJob
註解中的init
屬性是否存在且不爲空。如果存在,則嘗試獲取該類中名爲init
的方法,並將其設置爲可訪問狀態,以便後續調用。
同理,代碼接下來也檢查了@XxlJob
註解中的destroy
屬性是否存在且不爲空,如果是,則獲取該類中名爲destroy
的方法,並設置其爲可訪問狀態。
在這個過程中,如果某個方法不存在或者無法被訪問,則會拋出NoSuchMethodException
異常,並且使用throw new RuntimeException
將其包裝並拋出一個運行時異常。這樣做的目的是爲了提醒開發人員在任務處理器類中正確地設置init和destroy
屬性,並確保方法名稱與屬性值一致。
執行器的註冊過程
部分流程圖:
public void afterSingletonsInstantiated() {
// init JobHandler Repository
/*initJobHandlerRepository(applicationContext);*/
// init JobHandler Repository (for method)
initJobHandlerMethodRepository(applicationContext);
// refresh GlueFactory
GlueFactory.refreshInstance(1);
// super start
try {
super.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
在掃描完執行器中所有的任務後,開始進行執行器註冊XxlJobSpringExecutor中的super.start()
方法。
在初始化執行服務器啓動之前,進行了四種操作,初始化日誌、初始化adminBizList
地址(可視化管理臺地址)、初始化日誌清除、初始化回調線程等。
這裏需要注意的是第二步初始化地址,在初始化服務器啓動的時候需要用到。
private void initEmbedServer(String address, String ip, int port, String appname, String accessToken) throws Exception {
// fill ip port
port = port>0?port: NetUtil.findAvailablePort(9999);
ip = (ip!=null&&ip.trim().length()>0)?ip: IpUtil.getIp();
// generate address
if (address==null || address.trim().length()==0) {
String ip_port_address = IpUtil.getIpPort(ip, port); // registry-address:default use address to registry , otherwise use ip:port if address is null
address = "http://{ip_port}/".replace("{ip_port}", ip_port_address);
}
// accessToken
if (accessToken==null || accessToken.trim().length()==0) {
logger.warn(">>>>>>>>>>> xxl-job accessToken is empty. To ensure system security, please set the accessToken.");
}
// start
embedServer = new EmbedServer();
embedServer.start(address, port, appname, accessToken);
}
繼續到initEmbedServer
,開始初始化ip地址和端口等,需要明白的是,這一步的參數獲取方式其實是第一步讀取**XxlJobConfig**
獲得的;進行ip的校驗和拼接等操作,開始進行真正的註冊。
創建一個嵌入式的HTTP服務器,將當前執行器信息(包含應用名稱和IP地址端口等)註冊到註冊中心,註冊方式的實現在ExecutorRegistryThread
中實現。
校驗名字和註冊中心,如果註冊中心不可用,則等待一段時間後重新嘗試連接。
// registry
while (!toStop) {
try {
RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
try {
ReturnT<String> registryResult = adminBiz.registry(registryParam);
if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
registryResult = ReturnT.SUCCESS;
logger.debug(">>>>>>>>>>> xxl-job registry success, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
break;
} else {
logger.info(">>>>>>>>>>> xxl-job registry fail, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
}
} catch (Exception e) {
logger.info(">>>>>>>>>>> xxl-job registry error, registryParam:{}", registryParam, e);
}
}
} catch (Exception e) {
if (!toStop) {
logger.error(e.getMessage(), e);
}
}
try {
//心跳檢測,默認30s
if (!toStop) {
TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
}
} catch (InterruptedException e) {
if (!toStop) {
logger.warn(">>>>>>>>>>> xxl-job, executor registry thread interrupted, error msg:{}", e.getMessage());
}
}
}
開啓一個新線程,首先構建註冊參數(包含執行器分組、執行器名字、執行器本地地址及端口號),遍歷註冊中心地址,開始進行執行器註冊,註冊方式通過發送http的post請求。
@Override
public ReturnT<String> registry(RegistryParam registryParam) {
return XxlJobRemotingUtil.postBody(addressUrl + "api/registry", accessToken, timeout, registryParam, String.class);
}
在debug
的過程中,XxlJobRemotingUtil
執行到int statusCode = connection.getResponseCode();
纔會跳轉到JobApiController.api
中的註冊地址.
// services mapping
if ("callback".equals(uri)) {
List<HandleCallbackParam> callbackParamList = GsonTool.fromJson(data, List.class, HandleCallbackParam.class);
return adminBiz.callback(callbackParamList);
} else if ("registry".equals(uri)) {
RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class);
return adminBiz.registry(registryParam);
} else if ("registryRemove".equals(uri)) {
RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class);
return adminBiz.registryRemove(registryParam);
} else {
return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found.");
}
最後進入到JobRegistryHelper.registry()
方法中完成數據庫的入庫和更新操作。
通過更新語句判斷該執行器是否註冊,結果小於1,那麼保存註冊器信息,並向註冊中心發送一個請求,更新當前執行器所屬的應用名稱、執行器名稱和 IP 地址等信息,否則跳過。
public ReturnT<String> registry(RegistryParam registryParam) {
//.......
// async execute
registryOrRemoveThreadPool.execute(new Runnable() {
@Override
public void run() {
//更新註冊表信息
int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryUpdate(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
if (ret < 1) {
//保存執行器註冊信息
XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registrySave(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
// fresh 刷新執行器狀態
freshGroupRegistryInfo(registryParam);
}
}
});
return ReturnT.SUCCESS;
}
至此執行器的註冊流程分析完成。
執行器中的任務執行過程
部分流程圖:
執行器中的任務流程比較簡單,如果執行器啓動的話,那麼每次執行任務是通過JobThread
通過Cron
表達式進行操作的。
通過handler.execute()
進行執行,是在框架內部通過反射機制調用作業處理器對象 handler
中的 execute()
方法實現的。在這個過程中,handler 對象表示被加載的作業處理器,並且已經調用了init()
方法進行初始化。
method.invoke()
方法使用反射機制調用指定對象 target
中的方法 method
。在這個方法中,target
表示作業處理器對象,method
表示作業處理器中的 execute()
方法。
通過上述方法,獲取到SampleXxlJob.demoJobHandler
的任務,然後開始進行任務邏輯操作。