欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

分布式定时任务(xxl-job执行器的启动过程源码分析)

程序员文章站 2023-12-09 20:28:57
分布式定时任务—xxl-job学习(二):源码分析——执行器的启动过程前言一、执行器的启动1.1 分析核心类XxlJobSpringExecutor1.1.1 initJobHandlerRepository()1.1.2 initJobHandlerMethodRepository()1.1.3 GlueFactory.refreshInstance(1)1.1.4 super.start()1.2 分析核心类XxlJobExecutor1.2.1 XxlJobFileAppender.initLogPa...

前言

接上篇:分布式定时任务—xxl-job学习(一):简单demo搭建

从上一篇搭建一个简单的分布式demo任务调度项目可以知道,主要是三个部分:

  1. 配置并启动任务调度中心(xxl-job-admin)
  2. 配置并启动业务系统(执行器)
  3. 在调度中心web页面配置执行器及任务

本篇咱们先从业务系统的执行器的配置和启动的源码进行深度分析。
xxl.job.version使用的是 2.2.1-SNAPSHOT版本

一、执行器的启动

在业务定时任务系统

  1. 引入xxl-job的依赖配置
  2. 新增执行器组件配置类XxlJobConfig.java,其中配置了核心类XxlJobSpringExecutor
  3. 新增jobhandler类,类中有带 @XxlJob("xxx")注解的方法

1.1 分析核心类XxlJobSpringExecutor

public class XxlJobSpringExecutor extends XxlJobExecutor implements ApplicationContextAware, SmartInitializingSingleton, DisposableBean {
 
 	@Override
    public void afterSingletonsInstantiated() {
		//。。。。。。。。暂时省略这个方法的具体内容
    }

	@Override
    public void destroy() {
        super.destroy();
    }

	 private void initJobHandlerMethodRepository(ApplicationContext applicationContext) {
		//。。。。。。。。暂时省略这个方法的具体内容
	}

	private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
}

在源码中我们可以看到这个类继承了XxlJobExecutor类,实现了ApplicationContextAware、SmartInitializingSingleton、DisposableBean。

这个对象初始化的时候会调用afterSingletonsInstantiated()方法。

    @Override
    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);
        }
    }
  1. initJobHandlerRepository()和initJobHandlerMethodRepository()是将项目中配置的任务保存在项目的内存中,使用ConcurrentMap<String, IJobHandler>保存,使用 springbean的id为key,具体的任务实例对象为 value。;
  2. 刷新GlueFactory(glue执行工厂),把它刷新为 SpringGlueFactory,在执行 glue 模式的任务时使用 spring 来加载相应实例。
  3. 会调用执行器的核心XxlJobExecutor中的start()方法。

1.1.1 initJobHandlerRepository()

这个方法是旧版本中用来注册带有 @JobHandler 注解的bean的Java类, 2.2.1-SNAPSHOT版本已经不支持该种方式;

1.1.2 initJobHandlerMethodRepository()

private void initJobHandlerMethodRepository(ApplicationContext applicationContext) {
    if (applicationContext == null) {
        return;
    }
    // init job handler from method
    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;
        }

        for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
            Method method = methodXxlJobEntry.getKey();
            XxlJob xxlJob = methodXxlJobEntry.getValue();
            if (xxlJob == null) {
                continue;
            }

            String name = xxlJob.value();
            if (name.trim().length() == 0) {
                throw new RuntimeException("xxl-job method-jobhandler name invalid, for[" + bean.getClass() + "#" + method.getName() + "] .");
            }
            if (loadJobHandler(name) != null) {
                throw new RuntimeException("xxl-job jobhandler[" + name + "] naming conflicts.");
            }

            // execute method
            if (!(method.getParameterTypes().length == 1 && method.getParameterTypes()[0].isAssignableFrom(String.class))) {
                throw new RuntimeException("xxl-job method-jobhandler param-classtype invalid, for[" + bean.getClass() + "#" + method.getName() + "] , " +
                        "The correct method format like \" public ReturnT<String> execute(String param) \" .");
            }
            if (!method.getReturnType().isAssignableFrom(ReturnT.class)) {
                throw new RuntimeException("xxl-job method-jobhandler return-classtype invalid, for[" + bean.getClass() + "#" + method.getName() + "] , " +
                        "The correct method format like \" public ReturnT<String> execute(String param) \" .");
            }
            method.setAccessible(true);

            // init and destory
            Method initMethod = null;
            Method destroyMethod = null;

            if (xxlJob.init().trim().length() > 0) {
                try {
                    initMethod = bean.getClass().getDeclaredMethod(xxlJob.init());
                    initMethod.setAccessible(true);
                } catch (NoSuchMethodException e) {
                    throw new RuntimeException("xxl-job method-jobhandler initMethod invalid, for[" + bean.getClass() + "#" + method.getName() + "] .");
                }
            }
            if (xxlJob.destroy().trim().length() > 0) {
                try {
                    destroyMethod = bean.getClass().getDeclaredMethod(xxlJob.destroy());
                    destroyMethod.setAccessible(true);
                } catch (NoSuchMethodException e) {
                    throw new RuntimeException("xxl-job method-jobhandler destroyMethod invalid, for[" + bean.getClass() + "#" + method.getName() + "] .");
                }
            }

            // registry jobhandler 向`ConcurrentMap<String, IJobHandler>`中保存当前定时任务实例。
            registJobHandler(name, new MethodJobHandler(bean, method, initMethod, destroyMethod));
        }
    }

}

分析:

  1. 从applicationContext中获取所有的bean对象;
  2. 利用MethodIntrospector工具类的selectMethods方法和MetadataLookup接口得到Map<Method, XxlJob>(下边学习下这个工具类核心方法selectMethods的源码)
public static <T> Map<Method, T> selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup) {
	final Map<Method, T> methodMap = new LinkedHashMap<>();
	Set<Class<?>> handlerTypes = new LinkedHashSet<>();
	Class<?> specificHandlerType = null;
	//判断是否是代理类
	if (!Proxy.isProxyClass(targetType)) {
		//如果是代理类,找到实际的类型
		specificHandlerType = ClassUtils.getUserClass(targetType);
		handlerTypes.add(specificHandlerType);
	}
	handlerTypes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetType));
	//遍历所有找到的class对象
	for (Class<?> currentHandlerType : handlerTypes) {
		final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);
		
		ReflectionUtils.doWithMethods(currentHandlerType, method -> {
			//获取指定的method
			Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
			//获取方法关联的元数据,一般是指注解
			T result = metadataLookup.inspect(specificMethod);
			if (result != null) {
				Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
				if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) {
					methodMap.put(specificMethod, result);
				}
			}
		}, ReflectionUtils.USER_DECLARED_METHODS);
	}

	return methodMap;
}
  1. 循环第二步得到的Map<Method, XxlJob>,key就是注解的id,value是注解元数据。
  2. 校验注解元数据的name属性,如果为空则抛出异常;
  3. 根据name从内存ConcurrentMap<String, IJobHandler>(这其实是注册的时候存的所有任务的仓库)获取对应任务实例,如果已经存在,则抛出异常(任务冲突);
  4. 校验入参,必须为String param,因为 2.2.1-SNAPSHOT指定了开发Job方法,方式格式要求为 “public ReturnT< String> execute(String param)”。
  5. 校验出参,必须是ReturnT< String>格式;
  6. 注入元数据中配置的init()和destroy()方法;
  7. ConcurrentMap<String, IJobHandler>中保存当前定时任务实例。

1.1.3 GlueFactory.refreshInstance(1)

刷新GlueFactory为 SpringGlueFactory,在执行 glue 模式的任务时使用 spring 来加载相应实例。

1.1.4 super.start()

调用XxlJobExecutor.start()

1.2 分析核心类XxlJobExecutor

XxlJobExecutor的属性有:

    // ---------------------- param ----------------------
    private String adminAddresses;
    private String accessToken;
    private String appname;
    private String address;
    private String ip;
    private int port;
    private String logPath;
    private int logRetentionDays;

上一步介绍了最终XxlJobSpringExecutor会调用XxlJobExecutorstart()方法,下边我们继续看看这个方法做些什么:

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
    initEmbedServer(address, ip, port, appname, accessToken);
}

1.2.1 XxlJobFileAppender.initLogPath(logPath)

logPath是我们配置执行器组件里的xxl.job.executor.logpath日志路径。

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();
}
  1. 如果配置了日志路径,那么logBasePath就是我们配置文件里的地址;
  2. 判断这个日志路径是否存在,如果不存在则创建日志目录;
  3. 生成gluesource子文件夹;

1.2.2 initAdminBizList(adminAddresses, accessToken)

    // ---------------------- admin-client (rpc invoker) ----------------------
    private static List<AdminBiz> adminBizList;
    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);
                }
            }
        }
    }
    public static List<AdminBiz> getAdminBizList(){
        return adminBizList;
    }

这个方法是根据调度中心部署跟地址adminAddresses和执行器通讯TOKENaccessToken初始化AdminBizClient,AdminBizClient这个类有三个核心方法

    @Override
    public ReturnT<String> callback(List<HandleCallbackParam> callbackParamList) {
        return XxlJobRemotingUtil.postBody(addressUrl+"api/callback", accessToken, timeout, callbackParamList, String.class);
    }

    @Override
    public ReturnT<String> registry(RegistryParam registryParam) {
        return XxlJobRemotingUtil.postBody(addressUrl + "api/registry", accessToken, timeout, registryParam, String.class);
    }

    @Override
    public ReturnT<String> registryRemove(RegistryParam registryParam) {
        return XxlJobRemotingUtil.postBody(addressUrl + "api/registryRemove", accessToken, timeout, registryParam, String.class);
    }

提供callback(回调)、registry(注册)以及registryRemove(注册移除)到调度中心的方法。

1.2.3 JobLogFileCleanThread.getInstance().start(logRetentionDays)

这个方法是初始化日志清除线程,过期日志自动清理(清理N天前的日志文件)。

public class JobLogFileCleanThread {
    private static Logger logger = LoggerFactory.getLogger(JobLogFileCleanThread.class);

    private static JobLogFileCleanThread instance = new JobLogFileCleanThread();
    public static JobLogFileCleanThread getInstance(){
        return instance;
    }

    private Thread localThread;
    private volatile boolean toStop = false;
    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();
    }

    public void toStop() {
        toStop = true;

        if (localThread == null) {
            return;
        }

        // interrupt and wait
        localThread.interrupt();
        try {
            localThread.join();
        } catch (InterruptedException e) {
            logger.error(e.getMessage(), e);
        }
    }

}

我们主要关注下start()方法:

  1. 执行器组件配置的执行器日志文件保存天数必须大于3,否则不清理;
  2. 创建一个守护线程,每天执行一次(TimeUnit.DAYS.sleep(1););
  3. 获取日志路径根目录下的所有日期文件目录;
  4. 循环判断当前时间(当天的0时0分0秒0毫秒)和日期目录对应的"yyyy-MM-dd"时间差值是否大于配置的执行器日志文件保存天数参数;
(todayDate.getTime()-logFileCreateDate.getTime()) >= logRetentionDays * (24 * 60 * 60 * 1000) 
  1. 如果超过日志保存天数,则删除该时间目录及其目录下所有文件。

1.2.4 TriggerCallbackThread.getInstance().start()

public void start() {

    // valid
    if (XxlJobExecutor.getAdminBizList() == null) {
        logger.warn(">>>>>>>>>>> xxl-job, executor callback config fail, adminAddresses is null.");
        return;
    }

    // callback
    triggerCallbackThread = new Thread(new Runnable() {

        @Override
        public void run() {

            // normal callback
            while(!toStop){
                try {
                    HandleCallbackParam callback = getInstance().callBackQueue.take();
                    if (callback != null) {

                        // callback list param
                        List<HandleCallbackParam> callbackParamList = new ArrayList<HandleCallbackParam>();
                        int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList);
                        callbackParamList.add(callback);

                        // callback, will retry if error
                        if (callbackParamList!=null && callbackParamList.size()>0) {
                            doCallback(callbackParamList);
                        }
                    }
                } catch (Exception e) {
                    if (!toStop) {
                        logger.error(e.getMessage(), e);
                    }
                }
            }

            // last callback
            try {
                List<HandleCallbackParam> callbackParamList = new ArrayList<HandleCallbackParam>();
                int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList);
                if (callbackParamList!=null && callbackParamList.size()>0) {
                    doCallback(callbackParamList);
                }
            } catch (Exception e) {
                if (!toStop) {
                    logger.error(e.getMessage(), e);
                }
            }
            logger.info(">>>>>>>>>>> xxl-job, executor callback thread destory.");

        }
    });
    triggerCallbackThread.setDaemon(true);
    triggerCallbackThread.setName("xxl-job, executor TriggerCallbackThread");
    triggerCallbackThread.start();


    // retry
    triggerRetryCallbackThread = new Thread(new Runnable() {
        @Override
        public void run() {
            while(!toStop){
                try {
                    retryFailCallbackFile();
                } catch (Exception e) {
                    if (!toStop) {
                        logger.error(e.getMessage(), e);
                    }

                }
                try {
                    TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
                } catch (InterruptedException e) {
                    if (!toStop) {
                        logger.error(e.getMessage(), e);
                    }
                }
            }
            logger.info(">>>>>>>>>>> xxl-job, executor retry callback thread destory.");
        }
    });
    triggerRetryCallbackThread.setDaemon(true);
    triggerRetryCallbackThread.start();

}

分析:

  1. 校验有无配置调度中心,没有则抛出异常;
  2. 新建一个回调守护线程;
  3. 如果执行器正常运行,则从任务执行结果回调队列LinkedBlockingQueue<HandleCallbackParam> callBackQueue中获取一个回调入参对象
    HandleCallbackParam callback = getInstance().callBackQueue.take();
    
    如果callback不为空,则用drainTo()方法批量获取回调入参集合,调用doCallback(callbackParamList)方法;
  4. 如果执行器终止,则直接用drainTo()方法批量获取回调队列里的回调入参集合,如果回调入参集合不为空则调用doCallback(callbackParamList)方法;
  5. 分析下doCallback(callbackParamList)方法:
        private void doCallback(List<HandleCallbackParam> callbackParamList){
            boolean callbackRet = false;
            // callback, will retry if error
            for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
                try {
                    ReturnT<String> callbackResult = adminBiz.callback(callbackParamList);
                    if (callbackResult!=null && ReturnT.SUCCESS_CODE == callbackResult.getCode()) {
                        callbackLog(callbackParamList, "<br>----------- xxl-job job callback finish.");
                        callbackRet = true;
                        break;
                    } else {
                        callbackLog(callbackParamList, "<br>----------- xxl-job job callback fail, callbackResult:" + callbackResult);
                    }
                } catch (Exception e) {
                    callbackLog(callbackParamList, "<br>----------- xxl-job job callback error, errorMsg:" + e.getMessage());
                }
            }
            if (!callbackRet) {
                appendFailCallbackFile(callbackParamList);
            }
        }
    
    循环所有的AdminBiz,调用callback(callbackParamList)方法执行回调,调用callbackLog()方法记录当前任务执行日志并生成日志文件,如果发生异常则调用appendFailCallbackFile(callbackParamList)方法。
    private void appendFailCallbackFile(List<HandleCallbackParam> callbackParamList){
        // valid
        if (callbackParamList==null || callbackParamList.size()==0) {
            return;
        }
    
        // append file
        byte[] callbackParamList_bytes = JdkSerializeTool.serialize(callbackParamList);
    
        File callbackLogFile = new File(failCallbackFileName.replace("{x}", String.valueOf(System.currentTimeMillis())));
        if (callbackLogFile.exists()) {
            for (int i = 0; i < 100; i++) {
                callbackLogFile = new File(failCallbackFileName.replace("{x}", String.valueOf(System.currentTimeMillis()).concat("-").concat(String.valueOf(i)) ));
                if (!callbackLogFile.exists()) {
                    break;
                }
            }
        }
        FileUtil.writeFileContent(callbackLogFile, callbackParamList_bytes);
    }
    
    • 如果回调入参集合不为空,使用JdkSerializeTool工具类,将集合序列化为byte[];
    • 在日志根目录下创建callbacklog目录,生成回调失败记录文件xxl-job-callback-{x}.log,其中{x}为当前时间戳;
    • 判断当前文件是否存在,如果存在则生成xxl-job-callback-{x}-i.log(i为0~100的数值)
    • 将byte[]写入回调失败记录文件中。
  6. 新建一个回调重试守护线程,每30秒执行一次
    TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
  7. 当执行器还在运行时,回调重试守护线程调用retryFailCallbackFile()方法。
    private void retryFailCallbackFile(){
    
        // valid
        File callbackLogPath = new File(failCallbackFilePath);
        if (!callbackLogPath.exists()) {
            return;
        }
        if (callbackLogPath.isFile()) {
            callbackLogPath.delete();
        }
        if (!(callbackLogPath.isDirectory() && callbackLogPath.list()!=null && callbackLogPath.list().length>0)) {
            return;
        }
    
        // load and clear file, retry
        for (File callbaclLogFile: callbackLogPath.listFiles()) {
            byte[] callbackParamList_bytes = FileUtil.readFileContent(callbaclLogFile);
            List<HandleCallbackParam> callbackParamList = (List<HandleCallbackParam>) JdkSerializeTool.deserialize(callbackParamList_bytes, List.class);
    
            callbaclLogFile.delete();
            doCallback(callbackParamList);
        }
    
    }
    
    分析:
    • 判断回调失败日志记录目录是否存在,不存在则跳出方法;
    • 如果回调失败日志记录目录下没有子文件则跳出方法;
    • 循环每个子文件,使用JdkSerializeTool工具类将文件中读出的byte[]转为回调入参对象集合List< HandleCallbackParam>,删除对应日志记录文件,调用doCallback(callbackParamList)方法执行回调。

1.2.5 initEmbedServer(address, ip, port, appname, accessToken)

    // ---------------------- executor-server (rpc provider) ----------------------
    private EmbedServer embedServer = null;

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

        // start
        embedServer = new EmbedServer();
        embedServer.start(address, port, appname, accessToken);
    }

    private void stopEmbedServer() {
        // stop provider factory
        try {
            embedServer.stop();
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }

分析:

  1. 初始化执行器IP 地址与端口,如果IP没配置则默认取当前服务的地址,如果端口没配置则默认为9999;
  2. 生成服务调用地址"http://{ip_port}/"
  3. 调用embedServer.start(address, port, appname, accessToken)方法。

1.3 分析EmbedServer.start(address, port, appname, accessToken)

public void start(final String address, final int port, final String appname, final String accessToken) {
    executorBiz = new ExecutorBizImpl();
    thread = new Thread(new Runnable() {

        @Override
        public void run() {

            // param
            EventLoopGroup bossGroup = new NioEventLoopGroup();
            EventLoopGroup workerGroup = new NioEventLoopGroup();
            ThreadPoolExecutor bizThreadPool = new ThreadPoolExecutor(
                    0,
                    200,
                    60L,
                    TimeUnit.SECONDS,
                    new LinkedBlockingQueue<Runnable>(2000),
                    new ThreadFactory() {
                        @Override
                        public Thread newThread(Runnable r) {
                            return new Thread(r, "xxl-rpc, EmbedServer bizThreadPool-" + r.hashCode());
                        }
                    },
                    new RejectedExecutionHandler() {
                        @Override
                        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                            throw new RuntimeException("xxl-job, EmbedServer bizThreadPool is EXHAUSTED!");
                        }
                    });


            try {
                // start server
                ServerBootstrap bootstrap = new ServerBootstrap();
                bootstrap.group(bossGroup, workerGroup)
                        .channel(NioServerSocketChannel.class)
                        .childHandler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            public void initChannel(SocketChannel channel) throws Exception {
                                channel.pipeline()
                                        .addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS))  // beat 3N, close if idle
                                        .addLast(new HttpServerCodec())
                                        .addLast(new HttpObjectAggregator(5 * 1024 * 1024))  // merge request & reponse to FULL
                                        .addLast(new EmbedHttpServerHandler(executorBiz, accessToken, bizThreadPool));
                            }
                        })
                        .childOption(ChannelOption.SO_KEEPALIVE, true);

                // bind
                ChannelFuture future = bootstrap.bind(port).sync();

                logger.info(">>>>>>>>>>> xxl-job remoting server start success, nettype = {}, port = {}", EmbedServer.class, port);

                // start registry
                startRegistry(appname, address);

                // wait util stop
                future.channel().closeFuture().sync();

            } catch (InterruptedException e) {
                if (e instanceof InterruptedException) {
                    logger.info(">>>>>>>>>>> xxl-job remoting server stop.");
                } else {
                    logger.error(">>>>>>>>>>> xxl-job remoting server error.", e);
                }
            } finally {
                // stop
                try {
                    workerGroup.shutdownGracefully();
                    bossGroup.shutdownGracefully();
                } catch (Exception e) {
                    logger.error(e.getMessage(), e);
                }
            }

        }

    });
    thread.setDaemon(true);	// daemon, service jvm, user thread leave >>> daemon leave >>> jvm leave
    thread.start();
}

1.3.1 ExecutorBiz调度业务的实现类ExecutorBizImpl

定义了任务的心跳检测、忙碌检测、触发任务、终止任务以及查看执行日志的方法

1.3.1.1 心跳检测

    @Override
    public ReturnT<String> beat() {
        return ReturnT.SUCCESS;
    }

1.3.1.2 忙碌检测

    @Override
    public ReturnT<String> idleBeat(IdleBeatParam idleBeatParam) {

        // isRunningOrHasQueue
        boolean isRunningOrHasQueue = false;
        JobThread jobThread = XxlJobExecutor.loadJobThread(idleBeatParam.getJobId());
        if (jobThread != null && jobThread.isRunningOrHasQueue()) {
            isRunningOrHasQueue = true;
        }

        if (isRunningOrHasQueue) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, "job thread is running or has trigger queue.");
        }
        return ReturnT.SUCCESS;
    }

分析:

  1. 根据jobId从任务线程仓库ConcurrentMap<Integer, JobThread> jobThreadRepository中获取对应的任务线程;
  2. 如果任务线程对象不为空且正在运行或者正在队列中则认为处于忙碌状态。

1.3.1.3 触发任务

@Override
public ReturnT<String> run(TriggerParam triggerParam) {
    // load old:jobHandler + jobThread
    JobThread jobThread = XxlJobExecutor.loadJobThread(triggerParam.getJobId());
    IJobHandler jobHandler = jobThread!=null?jobThread.getHandler():null;
    String removeOldReason = null;

    // valid:jobHandler + jobThread
    GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerParam.getGlueType());
    if (GlueTypeEnum.BEAN == glueTypeEnum) {

        // new jobhandler
        IJobHandler newJobHandler = XxlJobExecutor.loadJobHandler(triggerParam.getExecutorHandler());

        // valid old jobThread
        if (jobThread!=null && jobHandler != newJobHandler) {
            // change handler, need kill old thread
            removeOldReason = "change jobhandler or glue type, and terminate the old job thread.";

            jobThread = null;
            jobHandler = null;
        }

        // valid handler
        if (jobHandler == null) {
            jobHandler = newJobHandler;
            if (jobHandler == null) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, "job handler [" + triggerParam.getExecutorHandler() + "] not found.");
            }
        }

    } else if (GlueTypeEnum.GLUE_GROOVY == glueTypeEnum) {

        // valid old jobThread
        if (jobThread != null &&
                !(jobThread.getHandler() instanceof GlueJobHandler
                    && ((GlueJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) {
            // change handler or gluesource updated, need kill old thread
            removeOldReason = "change job source or glue type, and terminate the old job thread.";

            jobThread = null;
            jobHandler = null;
        }

        // valid handler
        if (jobHandler == null) {
            try {
                IJobHandler originJobHandler = GlueFactory.getInstance().loadNewInstance(triggerParam.getGlueSource());
                jobHandler = new GlueJobHandler(originJobHandler, triggerParam.getGlueUpdatetime());
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
                return new ReturnT<String>(ReturnT.FAIL_CODE, e.getMessage());
            }
        }
    } else if (glueTypeEnum!=null && glueTypeEnum.isScript()) {

        // valid old jobThread
        if (jobThread != null &&
                !(jobThread.getHandler() instanceof ScriptJobHandler
                        && ((ScriptJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) {
            // change script or gluesource updated, need kill old thread
            removeOldReason = "change job source or glue type, and terminate the old job thread.";

            jobThread = null;
            jobHandler = null;
        }

        // valid handler
        if (jobHandler == null) {
            jobHandler = new ScriptJobHandler(triggerParam.getJobId(), triggerParam.getGlueUpdatetime(), triggerParam.getGlueSource(), GlueTypeEnum.match(triggerParam.getGlueType()));
        }
    } else {
        return new ReturnT<String>(ReturnT.FAIL_CODE, "glueType[" + triggerParam.getGlueType() + "] is not valid.");
    }

    // executor block strategy
    if (jobThread != null) {
        ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(triggerParam.getExecutorBlockStrategy(), null);
        if (ExecutorBlockStrategyEnum.DISCARD_LATER == blockStrategy) {
            // discard when running
            if (jobThread.isRunningOrHasQueue()) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, "block strategy effect:"+ExecutorBlockStrategyEnum.DISCARD_LATER.getTitle());
            }
        } else if (ExecutorBlockStrategyEnum.COVER_EARLY == blockStrategy) {
            // kill running jobThread
            if (jobThread.isRunningOrHasQueue()) {
                removeOldReason = "block strategy effect:" + ExecutorBlockStrategyEnum.COVER_EARLY.getTitle();

                jobThread = null;
            }
        } else {
            // just queue trigger
        }
    }

    // replace thread (new or exists invalid)
    if (jobThread == null) {
        jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason);
    }

    // push data to queue
    ReturnT<String> pushResult = jobThread.pushTriggerQueue(triggerParam);
    return pushResult;
}

分析:

  1. 根据jobId从任务线程仓库ConcurrentMap<Integer, JobThread> jobThreadRepository中获取对应的任务线程;
  2. 根据任务类型校验任务线程和任务具体处理实例;(如果是"BEAN"类型的任务,根据任务处理实例id从ConcurrentMap<String, IJobHandler> jobHandlerRepository获取到具体的任务处理实例,如果任务线程不为空并且任务具体实例和从任务线程中拿到的任务实例不同则必须更改任务类型并且终止之前的任务线程)
  3. 如果任务线程不为空,获取任务的阻塞处理策略(如果策略是丢弃后续调度,则本次请求将会被丢弃并返回为失败;如果策略是覆盖之前调度,则把任务线程置为null,移除原因置为“xxxxxx”);
  4. 如果任务线程为null,调用XxlJobExecutor.registJobThread()进行保存;
    public static JobThread registJobThread(int jobId, IJobHandler handler, String removeOldReason){
        JobThread newJobThread = new JobThread(jobId, handler);
        newJobThread.start();
        logger.info(">>>>>>>>>>> xxl-job regist JobThread success, jobId:{}, handler:{}", new Object[]{jobId, handler});
    
        JobThread oldJobThread = jobThreadRepository.put(jobId, newJobThread);	// putIfAbsent | oh my god, map's put method return the old value!!!
        if (oldJobThread != null) {
            oldJobThread.toStop(removeOldReason);
            oldJobThread.interrupt();
        }
    
        return newJobThread;
    }
    
    新建一个JobThread;判断任务线程仓库ConcurrentMap<Integer, JobThread> jobThreadRepository中是否已经存在旧值,如果存在则销毁对象,中断线程。
  5. 调用pushTriggerQueue()方法将入参放入任务线程的执行队列中。
    public ReturnT<String> pushTriggerQueue(TriggerParam triggerParam) {
    	// avoid repeat
    	if (triggerLogIdSet.contains(triggerParam.getLogId())) {
    		logger.info(">>>>>>>>>>> repeate trigger job, logId:{}", triggerParam.getLogId());
    		return new ReturnT<String>(ReturnT.FAIL_CODE, "repeate trigger job, logId:" + triggerParam.getLogId());
    	}
    
    	triggerLogIdSet.add(triggerParam.getLogId());
    	triggerQueue.add(triggerParam);
           return ReturnT.SUCCESS;
    }
    
    根据日志记录id判断任务是否重复执行

1.3.1.4 终止任务

@Override
public ReturnT<String> kill(KillParam killParam) {
    // kill handlerThread, and create new one
    JobThread jobThread = XxlJobExecutor.loadJobThread(killParam.getJobId());
    if (jobThread != null) {
        XxlJobExecutor.removeJobThread(killParam.getJobId(), "scheduling center kill job.");
        return ReturnT.SUCCESS;
    }

    return new ReturnT<String>(ReturnT.SUCCESS_CODE, "job thread already killed.");
}

根据jobId获取任务线程对象,调用removeJobThread()方法终止线程。

public static JobThread removeJobThread(int jobId, String removeOldReason){
    JobThread oldJobThread = jobThreadRepository.remove(jobId);
    if (oldJobThread != null) {
        oldJobThread.toStop(removeOldReason);
        oldJobThread.interrupt();

        return oldJobThread;
    }
    return null;
}

1.3.1.5 查看执行日志

@Override
public ReturnT<LogResult> log(LogParam logParam) {
    // log filename: logPath/yyyy-MM-dd/9999.log
    String logFileName = XxlJobFileAppender.makeLogFileName(new Date(logParam.getLogDateTim()), logParam.getLogId());

    LogResult logResult = XxlJobFileAppender.readLog(logFileName, logParam.getFromLineNum());
    return new ReturnT<LogResult>(logResult);
}

1.3.2 创建一个守护线程供调度中心调用

1.3.2.1 暴露一个任务调度的RPC服务提供给调度中心调用

使用netty_http启动http 服务,绑定你传入设置执行器的端口。通过 EmbedHttpServerHandler来处理调度中心调度执行器中的任务

1.3.2.2 注册当前执行器的地址到调度中心

startRegistry(appname, address)

public void startRegistry(final String appname, final String address) {
    // start registry
    ExecutorRegistryThread.getInstance().start(appname, address);
}
1.3.2.2.1 分析ExecutorRegistryThread类
public void start(final String appname, final String address){

    // valid
    if (appname==null || appname.trim().length()==0) {
        logger.warn(">>>>>>>>>>> xxl-job, executor registry config fail, appname is null.");
        return;
    }
    if (XxlJobExecutor.getAdminBizList() == null) {
        logger.warn(">>>>>>>>>>> xxl-job, executor registry config fail, adminAddresses is null.");
        return;
    }

    registryThread = new Thread(new Runnable() {
        @Override
        public void run() {

            // 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 {
                    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());
                    }
                }
            }

            // registry remove
            try {
                RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
                for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
                    try {
                        ReturnT<String> registryResult = adminBiz.registryRemove(registryParam);
                        if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
                            registryResult = ReturnT.SUCCESS;
                            logger.info(">>>>>>>>>>> xxl-job registry-remove success, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                            break;
                        } else {
                            logger.info(">>>>>>>>>>> xxl-job registry-remove fail, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                        }
                    } catch (Exception e) {
                        if (!toStop) {
                            logger.info(">>>>>>>>>>> xxl-job registry-remove error, registryParam:{}", registryParam, e);
                        }

                    }

                }
            } catch (Exception e) {
                if (!toStop) {
                    logger.error(e.getMessage(), e);
                }
            }
            logger.info(">>>>>>>>>>> xxl-job, executor registry thread destory.");

        }
    });
    registryThread.setDaemon(true);
    registryThread.setName("xxl-job, executor ExecutorRegistryThread");
    registryThread.start();
}
  1. 校验执行器配置appname和调度中心地址adminAddresses(不能为空);
  2. 创建一个注册守护线程registryThread(每30秒注册一次);
  3. 运行状态下,循环执行器配置的所有AdminBizClient,调用registry()方法发起注册;
  4. 服务下线后,调用registryRemove()方法发起注册移除。

二、彩蛋

后续我们深度跟进一下 调度中心(xxl-job-admin)的启动和任务调度过程源码分析

本文地址:https://blog.csdn.net/RabbitInTheGrass/article/details/106918236