魔改xxl-job,彻底告别手动配置任务!自动注册xxljob定时任务

news2024/9/28 3:24:00

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

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

 假设项目中有上百个这样的定时任务,那么每个任务都需要走一遍绑定jobHander后端接口,填写cron表达式这个流程…

我就想问问,填多了谁能不迷糊?

于是出于功能优化(偷懒)这一动机,前几天我萌生了一个想法,有没有什么方法能够告别xxl-job的管理页面,能够让我不再需要到页面上去手动注册执行器和任务,实现让它们自动注册到调度中心呢。

分析一下,其实我们要做的很简单,只要在项目启动时主动注册executor和各个jobHandler到调度中心就可以了,流程如下:

 

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

既然有了想法咱们就直接开干,先到github上拉一份xxl-job的源码下来:

GitHub - xuxueli/xxl-job: A distributed task scheduling framework.(分布式任务调度平台XXL-JOB)

xxl-job: 一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。

整个项目导入idea后,先看一下结构:

 

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

  • xxl-job-admin:任务调度中心,启动后就可以访问管理页面,进行执行器和任务的注册、以及任务调用等功能了

  • xxl-job-core:公共依赖,项目中使用到xxl-job时要引入的依赖包

  • xxl-job-executor-samples:执行示例,分别包含了springboot版本和不使用框架的版本

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

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

 

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

  • /jobgroup/pageList:执行器列表的条件查询

  • /jobgroup/save:添加执行器

  • /jobinfo/pageList:任务列表的条件查询

  • /jobinfo/add:添加任务 

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

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

 

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

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

 我们改造的目的是实现一个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、接口调用

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

登录接口

创建一个JobLoginService,在调用业务接口前,需要通过登录接口获取cookie,并在获取到cookie后,缓存到本地的Map中。

 

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

 

执行器接口

创建一个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);
}

 

任务接口

创建一个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!");
}

 

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为运行状态。

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();//注册任务
    }
}

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

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

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

 到这里starter的编写就完成了,可以通过maven发布jar包到本地或者私服:

mvn clean install/deploy

 测试---新建一个springboot项目,引入我们在上面打好的包:

 

<dependency>
    <groupId>com.cn.hydra</groupId>
    <artifactId>xxljob-autoregister-spring-boot-starter</artifactId>
    <version>0.0.1</version>
</dependency>

 在application.properties中配置xxl-job的信息,首先是原生的配置内容:

xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
xxl.job.accessToken=default_token
xxl.job.executor.appname=xxl-job-executor-test
xxl.job.executor.address=
xxl.job.executor.ip=127.0.0.1
xxl.job.executor.port=9999
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
xxl.job.executor.logretentiondays=30

 此外还要额外添加我们自己的starter要求的新配置内容:

# admin用户名
xxl.job.admin.username=admin
# admin 密码
xxl.job.admin.password=123456
# 执行器名称
xxl.job.executor.title=test-title

完成后在代码中配置一下XxlJobSpringExecutor,然后在测试接口上添加原生@XxlJob注解和我们自定义的@XxlRegister注解:

@XxlJob(value = "testJob")
@XxlRegister(cron = "0 0 0 * * ? *",
        author = "hydra",
        jobDesc = "测试job")
public void testJob(){
    System.out.println("#公众号:码农参上");
}


@XxlJob(value = "testJob222")
@XxlRegister(cron = "59 1-2 0 * * ?",
        triggerStatus = 1)
public void testJob2(){
    System.out.println("#作者:Hydra");
}

@XxlJob(value = "testJob444")
@XxlRegister(cron = "59 59 23 * * ?")
public void testJob4(){
    System.out.println("hello xxl job");
}

 启动项目,可以看到执行器自动注册成功:

 

项目的完整代码已经传到了我的github上,小伙伴们如果有需要的可以自行下载

https://github.com/trunks2008/xxl-job-auto-register

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/688377.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

WiFi模块测试|高通QCA9531方案WiFi模块网页配置说明-SKW99/SKW100

本篇以高通QCA9531方案无线路由WiFi模块SKW99为例&#xff0c;简单介绍 高通方案无线路由WiFi模块的软件使用。友情提示&#xff1a;多图&#xff0c;请在WiFi环境下阅读。 以SKW99为例&#xff0c;在SKW99规格书找到模块系统框图、PIN脚图及各个PIN脚的描述定义&#xff1b;之…

AutoSAR系列讲解(入门篇)3.4-RTE对Ports的支撑(下)

一、C/S接口的实现 之前在第二章AppL中讲过了C/S接口&#xff0c;这里再更加深入的说明一下其实现的原理&#xff1a;首先&#xff0c;C/S接口就是客户/服务接口&#xff0c;这个接口就是客户来调用服务 端的操作的一个接口。也就是我写着写着&#xff0c;发现我想要调用一个函…

【Figma技巧】布尔变量控制图层显隐

用2023年6月22日更新的Figma最新功能&#xff0c;实现按钮控制图层显隐的交互。 实现效果 步骤 1. 创建本地变量 点击右侧面板中的Local variables弹出变量面板&#xff0c;点击底部Create variable按钮&#xff0c;创建一个Boolean布尔变量。 本案例中&#xff0c;我需要分…

从0开始Jmeter接口测试实战

在之前的文章中给大家介绍过接口测试文档和接口测试用例示例&#xff0c;本文基于Jmeter工具给大家介绍一下如何实现接口测试用例&#xff1a;包括发起Http请求&#xff0c;绕过登陆&#xff0c;验证响应。JMeter是Apache组织开发的基于Java的压力测试工具。具有开源免费、框架…

Python|Pyppeteer操作浏览器,弹出文件选择框,实现自动选择“指定文件”(14)

前言 本文是该专栏的第14篇,结合优质项目案例持续分享Pyppeteer的干货知识,记得关注。 使用pyppeyeer操作浏览器的时候,可能有时候会遇到这样的情况,如下图所示: 通过程序脚本自动点击某个按钮之后,触发一个弹出框,需要输入对应的文件路径。经验丰富的同学,可能会想到…

深入浅出 - 单例模式

博主介绍&#xff1a; ✌博主从事应用安全和大数据领域&#xff0c;有8年研发经验&#xff0c;5年面试官经验&#xff0c;Java技术专家✌ Java知识图谱点击链接&#xff1a;体系化学习Java&#xff08;Java面试专题&#xff09; &#x1f495;&#x1f495; 感兴趣的同学可以收…

【MySQL】幽深不可测,登此方觉心

详解Mysql安装教程 一、MySQL基础 1、MySQL是什么? MySQL是一种用关系型数据库管理系统的软件。它是一种开源数据库&#xff0c;可以利用它来存储、管理和访问各种类型的数据。MySQL可用于多种应用程序&#xff0c;包括网站、电子商务系统、移动应用程序、企业级应用程序等…

Clickhouse物化视图原理和使用详解

前言 ClickHouse广泛用于用户和系统日志查询场景中&#xff0c;主要针对于OLAP场景&#xff0c;为业务方提供稳定高效的查询服务。在业务场景下&#xff0c;数据以不同的格式、途径写入到clickhouse。用传统JOIN方式查询海量数据&#xff0c;通常有如下痛点: 每个查询的代码冗…

【Java】ConcurrentHashMap1.8源码保姆级解析

目录 ConcurrentHashMap 1.8的优化 初始化流程 扩容流程 读取数据流程 计数器的实现 ConcurrentHashMap 1.8的优化 存储结构的优化 数组链表 -> 数组链表红黑树 写数据加锁的优化 扩容的优化&#xff08;协助优化&#xff09; 计数器的优化 LongAddr -> Cell[] 分…

力扣算法练习(二)

主要参考自力扣官网解法 1.最长回文子串(5) 给你一个字符串 s&#xff0c;找到 s 中最长的回文子串。 如果字符串的反序与原始字符串相同&#xff0c;则该字符串称为回文字符串。 示例 1&#xff1a; 输入&#xff1a;s "babad" 输出&#xff1a;"bab"…

【Java】JVM学习(七)

JVM调优 堆空间如何设置 在分代模型中&#xff0c;各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小&#xff0c;分析活跃数据的大小是很好的切入点。 活跃数据的大小&#xff1a;应用程序稳定运行时长期存活对象在堆中占用的空间大小&#xff0c;也就是Full …

Git安装及使用图文教程详解(附带安装文件)

Git安装及使用图文教程详解&#xff08;附带安装文件&#xff09; 原创&#xff1a;丶无殇  2023-06-26 文章目录 下载安装下载安装验证安装成功版本查看 基础指令Git常用指令【首次必须】设置签名用户、邮箱1.初始化本地仓库2.查看本地库状态3.创建文件4.添加文件至暂存区5…

【DCT变换】Python矩阵运算实现DCT变换

一、前言 DCT变换&#xff08;离散余弦变换&#xff09; 是数字图像处理过程中广泛采用的一种操作&#xff0c;用于将空域的图像转换为频域表示&#xff0c;从而能够更有效地进行压缩、滤波和特征提取等处理。它在许多应用领域中发挥着重要的作用&#xff0c;尤其在图像和视频…

感知机(Perceptron)的原理及实现

1.感知机&#xff08;Perceptron&#xff09;的原理及实现 声明&#xff1a;笔记来源于《白话机器学习的数学》 感知机是接受多个输入后将每个值与各自权重相乘&#xff0c;最后输出总和的模型。 单层感知机因过于简单&#xff0c;无法应用于实际问题&#xff0c;但它是神经网络…

一文让你搞定接口测试

一、什么是接口测试&#xff1f; 所谓接口&#xff0c;是指同一个系统中模块与模块间的数据传递接口、前后端交互、跨系统跨平台跨数据库的对接。而接口测试&#xff0c;则是通过接口的不同情况下的输入&#xff0c;去对比输出&#xff0c;看看是否满足接口规范所规定的功能、…

二叉树知识小结

思维导图&#xff1a; 一&#xff0c;树 树&#xff0c;这是一种对计算机里的某种数据结构的形象比喻。比如这种: 这种&#xff1a; 这种&#xff1a; 这几种都是树形结构。在百度百科中对树形结构的定义如下&#xff1a; 树形结构指的是数据元素之间存在着“一对多”的树形关系…

津津乐道设计模式 - 建造者模式详解(教你如何构造一个专属女友)

&#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Micro麦可乐的博客 &#x1f425;《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程&#xff0c;入门到实战 &#x1f33a;《RabbitMQ》…

接口测试断言详解(Jmeter)

接口测试是目前最主流的自动化测试手段&#xff0c;它向服务器发送请求&#xff0c;接收和解析响应结果&#xff0c;通过验证响应报文是否满足需求规约来验证系统逻辑正确性。接口的响应类型通过Content-Type指定&#xff0c;常见的响应类型有&#xff1a; • text/html &…

Android Jetpack Compose之轻松添加分隔线:Divider组件

引言&#xff1a; 在构建用户界面时&#xff0c;有效地组织和分隔内容是至关重要的。这就是Android Jetpack Compose的Divider组件派上用场的地方。在这篇博客中&#xff0c;我们将详细了解Divider组件的功能和用法&#xff0c;并通过示例展示如何将其融入您的Compose UI。 Je…

自动化测试和性能测试面试题精选

自动化测试相关 包含 Selenium、Appium 和接口测试。 1. 自动化代码中&#xff0c;用到了哪些设计模式&#xff1f; 单例模式工厂模式PO模式数据驱动模式 2. 什么是断言&#xff1f; 检查一个条件&#xff0c;如果它为真&#xff0c;就不做任何事&#xff0c;用例通过。如果…