xxl-job源码分析之----XxlJobSpringExecutor分析

一、介绍

上一章xxl-job源码分析之---- 搭建项目demo简单介绍了搭建相关的步骤,本章主要介绍xxl-job 的核心jar(xxl-jar-core),这里用的版本是最新的 xxl-job-2.2.0,更新比较快,不同版本可能逻辑略有不同

二、流程分析(Spring 框架)

在项目里面需要注入XxlJobSpringExecutor.class 这个类, 就从这个类开始分析,首先其类的层次结构如下,
XxlJobSpringExecutor 继承了 XxlJobExecutor,实现了 ApplicationContextAware, DisposableBean,SmartInitializingSingleton
可以看出 ,其实整个逻辑是在类XxlJobExecutor 里面,类XxlJobSpringExecutor 是为了匹配Spring 框架而做的改造
实现ApplicationContextAware 是为了获取上下文
实现DisposableBean是为了重写destroy 方法,用于释放一些资源
实现SmartInitializingSingleton,这个比较关键,也是重点,就是在Bean 创建的生命周期里面,单例 bean 都初始化完成以后,找出带有注解的 job进行注册等一系列操作
在这里插入图片描述

2.1 afterSingletonsInstantiated 里面逻辑

afterSingletonsInstantiated 方法里面主要做了三件事:

  1. 初始化JobHandler ,就是找到 带有注解@XxlJob的 Job ,进行注册
  2. 刷新GlueFactory
  3. 调用父类start() 方法,上面说过XxlJobExecutor 里面的才是核心逻辑
    public void afterSingletonsInstantiated() {
    
        //  初始化JobHandler ,就是找到 带有注解@XxlJob的 Job ,进行注册
        initJobHandlerMethodRepository(applicationContext);

        // 刷新GlueFactory, 这里type 为 0/1
        // 1就是说明使用的是Spring 框架
        GlueFactory.refreshInstance(1);

        //启动
        try {
            super.start();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

2.2 XxlJobSpringExecutor#initJobHandlerMethodRepository

initJobHandlerMethodRepository 方法 主要做了以下几件事:

  1. 获取到所有的注册的beanName
  2. 针对每一个bean 进行扫描,获取带有@XxlJob 注解的方法,涉及一个工具类工具类MethodIntrospector
  3. 对每一个获带有@XxlJob 注解的方法进行校验
    这里进行一些校验:
    1. job 名字不能为空
    2. job 名字不能重复
    3. 入参必须要用,而且只能有1个, 还要是String 类型
    4. 返回类型 必须要是ReturnT.class
  4. 获取 初始化方法init() 和 销毁方法 destroy()
  5. 放入Map ,后续进行注册
private void initJobHandlerMethodRepository(ApplicationContext applicationContext) {
        if (applicationContext == null) {
            return;
        }
        //获取到所有的注册的beanName
        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
            // 针对每一个bean 进行扫描,获取带有@XxlJob 注解的方法
            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;
            }
            // 对获取到的带有@XxlJob 注解的方法进行处理
            for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
                Method method = methodXxlJobEntry.getKey();
                XxlJob xxlJob = methodXxlJobEntry.getValue();
                if (xxlJob == null) {
                    continue;
                }

                /**
                这里进行一些校验, 
                1. job 名字不能为空
                2. job 名字不能重复
                3. 入参必须要用,而且只能有1个, 还要是String 类型
                4. 返回类型 必须要是ReturnT.class
                **/
                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 和销毁方法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
                // 放入jobHandlerRepository Map
                registJobHandler(name, new MethodJobHandler(bean, method, initMethod, destroyMethod));
            }
        }

    }

2.3 XxlJobSpringExecutor#start

start方法 ,有点像Spring 里面的refresh() 方法,将几个主要的大步骤都列出来了,主要做了以下几件事:

  1. 初始化log路径,创建对应的log路径文件夹
  2. 初始化 admin-client, 封装成一个或者多个AdminBizClient 对象,放在adminBizList list里面,以便后面的回调,注册,移除等操作
  3. 启动日志线程,定时清理日志
    public void start() throws Exception {

        // 初始化log路径,没有给定,默认是 /data/applogs/xxl-job/jobhandler
        XxlJobFileAppender.initLogPath(logPath);

        /** 初始化invoker, admin-client
         这里就是给定的 admin server 端地址,一个或者多个,封装成AdminBizClient 对象
         放在adminBizList list里面,以便后面的回调,注册,移除等操作
        **/
        initAdminBizList(adminAddresses, accessToken);

        // 初始化 日志 清理线程,定时清理
        JobLogFileCleanThread.getInstance().start(logRetentionDays);

        // 初始化回调线程
        TriggerCallbackThread.getInstance().start();

        // 初始化 executor-server
        initEmbedServer(address, ip, port, appname, accessToken);
    }

2.3.1 日志路径

log日志默认是在 /data/applogs/xxl-job/jobhandler 目录下面,目录结构如下
在这里插入图片描述

2.3.2 日志文件清理线程

日志线程主要作用就是清除超过指定logRetentionDays 天数的日志,此线程主要做了如下事情:

  1. 判断用户定义的时间, 最小3天 ,小于3天时,没有定时清理日志这个功能, 这个不合理啊, 应该 小于3天给个默认值 3天,不应该不生效啊
  2. 启动一个守护进程,每隔1天进行一次处理,主要逻辑如下
  3. 获取日志目录下面的所有的文件和子目录
  4. 对所有文件和目录进行遍历,去掉文件,和文件夹名字中不带有‘-’ (这个是由于 文件名是用 日期 yyyy-MM-dd 定义的)
  5. 将文件夹名字上的日期转为 时间,和当前进行对比 ,超过了 最大保留时间,就删除
    public void start(final long logRetentionDays){

        // limit min value
        // 这里的日志最少3天,小于3天还不生效
        if (logRetentionDays < 3 ) {
            return;
        }
        localThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!toStop) {
                    try {
                        //获取日志路径下面的所有文件
                        File[] childDirs = new File(XxlJobFileAppender.getLogPath()).listFiles();
                        if (childDirs!=null && childDirs.length>0) {

                            //获取到当前日期
                            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) {

                                /**
                                如果不是 文件夹, 文件夹名字中没有'-' 都跳过
                                这里都是用日期作为文件夹
                                **/
                                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 {
                        // sleep 1天
                        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();
    }

2.3.3 callback 线程

TriggerCallbackThread 线程主要逻辑也比较清晰,主要流程如下:

  1. 启动了一个 触发回调的线程, 通过 take()+ drainTo() 方法 避免高CPU ,每次把需要回到的方法进行回调
  2. 如果出现停止的情况之后,最后一把把剩余的推送过去
  3. 这里会再起一个 重试机制的线程,用于处理在回调过程中失败的数据.
public void start() {

        // 如果没有配置 admin server 的地址,那就跳过
        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() {

                // 正常回调
                while(!toStop){
                    try {
                        /**移除并返回队列的头部, take() 方法是会阻塞的
                        这里采用了 take()+ drainTo() 方法,既保留了drainTo批量处理数据的高效, 又让其拥有了阻塞效果, 没有数据        时方法不会空循环. 避免CPU占用比较高
                        **/
                        HandleCallbackParam callback = getInstance().callBackQueue.take();
                        if (callback != null) {

                            // callback list param
                            List<HandleCallbackParam> callbackParamList = new ArrayList<HandleCallbackParam>();
                            //调用队列的drainTo 方法,将一次获取所有的数据,drainTo 是不会阻塞的
                            int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList);
                            // 加上头部
                            callbackParamList.add(callback);

                            // 进行回调, 如果失败会进行重试
                            if (callbackParamList!=null && callbackParamList.size()>0) {
                                doCallback(callbackParamList);
                            }
                        }
                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                }

                 /** 如果程序运行到这里,说明跳出了上面的while 循环, 那就是toStop 为true 了
                  此时队列中可能还有有遗留没有处理,最后处理一次
                  通过drainTo 一把捞出,进行处理
                 **/
                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();


        // 这里是 重试机制线程
        triggerRetryCallbackThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while(!toStop){
                    try {
                        retryFailCallbackFile();
                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(e.getMessage(), e);
                        }

                    }
                    try {
                        // 每隔30秒一次
                        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();

    }
2.3.3.1 doCallback

doCallback 方法主要是以下逻辑:
1.从 配置的 admin server 地址列表中依次获取,调用对应的callback 方法进行 回调.
2. 如果成功,则记录log 并跳槽, 如果失败,那就 用下一个 admin server 地址进行尝试
3. 如果所有的 admin server 都失败,那就 追加到文件,等重试线程来调用

遍历admin server,这里进行回调,但是如果服务端机器比较多,这里所有的负载都会压在第一台机器上面,
个人感觉可以打乱list,如 Collections.shuffle(list); 这样使负载更加均衡, 同时这里 如果 admin server 动态添加机器(没有使用域名),也是无感知的.

    private void doCallback(List<HandleCallbackParam> callbackParamList){
        boolean callbackRet = false;
        // callback, will retry if error
        /** 遍历admin server,这里进行回调,但是如果服务端机器比较多,这里所有的负载都会压在第一台机器上面
        如果失败,会用第二台进行测试,以此类推
        **/
        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);
        }
    }

    private void callbackLog(List<HandleCallbackParam> callbackParamList, String logContent){
        for (HandleCallbackParam callbackParam: callbackParamList) {
            // 创建文件夹, 并返回对应的log文件全路径
            String logFileName = XxlJobFileAppender.makeLogFileName(new Date(callbackParam.getLogDateTim()), callbackParam.getLogId());
            // 存放到threadlocal
            XxlJobFileAppender.contextHolder.set(logFileName);
            //记录log
            XxlJobLogger.log(logContent);
        }
    }

这里简单介绍一下记录的log 的格式

/// “yyyy-MM-dd HH:mm:ss [ClassName]-[MethodName]-[LineNumber]-[ThreadName] log”;
StackTraceElement[] stackTraceElements = new Throwable().getStackTrace();
StackTraceElement callInfo = stackTraceElements[1];
/
这里是使用了new Throwable().getStackTrace() [1] 通过 获取栈信息,把调用的 类-方法-行号-log 打印出来

一般获取到堆栈轨迹的两种方法
Thread.currentThread().getStackTrace()
new Throwable().getStackTrace()

2.3.3.2 appendFailCallbackFile

appendFailCallbackFile 方法就是将失败之后的数据存到文件里面,便于下次重试

    private void appendFailCallbackFile(List<HandleCallbackParam> callbackParamList){
        // valid
        if (callbackParamList==null || callbackParamList.size()==0) {
            return;
        }

        // 这里是将 Object-->byte[]
        byte[] callbackParamList_bytes = JdkSerializeTool.serialize(callbackParamList);

        // 这里是 存放在 日志路径下面的 /callbacklog/xxl-job-callback-{x}.log
        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);
    }
2.3.3.3 重试triggerRetryCallbackThread 线程

重试机制也比较简单,就是 读取 对应目录下面的数据,转换为对应的类型,先将原先文件删除,
然后进行再次调用doCallback 方法

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

    }
2.3.3.4 AdminBizClient#callback

在上面doCallback 方法里面 ,通过调用 adminBiz.callback(callbackParamList);进行回调,这里就是通过HttpURLConnection 进行调用,调用 admin server 的API 接口.

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

2.3.4 initEmbedServer

initEmbedServer 方法主要逻辑是 ,获取IP 和port , 调用EmbedServer.start

    private void initEmbedServer(String address, String ip, int port, String appname, String accessToken) throws Exception {

        /**填充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);
    }
2.3.4.1 NetUtil

findAvailablePort 方法主要逻辑就是 获取一个可以使用的端口.
先对Port 进行递增校验, 再递减校验,直到获取可用的端口

    public static int findAvailablePort(int defaultPort) {
        int portTmp = defaultPort;
        while (portTmp < 65535) {
            if (!isPortUsed(portTmp)) {
                return portTmp;
            } else {
                portTmp++;
            }
        }
        portTmp = defaultPort--;
        while (portTmp > 0) {
            if (!isPortUsed(portTmp)) {
                return portTmp;
            } else {
                portTmp--;
            }
        }
        throw new RuntimeException("no available port.");
    }

2.3.4.2 EmbedServer
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() {

                // 创建两个线程池组,分别处理客户端的连接和 客户端的读写
                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
                    // 创建netty引导类,配置和串联系列组件(设置线程模型,设置通道类型,设置客户端处理器handler,设置绑定端口号)
                    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
                                            // 解码成HttpRequest
                                            .addLast(new HttpServerCodec())
                                            // 解码成FullHttpRequest
                                            .addLast(new HttpObjectAggregator(5 * 1024 * 1024))  // merge request & reponse to FULL
                                            // 添加处自定义的处理器,这里还是http 协议
                                            .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);

                    // 进行注册
                    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();
    }
2.3.4.3 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() {

                // 进行注册
                while (!toStop) {
                    try {
                        RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
                        for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
                            try {
                               // 调用AdminBizClient 的registry 进行注册
                                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);
                        }

                    }
                    // 每隔30s 进行注册一次
                    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());
                        }
                    }
                }

                // 代码到这里时,说明 toStop =true, 进行移除注册
                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();
    }

2.4 EmbedHttpServerHandler

EmbedHttpServerHandler 是继承了SimpleChannelInboundHandler 的自定义处理器,我们看一下channelRead0 里面的逻辑

2.4.1 channelRead0

这里主要如下逻辑,

  1. 获取 请求的内容,方法类型(post/get),url 等信息,
  2. 异步线程调用触发
  3. 转json
  4. 返回response.
        protected void channelRead0(final ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {

            //final byte[] requestBytes = ByteBufUtil.getBytes(msg.content());    
            // byteBuf.toString(io.netty.util.CharsetUtil.UTF_8);
            // 解析请求,获取 内容、url、获取请求方法类型(post/get)
            String requestData = msg.content().toString(CharsetUtil.UTF_8);
            String uri = msg.uri();
            HttpMethod httpMethod = msg.method();
            boolean keepAlive = HttpUtil.isKeepAlive(msg);
            String accessTokenReq = msg.headers().get(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN);

            // invoke
            bizThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    // do invoke
                    Object responseObj = process(httpMethod, uri, requestData, accessTokenReq);

                    // to json
                    String responseJson = GsonTool.toJson(responseObj);

                    // write response
                    writeResponse(ctx, keepAlive, responseJson);
                }
            });
        }

2.4.2 EmbedHttpServerHandler#process

process 这里主要做了一些校验

private Object process(HttpMethod httpMethod, String uri, String requestData, String accessTokenReq) {

            /**
            Check,以下类型直接返回错误信息
            1. 如果method 类型不是POST
            2. 如果请求方法为null
            3. accessToken 和给定的不一样(如果存在)
            **/
            if (HttpMethod.POST != httpMethod) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support.");
            }
            if (uri==null || uri.trim().length()==0) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty.");
            }
            if (accessToken!=null
                    && accessToken.trim().length()>0
                    && !accessToken.equals(accessTokenReq)) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, "The access token is wrong.");
            }

            // 具体的方法,调用对应的services
            try {
                if ("/beat".equals(uri)) {
                    return executorBiz.beat();
                } else if ("/idleBeat".equals(uri)) {
                    IdleBeatParam idleBeatParam = GsonTool.fromJson(requestData, IdleBeatParam.class);
                    return executorBiz.idleBeat(idleBeatParam);
                } else if ("/run".equals(uri)) {
                    TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class);
                    return executorBiz.run(triggerParam);
                } else if ("/kill".equals(uri)) {
                    KillParam killParam = GsonTool.fromJson(requestData, KillParam.class);
                    return executorBiz.kill(killParam);
                } else if ("/log".equals(uri)) {
                    LogParam logParam = GsonTool.fromJson(requestData, LogParam.class);
                    return executorBiz.log(logParam);
                } else {
                    return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found.");
                }
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
                return new ReturnT<String>(ReturnT.FAIL_CODE, "request error:" + ThrowableUtil.toString(e));
            }
        }

2.4.3 ExecutorBizImpl

2.4.3.1 ExecutorBizImpl#run

这里主要做了以下几件事:

  1. 判断是否存在老的jobThread,存在说明这个job 之前已经调用过了,如果存在,获取 jobHandler
  2. 根据不同的运行模式,走不同的分支,获取最新的jobHandler
  3. 如果当前job 在运行,根据运行的策略(Cover Early / Discard Later / Serial execution ), 就行调整
  4. 如果第一次运行此job ,直接运行,如果不是加入队列
public ReturnT<String> run(TriggerParam triggerParam) {
        // load old:jobHandler + jobThread
        /**
        根据jobId,加载老的jobThread,如果存在,那就获取对应的Handler
        **/
        JobThread jobThread = XxlJobExecutor.loadJobThread(triggerParam.getJobId());
        IJobHandler jobHandler = jobThread!=null?jobThread.getHandler():null;
        String removeOldReason = null;

        /** 根据不同的运行模式执行对应的方法
        BEAN, GLUE(Java),GLUE(Shell),GLUE(Python),GLUE(PHP),GLUE(Nodejs),GLUE(PowerShell)
        **/
        GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerParam.getGlueType());
        /**
        如果是BEAN 模式,获取对应的jobhandler ,和 jobThread 里面的jobhandler 对比,是否是同一个
        **/
        if (GlueTypeEnum.BEAN == glueTypeEnum) {

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

            /** 校验老的jobThread ,这里如果jobThread存在,并且老的jobHandler  和新的不一样,说明发生了变化
            将老的jobThread  和 jobHandler  置为空
            **/
            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;
            }

            // 如果老的jobHandler ,赋值为新的,如果都没有找到,则提示错误信息,FAIL_CODE设置500
            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) {

            // 校验老的jobThread ,需要JobHandler 为GlueJobHandler类型,并且glueUpdatetime 和传入的要一致
            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) {
                // 如果是运行时丢弃,直接返回
                if (jobThread.isRunningOrHasQueue()) {
                    return new ReturnT<String>(ReturnT.FAIL_CODE, "block strategy effect:"+ExecutorBlockStrategyEnum.DISCARD_LATER.getTitle());
                }
            } else if (ExecutorBlockStrategyEnum.COVER_EARLY == blockStrategy) {
                // 如果是覆盖之前的类型,将之前的置空
                if (jobThread.isRunningOrHasQueue()) {
                    removeOldReason = "block strategy effect:" + ExecutorBlockStrategyEnum.COVER_EARLY.getTitle();

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

        //这里是替换旧的线程,或者新建一个新的
        if (jobThread == null) {
            jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason);
        }

        // 将数据放入队列
        ReturnT<String> pushResult = jobThread.pushTriggerQueue(triggerParam);
        return pushResult;
    }
    public static JobThread registJobThread(int jobId, IJobHandler handler, String removeOldReason){
        // 新起一个jobThread,并启动
        JobThread newJobThread = new JobThread(jobId, handler);
        newJobThread.start();
        logger.info(">>>>>>>>>>> xxl-job regist JobThread success, jobId:{}, handler:{}", new Object[]{jobId, handler});
         // 返回老的jobThread ,中断
        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;
    }

2.5 JobThread

这里就是具体执行的逻辑,主要看一下 run() 方法里面的东西,主要逻辑也比较简单,流程如下:

  1. 先运行 init 方法
  2. 运行具体的job 方法
  3. 运行 destory() 方法,如果存在
public void run() {

    	// 如果存在init 方法,调用 init()
    	try {
			handler.init();
		} catch (Throwable e) {
    		logger.error(e.getMessage(), e);
		}

		//执行具体的逻辑
		while(!toStop){
			running = false;
			idleTimes++;

            TriggerParam triggerParam = null;
            ReturnT<String> executeResult = null;
            try {
				/**
				这里使用的是 poll(timeout),主要是为了考虑检查toStop 标志是否发生变成,所以需要循环,所以这里不能使用queue.take()
				**/
				triggerParam = triggerQueue.poll(3L, TimeUnit.SECONDS);
				if (triggerParam!=null) {
					running = true;
					idleTimes = 0;
					triggerLogIdSet.remove(triggerParam.getLogId());

					// 记录文件名称,like "logPath/yyyy-MM-dd/9999.log"
					String logFileName = XxlJobFileAppender.makeLogFileName(new Date(triggerParam.getLogDateTime()), triggerParam.getLogId());
					XxlJobFileAppender.contextHolder.set(logFileName);
					ShardingUtil.setShardingVo(new ShardingUtil.ShardingVO(triggerParam.getBroadcastIndex(), triggerParam.getBroadcastTotal()));

					// execute
					XxlJobLogger.log("<br>----------- xxl-job job execute start -----------<br>----------- Param:" + triggerParam.getExecutorParams());

					if (triggerParam.getExecutorTimeout() > 0) {
						// limit timeout
						Thread futureThread = null;
						try {
							final TriggerParam triggerParamTmp = triggerParam;
							FutureTask<ReturnT<String>> futureTask = new FutureTask<ReturnT<String>>(new Callable<ReturnT<String>>() {
								@Override
								public ReturnT<String> call() throws Exception {
									return handler.execute(triggerParamTmp.getExecutorParams());
								}
							});
							futureThread = new Thread(futureTask);
							futureThread.start();

							executeResult = futureTask.get(triggerParam.getExecutorTimeout(), TimeUnit.SECONDS);
						} catch (TimeoutException e) {

							XxlJobLogger.log("<br>----------- xxl-job job execute timeout");
							XxlJobLogger.log(e);

							executeResult = new ReturnT<String>(IJobHandler.FAIL_TIMEOUT.getCode(), "job execute timeout ");
						} finally {
							futureThread.interrupt();
						}
					} else {
						// just execute
						executeResult = handler.execute(triggerParam.getExecutorParams());
					}

					if (executeResult == null) {
						executeResult = IJobHandler.FAIL;
					} else {
						executeResult.setMsg(
								(executeResult!=null&&executeResult.getMsg()!=null&&executeResult.getMsg().length()>50000)
										?executeResult.getMsg().substring(0, 50000).concat("...")
										:executeResult.getMsg());
						executeResult.setContent(null);	// limit obj size
					}
					XxlJobLogger.log("<br>----------- xxl-job job execute end(finish) -----------<br>----------- ReturnT:" + executeResult);

				} else {
					if (idleTimes > 30) {
						if(triggerQueue.size() == 0) {	// avoid concurrent trigger causes jobId-lost
							XxlJobExecutor.removeJobThread(jobId, "excutor idel times over limit.");
						}
					}
				}
			} catch (Throwable e) {
				if (toStop) {
					XxlJobLogger.log("<br>----------- JobThread toStop, stopReason:" + stopReason);
				}

				StringWriter stringWriter = new StringWriter();
				e.printStackTrace(new PrintWriter(stringWriter));
				String errorMsg = stringWriter.toString();
				executeResult = new ReturnT<String>(ReturnT.FAIL_CODE, errorMsg);

				XxlJobLogger.log("<br>----------- JobThread Exception:" + errorMsg + "<br>----------- xxl-job job execute end(error) -----------");
			} finally {
                if(triggerParam != null) {
                    // callback handler info
                    if (!toStop) {
                        // commonm
                        TriggerCallbackThread.pushCallBack(new HandleCallbackParam(triggerParam.getLogId(), triggerParam.getLogDateTime(), executeResult));
                    } else {
                        // is killed
                        ReturnT<String> stopResult = new ReturnT<String>(ReturnT.FAIL_CODE, stopReason + " [job running, killed]");
                        TriggerCallbackThread.pushCallBack(new HandleCallbackParam(triggerParam.getLogId(), triggerParam.getLogDateTime(), stopResult));
                    }
                }
            }
        }

		// callback trigger request in queue
		while(triggerQueue !=null && triggerQueue.size()>0){
			TriggerParam triggerParam = triggerQueue.poll();
			if (triggerParam!=null) {
				// is killed
				ReturnT<String> stopResult = new ReturnT<String>(ReturnT.FAIL_CODE, stopReason + " [job not executed, in the job queue, killed.]");
				TriggerCallbackThread.pushCallBack(new HandleCallbackParam(triggerParam.getLogId(), triggerParam.getLogDateTime(), stopResult));
			}
		}

		// destroy
		try {
			handler.destroy();
		} catch (Throwable e) {
			logger.error(e.getMessage(), e);
		}

		logger.info(">>>>>>>>>>> xxl-job JobThread stoped, hashCode:{}", Thread.currentThread());
	}

三、小结

本章主要分析了 xxl-job-core 里面的核心代码,整个流程还是相对比较清晰的.

支付宝微信
支付宝微信
如果有帮助记得打赏哦特别需要您的打赏哦
  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一直打铁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值