目的:分析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
的任務,然後開始進行任務邏輯操作。