【原始碼分析】XXL-JOB的執行器的註冊流程

xbhog發表於2023-04-22

目的:分析xxl-job執行器的註冊過程

流程:

  1. 獲取執行器中所有被註解(@xxlJjob)修飾的handler
  2. 執行器註冊過程
  3. 執行器中任務執行過程

版本:xxl-job 2.3.1

建議:下載xxl-job原始碼,按流程圖debug除錯,看堆疊資訊並按文章內容理解執行流程

完整流程圖:

img

查詢Handler任務

部分流程圖:

img

首先啟動管理臺介面(服務XxlJobAdminApplication),然後啟動專案中給的執行器例項(SpringBoot);

img

這個方法是掃描專案中使用@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:

img

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.");
}

然後進行遍歷註冊;開始進行名字判斷:

  1. 判斷bean名字是否為空
  2. 判斷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)

  1. 未設定生命週期,則直接開始註冊
//注意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);
}
  1. 有生命週期,設定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屬性,並確保方法名稱與屬性值一致。

執行器的註冊過程

部分流程圖:

img

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;
}

至此執行器的註冊流程分析完成。

執行器中的任務執行過程

img

部分流程圖:

img

執行器中的任務流程比較簡單,如果執行器啟動的話,那麼每次執行任務是透過JobThread透過Cron 表示式進行操作的。

透過handler.execute()進行執行,是在框架內部透過反射機制呼叫作業處理器物件 handler 中的 execute() 方法實現的。在這個過程中,handler 物件表示被載入的作業處理器,並且已經呼叫了init()方法進行初始化。

method.invoke() 方法使用反射機制呼叫指定物件 target 中的方法 method。在這個方法中,target 表示作業處理器物件,method 表示作業處理器中的 execute() 方法。

透過上述方法,獲取到SampleXxlJob.demoJobHandler的任務,然後開始進行任務邏輯操作。

相關文章