xxl-job 2.2.0 魔改自动主动执行器以及任务

315 阅读5分钟

xxl-job 需要手动在控制台注册任务和执行器,操作麻烦。增加操作复杂性。

魔改2.2.0版本 自动注册~

Quick start

1、 首先定义一个注解, 通过注解在client端注入我们的同步器

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(SyncJobSpringExecutor.class)
public @interface EnableSyncJob {

    /**
     * 报警邮箱 全局设置
     */
    String alarmEmail() default "";

    /**
     * 失败重试次数 全局设置
     */
    int executorFailRetryCount() default 0;

    /**
     * 负责人 全局设置
     */
    String author() default "";

}

2、同步器和工具方法

public class SyncJobSpringExecutor implements ApplicationContextAware, SmartInitializingSingleton {

    private static final Logger logger = LoggerFactory.getLogger(SyncJobSpringExecutor.class);

    @Value("${server.servlet.context-path}")
    private String contentPath;

    @Override
    public void afterSingletonsInstantiated() {

        //避免客户端有多个,通过redis做一个分布式的锁避免重复注册
        String redisKey = "xxl-job-sync-info:xxl:y" + contentPath;

        StringRedisTemplate stringRedisTemplate = applicationContext.getBean(StringRedisTemplate.class);

        String syncId = stringRedisTemplate.opsForValue().get(redisKey);

        if (!StringUtils.isEmpty(syncId)) {
            return;
        }

        stringRedisTemplate.opsForValue().set(redisKey,"1",10, TimeUnit.MINUTES);

        logger.info("同步XXL-JOB任务开始");

        Object object = applicationContext
                .getBeansWithAnnotation(EnableSyncJob.class)
                .entrySet()
                .stream()
                .findFirst()
                .orElseThrow(() -> new NullPointerException("Not found EnableSyncJob Class"))
                .getValue();

        EnableSyncJob enableSyncJob = AnnotationUtils.findAnnotation(object.getClass(), EnableSyncJob.class);

        logger.info("获取到注解信息enableSyncJob:{}",JSONUtil.toJsonStr(enableSyncJob));

        String url = applicationContext.getEnvironment().getProperty("xxl.job.admin.addresses");

        if (StringUtils.isEmpty(url)) {
            throw new NullPointerException("Please check the server address ---》 xxl.job.admin.addresses");
        }

        XxlJobServerHttpUtil xxlJobServerHttpUtil = null;

        try {
            xxlJobServerHttpUtil = new XxlJobServerHttpUtil(url);
        } catch (Exception e) {
            e.printStackTrace();
        }

        /**
         * 初始化执行器
         */
        Integer groupId = this.initActuatorData(xxlJobServerHttpUtil);

        /**
         * 同步任务
         */
        this.syncJobInfo(groupId,xxlJobServerHttpUtil,enableSyncJob);

        logger.info("同步XXL-JOB任务结束");

        stringRedisTemplate.delete(redisKey);
    }

    /**
     * 同步任务
     * @param groupId
     * @param xxlJobServerHttpUtil
     * @param enableSyncJob
     */
    private void syncJobInfo(Integer groupId, XxlJobServerHttpUtil xxlJobServerHttpUtil, EnableSyncJob enableSyncJob) {
        List<XxlJobBeanUtil.Data> mapLists = XxlJobBeanUtil.getMapLists(applicationContext);

        for (XxlJobBeanUtil.Data mapList : mapLists) {
            for (Map.Entry<Method, XxlJob> map : mapList.getMethod().entrySet()) {
                XxlJob xxlJob = map.getValue();

                //如果任务不存在则新增
                if (xxlJobServerHttpUtil.getJobInfoIdByHandlerName(xxlJob.value(),groupId) == null) {

                    String alarmEmail = xxlJob.alarmEmail();

                    if (StringUtils.isEmpty(alarmEmail)) {
                        alarmEmail = enableSyncJob.alarmEmail();
                    }

                    int executorFailRetryCount = xxlJob.executorFailRetryCount();

                    if (executorFailRetryCount == -1) {
                        executorFailRetryCount = enableSyncJob.executorFailRetryCount();
                    }

                    String author = xxlJob.author();

                    if (StringUtils.isEmpty(author)) {
                        author = enableSyncJob.author();
                    }

                    String cron = xxlJob.cron();

                    int executorTimeout = xxlJob.executorTimeout();
                    String jobDesc = xxlJob.jobDesc();

                    XxlJobServerHttpUtil.JobInfoData jobInfoData = new XxlJobServerHttpUtil.JobInfoData(
                            groupId, jobDesc, cron, xxlJob.value()
                            , executorTimeout, executorFailRetryCount, author, alarmEmail
                    );

                    if (xxlJobServerHttpUtil.saveJobInfo(
                            jobInfoData
                    )) {
                        logger.info("新增任务成功jobInfoData:{}", JSONUtil.toJsonStr(jobInfoData));
                    }else {
                        logger.info("任务已存在handlerName:{}",xxlJob.value());
                    }
                }

            }
        }
    }

    /**
     * 初始化执行器
     * @param xxlJobServerHttpUtil
     */
    private Integer initActuatorData(XxlJobServerHttpUtil xxlJobServerHttpUtil) {
        XxlJobServerHttpUtil.ActuatorData actuatorData = new XxlJobServerHttpUtil.ActuatorData(applicationContext);

        Integer groupId = xxlJobServerHttpUtil.getGroupIdByAppName(actuatorData.getAppName());

        //判断执行器是否存在
        if (groupId == null) {

            //不存在则新建
            xxlJobServerHttpUtil.saveActuator(actuatorData);

            logger.info("执行器不存在,新建执行器actuatorData:{}",JSONUtil.toJsonStr(actuatorData));

            return xxlJobServerHttpUtil.getGroupIdByAppName(actuatorData.getAppName());
        }

        logger.info("执行器存在返回执行器ID:{}",groupId);

        return groupId;
    }

    private ApplicationContext applicationContext;

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

}

工具类

XxlJobBeanUtil

import com.xxl.job.core.handler.annotation.XxlJob;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.AnnotatedElementUtils;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;


public class XxlJobBeanUtil {

    private static final Logger logger = LoggerFactory.getLogger(XxlJobBeanUtil.class);

    private static List<Data> mapLists;

    public synchronized static List<Data> getMapLists(ApplicationContext applicationContext) {
        if (Objects.isNull(mapLists)) {
            if (Objects.isNull(applicationContext)) {
                throw new NullPointerException("applicationContext is null");
            }

            String[] beanDefinitionNames = applicationContext.getBeanNamesForType(Object.class, false, true);

            mapLists = Arrays.stream(beanDefinitionNames)
                    .map(beanDefinitionName -> {
                        Object bean = applicationContext.getBean(beanDefinitionName);

                        Map<Method, XxlJob> annotatedMethods = null;   // referred to :org.springframework.context.event.EventListenerMethodProcessor.processBean
                        try {
                            annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(),
                                    (MethodIntrospector.MetadataLookup<XxlJob>) method -> 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()) {
                            return null;
                        }

                        return new Data(annotatedMethods,bean);
                    })
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList());
        }
        return mapLists;
    }

    public static class Data{

        private Map<Method, XxlJob> method;

        private Object bean;

        public Data() {
        }

        public Data(Map<Method, XxlJob> method, Object bean) {
            this.method = method;
            this.bean = bean;
        }

        public Map<Method, XxlJob> getMethod() {
            return method;
        }

        public void setMethod(Map<Method, XxlJob> method) {
            this.method = method;
        }

        public Object getBean() {
            return bean;
        }

        public void setBean(Object bean) {
            this.bean = bean;
        }
    }
}

XxlJobServerHttpUtil

public class XxlJobServerHttpUtil {

    private static final Logger logger = LoggerFactory.getLogger(XxlJobServerHttpUtil.class);

    /**
     * 获取执行器列表地址
     */
    private final String GET_JOB_GROUP_URL = "/jobgroup/pageList";

    /**
     * 保存执行器地址
     */
    private final String SAVE_ACTUATOR_URL = "/jobgroup/save";

    /**
     * 登陆地址
     */
    private final String LOGIN_URL = "/login";

    /**
     * 获取任务列表
     */
    private final String GET_JOB_INFO_URL = "/jobinfo/pageList";

    /**
     * 编辑任务
     * TODO 因一个handler可以存在多个时间点执行不好判断是否要编辑如果发现已存在则不管了
     */
    private final String UPDATE_JOB_INFO_URL = "/jobinfo/update";

    /**
     * 保存任务
     */
    private final String SAVE_JOB_INFO_URL = "/jobinfo/add";

    /**
     * 获取服务信息url
     */
    private final String SERVER_INFO_URL = "/server-info/getServerInfo";

    /**
     * 服务器地址记得带上项目名~
     */
    private String url;

    /**
     * 登陆态
     */
    private HttpCookie[] httpCookies;

    public XxlJobServerHttpUtil(String url) throws Exception {
        this.url = url;

        ServerInfoVO serverInfoVO = this.getServerInfo(url);

        //初始化登陆态
        HttpResponse execute = HttpRequest
                .post(url + LOGIN_URL)
                .form(new HashMap<String, Object>() {{
                    this.put("userName", serverInfoVO.getUserName());
                    this.put("password", serverInfoVO.getPassWord());
                }})
                .execute();

        if ("500".equals(new JSONObject(execute.body()).getStr("code"))) {
            throw new Exception("Xxl-Job登陆用户名或密码错误");
        }

        List<HttpCookie> cookies = execute.getCookies();

        httpCookies = new HttpCookie[cookies.size()];

        cookies.toArray(httpCookies);
    }

    /**
     * 获取服务信息,主要是登陆xxl-job用户名密码
     * 主要是用于获取登陆server的用户名和密码
     * @param url 定时任务服务端url
     * @return 服务信息
     */
    private ServerInfoVO getServerInfo(String url) {
        String body = HttpUtil.createGet(url + SERVER_INFO_URL).execute().body();

        return JSONUtil.parseObj(body).get("data", ServerInfoVO.class);
    }

    /**
     * 获取执行器id
     * @return
     */
    public Integer getGroupIdByAppName(String appName){

        String body = this.getRequest()
                .method(Method.POST)
                .setUrl(url + GET_JOB_GROUP_URL)
                .form(new HashMap<String, Object>() {{
                    this.put("start", "0");
                    this.put("length", "10");
                    this.put("appName", appName);
                }})
                .execute()
                .body();

        JSONObject jsonObject = new JSONObject(body);

        JSONArray data = jsonObject.getJSONArray("data");

        for (int i = 0; i < data.size(); i++) {
            JSONObject dataJSONObject = data.getJSONObject(i);

            if (appName.equals(dataJSONObject.getStr("appName"))) {
                return dataJSONObject.getInt("id");
            }
        }

        return null;
    }

    /**
     * 保存执行器
     */
    public boolean saveActuator(ActuatorData actuatorData){
        String body = this.getRequest()
                .method(Method.POST)
                .setUrl(url + SAVE_ACTUATOR_URL)
                .form(new HashMap<String, Object>() {{
                    this.put("addressList", actuatorData.getAddress());
                    this.put("addressType", "1");
                    this.put("title", actuatorData.getName());
                    this.put("appName", actuatorData.getAppName());
                }})
                .execute()
                .body();

        JSONObject jsonObject = new JSONObject(body);

        boolean code = "200".equals(jsonObject.getStr("code"));

        if (!code) {
            logger.info("保存执行器失败body:{}",body);
        }

        return code;
    }

    /**
     * 获取任务Id
     */
    public Integer getJobInfoIdByHandlerName(String executorHandler,Integer jobGroupId){
        String body = this.getRequest()
                .method(Method.POST)
                .setUrl(url + GET_JOB_INFO_URL)
                .form(new HashMap<String, Object>() {{
                    this.put("executorHandler", executorHandler);
                    this.put("jobGroup", jobGroupId);
                    this.put("start", "0");
                    this.put("length", "10");
                    this.put("triggerStatus", "-1");
                }})
                .execute()
                .body();

        JSONObject jsonObject = new JSONObject(body);


        JSONArray data = jsonObject.getJSONArray("data");

        for (int i = 0; i < data.size(); i++) {
            JSONObject dataJSONObject = data.getJSONObject(i);

            if (executorHandler.equals(dataJSONObject.getStr("executorHandler"))) {
                return dataJSONObject.getInt("id");
            }
        }

        return null;
    }

    /**
     * 保存任务
     */
    public boolean saveJobInfo(JobInfoData jobInfoData){

        if (
                StringUtils.isEmpty(jobInfoData.getJobCron())
                        || StringUtils.isEmpty(jobInfoData.getExecutorHandler())
                        || Objects.isNull(jobInfoData.getJobGroup())
                        || StringUtils.isEmpty(jobInfoData.getAuthor())
                        || StringUtils.isEmpty(jobInfoData.getJobDesc())
        ){
            throw new NullPointerException("Please ensure that the parameters are correct");
        }

        String body = this.getRequest()
                .method(Method.POST)
                .setUrl(url + SAVE_JOB_INFO_URL)
                .form(
                        BeanUtil.beanToMap(jobInfoData)
                )
                .execute()
                .body();

        JSONObject jsonObject = new JSONObject(body);

        boolean code = "200".equals(jsonObject.getStr("code"));

        if (!code){
            logger.info("保存任务失败body:{}",body);
        }

        return code;
    }

    private HttpRequest getRequest(){
        HttpRequest request = new HttpRequest(url);

        request.cookie(httpCookies);
        request.header("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");

        return request;
    }

    /**
     * 执行器信息
     */
    public static class ActuatorData{

        private String appName;

        private String name;

        private String address;

        public ActuatorData() {
        }

        public ActuatorData(ApplicationContext applicationContext) {
            this.appName = applicationContext.getEnvironment().getProperty("xxl.job.executor.appname");
            this.name = appName;
            this.address = applicationContext.getEnvironment().getProperty("xxl.job.executor.address");

            if (StringUtils.isEmpty(address)){
                throw new NullPointerException("Please specify the actuator address ---》 xxl.job.executor.address");
            }
        }

        public String getAppName() {
            return appName;
        }

        public void setAppName(String appName) {
            this.appName = appName;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getAddress() {
            return address;
        }

        public void setAddress(String address) {
            this.address = address;
        }
    }

    /**
     * 任务信息
     */
    public static class JobInfoData{
        private Integer jobGroup;
        private String jobDesc;
        private String executorRouteStrategy;
        private String cronGen_display;
        private String jobCron;
        private String glueType;
        private String executorHandler;
        private String executorBlockStrategy;
        private Integer executorTimeout;
        private Integer executorFailRetryCount;
        private String author;
        private String alarmEmail;
        private String glueRemark;

        public JobInfoData() {
        }

        public JobInfoData(Integer jobGroup, String jobDesc, String jobCron, String executorHandler, Integer executorTimeout, Integer executorFailRetryCount, String author, String alarmEmail) {
            this.jobGroup = jobGroup;
            this.jobDesc = jobDesc;
            this.executorRouteStrategy = "FIRST";
            this.cronGen_display = jobCron;
            this.jobCron = jobCron;
            this.glueType = "BEAN";
            this.executorHandler = executorHandler;
            this.executorBlockStrategy = "SERIAL_EXECUTION";
            this.executorTimeout = executorTimeout;
            this.executorFailRetryCount = executorFailRetryCount;
            this.author = author;
            this.alarmEmail = alarmEmail;
            this.glueRemark = "GLUE代码初始化";
        }

        public Integer getJobGroup() {
            return jobGroup;
        }

        public void setJobGroup(Integer jobGroup) {
            this.jobGroup = jobGroup;
        }

        public String getJobDesc() {
            return jobDesc;
        }

        public void setJobDesc(String jobDesc) {
            this.jobDesc = jobDesc;
        }

        public String getExecutorRouteStrategy() {
            return executorRouteStrategy;
        }

        public void setExecutorRouteStrategy(String executorRouteStrategy) {
            this.executorRouteStrategy = executorRouteStrategy;
        }

        public String getCronGen_display() {
            return cronGen_display;
        }

        public void setCronGen_display(String cronGen_display) {
            this.cronGen_display = cronGen_display;
        }

        public String getJobCron() {
            return jobCron;
        }

        public void setJobCron(String jobCron) {
            this.jobCron = jobCron;
        }

        public String getGlueType() {
            return glueType;
        }

        public void setGlueType(String glueType) {
            this.glueType = glueType;
        }

        public String getExecutorHandler() {
            return executorHandler;
        }

        public void setExecutorHandler(String executorHandler) {
            this.executorHandler = executorHandler;
        }

        public String getExecutorBlockStrategy() {
            return executorBlockStrategy;
        }

        public void setExecutorBlockStrategy(String executorBlockStrategy) {
            this.executorBlockStrategy = executorBlockStrategy;
        }

        public Integer getExecutorTimeout() {
            return executorTimeout;
        }

        public void setExecutorTimeout(Integer executorTimeout) {
            this.executorTimeout = executorTimeout;
        }

        public Integer getExecutorFailRetryCount() {
            return executorFailRetryCount;
        }

        public void setExecutorFailRetryCount(Integer executorFailRetryCount) {
            this.executorFailRetryCount = executorFailRetryCount;
        }

        public String getAuthor() {
            return author;
        }

        public void setAuthor(String author) {
            this.author = author;
        }

        public String getAlarmEmail() {
            return alarmEmail;
        }

        public void setAlarmEmail(String alarmEmail) {
            this.alarmEmail = alarmEmail;
        }

        public String getGlueRemark() {
            return glueRemark;
        }

        public void setGlueRemark(String glueRemark) {
            this.glueRemark = glueRemark;
        }
    }
}

3、使用

在我们的spring boot项目中直接注入注解。启动项目就好啦

image

然后就可以愉快的玩xxl-job了 不用在手动创建任务了,真好~