美文网首页
xxl-job之API的方式接入

xxl-job之API的方式接入

作者: 上善若泪 | 来源:发表于2024-01-25 21:06 被阅读0次

    1 xxl-job

    1.1 简介

    xxl-job是一款非常优秀的任务调度中间件,轻量级、使用简单、支持分布式等优点,让它广泛应用在我们的项目中,解决了不少定时任务的调度问题。

    我们都知道,在使用过程中需要先到xxl-job的任务调度中心页面上,配置执行器executor和具体的任务job,这一过程如果项目中的定时任务数量不多还好说,如果任务多了的话还是挺费工夫的。

    42b6003ed637b0ec084f51db58707dc5_e5d38c170e674b83abea9a10dbbee420.png

    1.2 分析

    假如在项目启动时主动注册executor和各个jobHandler到调度中心就可以了,流程如下:

    284672b25b29ceaa3ec91b831b5ffdf2_4934c8009239405cb41d79e308ed3124.jpeg

    有的小伙伴们可能要问了,我在页面上创建执行器的时候,不是有一个选项叫做自动注册吗,为什么我们这里还要自己添加新执行器?

    其实这里有个误区,这里的自动注册指的是会根据项目中配置的xxl.job.executor.appname,将配置的机器地址自动注册到这个执行器的地址列表中。但是如果你之前没有手动创建过执行器,那么是不会给你自动添加一个新执行器到调度中心的。

    1.3 学习xxl-job源码

    xxl-job github 地址:https://github.com/xuxueli/xxl-job
    整个项目导入idea后,先看一下结构:

    83131142e162592ac580879748db57eb_3da458b105e94eeaa6b5c391e94feeb2.png

    结合着文档和代码,先梳理一下各个模块都是干什么的:

    • xxl-job-admin:任务调度中心,启动后就可以访问管理页面,进行执行器和任务的注册、以及任务调用等功能了
    • xxl-job-core:公共依赖,项目中使用到xxl-job时要引入的依赖包
    • xxl-job-executor-samples:执行示例,分别包含了springboot版本和不使用框架的版本

    为了弄清楚注册和查询executor和jobHandler调用的是哪些接口,我们先从页面上去抓一个请求看看:


    490ead235a0bc81576abaf64d0dac6ef_5ff6b15a7c3a431ea7b34889c5d9c788.png

    好了,这样就能定位到xxl-job-admin模块中/jobgroup/save这个接口,接下来可以很容易地找到源码位置:

    4fdcdd2293a3ba7ea55561953fe5d631_636b23055a4d4f63a5cc97e336072dd5.png

    按照这个思路,可以找到下面这几个关键接口:

    • /jobgroup/pageList:执行器列表的条件查询
    • /jobgroup/save:添加执行器
    • /jobinfo/pageList:任务列表的条件查询
    • /jobinfo/add:添加任务

    但是如果直接调用这些接口,那么就会发现它会跳转到xxl-job-admin的的登录页面:

    ec292c0a42cda36714e87754b840fe54_97edbf92387b4243802479470129b94a.png

    其实想想也明白,出于安全性考虑,调度中心的接口也不可能允许裸调的。那么再回头看一下刚才页面上的请求就会发现,它在Headers中添加了一条名为XXL_JOB_LOGIN_IDENTITY的cookie:

    58128f3c4f4b1ddd2bcbc754be2987cc_ccde4f2693b44a749c86cfabd65de5ea.png

    至于这条cookie,则是在通过用户名和密码调用调度中心的/login接口时返回的,在返回的response可以直接拿到。只要保存下来,并在之后每次请求时携带,就能够正常访问其他接口了。

    到这里,我们需要的5个接口就基本准备齐了,接下来准备开始正式的改造工作。

    1.4 改造项目

    我们改造的目的是实现一个starter,以后只要引入这个starter就能实现executorjobHandler的自动注册,要引入的关键依赖有下面两个:

    <dependency>
        <groupId>com.xuxueli</groupId>
        <artifactId>xxl-job-core</artifactId>
        <version>2.3.0</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    

    1.4.1 接口调用

    在调用调度中心的接口前,先把xxl-job-admin模块中的XxlJobInfo和XxlJobGroup这两个类拿到我们的starter项目中,用于接收接口调用的结果。

    1.4.1.1 对接登录接口

    创建一个JobLoginService,在调用业务接口前,需要通过登录接口获取cookie,并在获取到cookie后,缓存到本地的Map中。
    如果不想调用登录接口可以添加注解 @PermissionLimit(limit = false),可以跳过验证限制

    private final Map<String,String> loginCookie=new HashMap<>();
    
    public void login() {
        String url=adminAddresses+"/login";
        HttpResponse response = HttpRequest.post(url)
                .form("userName",username)
                .form("password",password)
                .execute();
        List<HttpCookie> cookies = response.getCookies();
        Optional<HttpCookie> cookieOpt = cookies.stream()
                .filter(cookie -> cookie.getName().equals("XXL_JOB_LOGIN_IDENTITY")).findFirst();
        if (!cookieOpt.isPresent())
            throw new RuntimeException("get xxl-job cookie error!");
    
        String value = cookieOpt.get().getValue();
        loginCookie.put("XXL_JOB_LOGIN_IDENTITY",value);
    }
    

    其他接口在调用时,直接从缓存中获取cookie,如果缓存中不存在则调用/login接口,为了避免这一过程失败,允许最多重试3次。

    public String getCookie() {
        for (int i = 0; i < 3; i++) {
            String cookieStr = loginCookie.get("XXL_JOB_LOGIN_IDENTITY");
            if (cookieStr !=null) {
                return "XXL_JOB_LOGIN_IDENTITY="+cookieStr;
            }
            login();
        }
        throw new RuntimeException("get xxl-job cookie error!");
    }
    

    1.4.1.2 对接执行器接口

    创建一个JobGroupService,根据appName和执行器名称title查询执行器列表:

    public List<XxlJobGroup> getJobGroup() {
        String url=adminAddresses+"/jobgroup/pageList";
        HttpResponse response = HttpRequest.post(url)
                .form("appname", appName)
                .form("title", title)
                .cookie(jobLoginService.getCookie())
                .execute();
    
        String body = response.body();
        JSONArray array = JSONUtil.parse(body).getByPath("data", JSONArray.class);
        List<XxlJobGroup> list = array.stream()
                .map(o -> JSONUtil.toBean((JSONObject) o, XxlJobGroup.class))
                .collect(Collectors.toList());
        return list;
    }
    

    我们在后面要根据配置文件中的appNametitle判断当前执行器是否已经被注册到调度中心过,如果已经注册过那么则跳过,而/jobgroup/pageList接口是一个模糊查询接口,所以在查询列表的结果列表中,还需要再进行一次精确匹配。

    public boolean preciselyCheck() {
        List<XxlJobGroup> jobGroup = getJobGroup();
        Optional<XxlJobGroup> has = jobGroup.stream()
                .filter(xxlJobGroup -> xxlJobGroup.getAppname().equals(appName)
                        && xxlJobGroup.getTitle().equals(title))
                .findAny();
        return has.isPresent();
    }
    

    注册新executor到调度中心:

    public boolean autoRegisterGroup() {
        String url=adminAddresses+"/jobgroup/save";
        HttpResponse response = HttpRequest.post(url)
                .form("appname", appName)
                .form("title", title)
                .cookie(jobLoginService.getCookie())
                .execute();
        Object code = JSONUtil.parse(response.body()).getByPath("code");
        return code.equals(200);
    }
    

    1.4.1.3 对接任务接口

    创建一个JobInfoService,根据执行器idjobHandler名称查询任务列表,和上面一样,也是模糊查询:

    public List<XxlJobInfo> getJobInfo(Integer jobGroupId,String executorHandler) {
        String url=adminAddresses+"/jobinfo/pageList";
        HttpResponse response = HttpRequest.post(url)
                .form("jobGroup", jobGroupId)
                .form("executorHandler", executorHandler)
                .form("triggerStatus", -1)
                .cookie(jobLoginService.getCookie())
                .execute();
    
        String body = response.body();
        JSONArray array = JSONUtil.parse(body).getByPath("data", JSONArray.class);
        List<XxlJobInfo> list = array.stream()
                .map(o -> JSONUtil.toBean((JSONObject) o, XxlJobInfo.class))
                .collect(Collectors.toList());
    
        return list;
    }
    

    注册一个新任务,最终返回创建的新任务的id:

    public Integer addJobInfo(XxlJobInfo xxlJobInfo) {
        String url=adminAddresses+"/jobinfo/add";
        Map<String, Object> paramMap = BeanUtil.beanToMap(xxlJobInfo);
        HttpResponse response = HttpRequest.post(url)
                .form(paramMap)
                .cookie(jobLoginService.getCookie())
                .execute();
    
        JSON json = JSONUtil.parse(response.body());
        Object code = json.getByPath("code");
        if (code.equals(200)){
            return Convert.toInt(json.getByPath("content"));
        }
        throw new RuntimeException("add jobInfo error!");
    }
    

    1.4.2 创建新注解

    在创建任务时,必填字段除了执行器jobHandler之外,还有任务描述、负责人、Cron表达式、调度类型、运行模式。在这里,我们默认调度类型为CRON、运行模式为BEAN,另外的3个字段的信息需要用户指定。

    因此我们需要创建一个新注解@XxlRegister,来配合原生的@XxlJob注解进行使用,填写这几个字段的信息:

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface XxlRegister {
        String cron();
        String jobDesc() default "default jobDesc";
        String author() default "default Author";
        int triggerStatus() default 0;
    }
    

    最后,额外添加了一个triggerStatus属性,表示任务的默认调度状态,0为停止状态,1为运行状态。

    1.4.3 自动注册核心

    基本准备工作做完后,下面实现自动注册执行器jobHandler的核心代码。核心类实现ApplicationListener接口,在接收到ApplicationReadyEvent事件后开始执行自动注册逻辑。

    @Component
    public class XxlJobAutoRegister implements ApplicationListener<ApplicationReadyEvent>, 
            ApplicationContextAware {
        private static final Log log =LogFactory.get();
        private ApplicationContext applicationContext;
        @Autowired
        private JobGroupService jobGroupService;
        @Autowired
        private JobInfoService jobInfoService;
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext=applicationContext;
        }
    
        @Override
        public void onApplicationEvent(ApplicationReadyEvent event) {
            addJobGroup();//注册执行器
            addJobInfo();//注册任务
        }
    }
    

    自动注册执行器的代码非常简单,根据配置文件中的appName和title精确匹配查看调度中心是否已有执行器被注册过了,如果存在则跳过,不存在则新注册一个:

    private void addJobGroup() {
        if (jobGroupService.preciselyCheck())
            return;
    
        if(jobGroupService.autoRegisterGroup())
            log.info("auto register xxl-job group success!");
    }
    

    自动注册任务的逻辑则相对复杂一些,需要完成:

    • 通过applicationContext拿到spring容器中的所有bean,再拿到这些bean中所有添加了@XxlJob注解的方法
    • 对上面获取到的方法进行检查,是否添加了我们自定义的@XxlRegister注解,如果没有则跳过,不进行自动注册
    • 对同时添加了@XxlJob@XxlRegister的方法,通过执行器id和jobHandler的值判断是否已经在调度中心注册过了,如果已存在则跳过
    • 对于满足注解条件且没有注册过的jobHandler,调用接口注册到调度中心

    具体代码如下:

    private void addJobInfo() {
        List<XxlJobGroup> jobGroups = jobGroupService.getJobGroup();
        XxlJobGroup xxlJobGroup = jobGroups.get(0);
    
        String[] beanDefinitionNames = applicationContext.getBeanNamesForType(Object.class, false, true);
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = applicationContext.getBean(beanDefinitionName);
    
            Map<Method, XxlJob> annotatedMethods  = MethodIntrospector.selectMethods(bean.getClass(),
                    new MethodIntrospector.MetadataLookup<XxlJob>() {
                        @Override
                        public XxlJob inspect(Method method) {
                            return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
                        }
                    });
            for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
                Method executeMethod = methodXxlJobEntry.getKey();
                XxlJob xxlJob = methodXxlJobEntry.getValue();
    
                //自动注册
                if (executeMethod.isAnnotationPresent(XxlRegister.class)) {
                    XxlRegister xxlRegister = executeMethod.getAnnotation(XxlRegister.class);
                    List<XxlJobInfo> jobInfo = jobInfoService.getJobInfo(xxlJobGroup.getId(), xxlJob.value());
                    if (!jobInfo.isEmpty()){
                        //因为是模糊查询,需要再判断一次
                        Optional<XxlJobInfo> first = jobInfo.stream()
                                .filter(xxlJobInfo -> xxlJobInfo.getExecutorHandler().equals(xxlJob.value()))
                                .findFirst();
                        if (first.isPresent())
                            continue;
                    }
    
                    XxlJobInfo xxlJobInfo = createXxlJobInfo(xxlJobGroup, xxlJob, xxlRegister);
                    Integer jobInfoId = jobInfoService.addJobInfo(xxlJobInfo);
                }
            }
        }
    }
    

    1.4.4 自动装配

    创建一个配置类,用于扫描bean:

    @Configuration
    @ComponentScan(basePackages = "com.xxl.job.plus.executor")
    public class XxlJobPlusConfig {
    }
    

    将它添加到 META-INF/spring.factories 文件:

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
      com.xxl.job.plus.executor.config.XxlJobPlusConfig
    

    1.4.5 获取格式化时间工具

    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.Objects;
    
    public class CronUtil {
        /**
         * 年 (可选) 留空
         * 允许的特殊字符:留空, 1970-2099 , - * /
         */
        private String year;
        /**
         * 星期 可以用数字1-7表示(1 = 星期日)或用字符口串“SUN, MON, TUE, WED, THU, FRI and SAT”表示
         * 允许的特殊字符:1-7 或者 SUN-SAT , - * ? / L C #
         */
        private String week;
        /**
         * 月  可以用0-11 或用字符串  “JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and DEC” 表示
         * 允许的特殊字符:1-12 或者 JAN-DEC , - * /
         */
        private String month;
        /**
         * 日 可以用数字1-31 中的任一一个值,但要注意一些特别的月份
         * 允许的特殊字符:1-31 , - * ? / L W C
         */
        private String day;
        /**
         * 时 可以用数字0-23表示
         * 允许的特殊字符:0-23, - * /
         */
        private String hour;
        /**
         * 分 可以用数字0-59 表示
         * 允许的特殊字符:0-59,- * /
         */
        private String minutes;
        /**
         * 秒 可以用数字0-59 表示
         * 允许的特殊字符:0-59,- * /
         */
        private String seconds ;
    
    
        private static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("ss mm HH dd MM ? yyyy"));
        /***
         *  日期转换cron表达式 例如 "0 07 10 15 1 ? 2016"
         * @param date 时间点
         * @return
         */
        public static String getCron(Date date) {
            String formatTimeStr = null;
            if (Objects.nonNull(date)) {
                formatTimeStr = dateFormat.get().format(date);
            }
            return formatTimeStr;
        }
    
        /**
         * 获取指定日期的cron表达式
         * @param year 年
         * @param week 星期 可以用数字1-7表示(1 = 星期日)或用字符口串“SUN, MON, TUE, WED, THU, FRI and SAT”表示
         * @param month 月 可以用0-11 或用字符串  “JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and DEC” 表示
         * @param day 日 可以用数字1-31 中的任一一个值,但要注意一些特别的月份
         * @param hour 时 可以用数字0-23表示
         * @param minutes 分 可以用数字0-59 表示
         * @param seconds 秒 可以用数字0-59 表示
         * @return
         */
        public static String getCron(String year,String week,String month,String day,String hour,String minutes,String seconds) {
            return seconds+" "+minutes+" "+hour+" "+day+" "+month+" "+week+" "+year;
        }
    
        /**
         * 获取指定日期的cron表达式
         * @param week 星期 可以用数字1-7表示(1 = 星期日)或用字符口串“SUN, MON, TUE, WED, THU, FRI and SAT”表示
         * @param month 月 可以用0-11 或用字符串  “JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and DEC” 表示
         * @param day 日 可以用数字1-31 中的任一一个值,但要注意一些特别的月份
         * @param hour 时 可以用数字0-23表示
         * @param minutes 分 可以用数字0-59 表示
         * @param seconds 秒 可以用数字0-59 表示
         * @return
         */
        public static String getCron(String week,String month,String day,String hour,String minutes,String seconds) {
            return getCron("*",week,month,day,hour,minutes,seconds);
        }
    
        /**
         * 获取指定日期的cron表达式
         * @param month 月 可以用0-11 或用字符串  “JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and DEC” 表示
         * @param day 日 可以用数字1-31 中的任一一个值,但要注意一些特别的月份
         * @param hour 时 可以用数字0-23表示
         * @param minutes 分 可以用数字0-59 表示
         * @param seconds 秒 可以用数字0-59 表示
         * @return
         */
        static String getCron(String month,String day,String hour,String minutes,String seconds) {
            return getCron("?",month,day,hour,minutes,seconds);
        }
    
        /**
         * 获取指定范围的Cron表达式 例如 13-14 30-31 11-12 20-21 04-05 1-2 2021-2022
         * @param year 年 使用(year1-year2) year1<=year2
         * @param week 星期 使用(week1-week2) 可以用数字1-7表示(1 = 星期日)或用字符口串“SUN, MON, TUE, WED, THU, FRI and SAT”表示
         * @param month 月 使用(month1-month2) 可以用0-11 或用字符串  “JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and DEC” 表示
         * @param day 日  使用(day1-day2) 可以用数字1-31 中的任一一个值,但要注意一些特别的月份
         * @param hour 时 使用(hour1-hour2) 可以用数字0-23表示
         * @param minutes 分  使用(minutes1-minutes2) 可以用数字0-59 表示
         * @param seconds 秒  使用(seconds1-seconds2) 可以用数字0-59 表示
         * @return
         */
        public static String getCronByRange(String year,String week,String month,String day,String hour,String minutes,String seconds) {
            return seconds+" "+minutes+" "+hour+" "+day+" "+month+" "+week+" "+year;
        }
    
        /**
         * 获取指定范围的Cron表达式 例如 13-14 30-31 11-12 20-21 04-05 1-2
         * @param week 星期 使用(week1-week2) 可以用数字1-7表示(1 = 星期日)或用字符口串“SUN, MON, TUE, WED, THU, FRI and SAT”表示
         * @param month 月 使用(month1-month2) 可以用0-11 或用字符串  “JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and DEC” 表示
         * @param day 日  使用(day1-day2) 可以用数字1-31 中的任一一个值,但要注意一些特别的月份
         * @param hour 时 使用(hour1-hour2) 可以用数字0-23表示
         * @param minutes 分  使用(minutes1-minutes2) 可以用数字0-59 表示
         * @param seconds 秒  使用(seconds1-seconds2) 可以用数字0-59 表示
         * @return
         */
        public static String getCronByRange(String week,String month,String day,String hour,String minutes,String seconds) {
            return getCron("*",week,month,day,hour,minutes,seconds);
        }
    
        /**
         * 获取指定范围的Cron表达式  例如 13-14 30-31 11-12 20-21 04-05
         * @param month 月 使用(month1-month2) 可以用0-11 或用字符串  “JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and DEC” 表示
         * @param day 日  使用(day1-day2) 可以用数字1-31 中的任一一个值,但要注意一些特别的月份
         * @param hour 时 使用(hour1-hour2) 可以用数字0-23表示
         * @param minutes 分  使用(minutes1-minutes2) 可以用数字0-59 表示
         * @param seconds 秒  使用(seconds1-seconds2) 可以用数字0-59 表示
         * @return
         */
        static String getCronByRange(String month,String day,String hour,String minutes,String seconds) {
            return getCron("?",month,day,hour,minutes,seconds);
        }
    
        public static void main(String[] args) {
            System.out.println(getCron(new Date()));
        }
    }
    

    相关文章

      网友评论

          本文标题:xxl-job之API的方式接入

          本文链接:https://www.haomeiwen.com/subject/lkbvodtx.html