魔改xxl-job,彻底告别手动配置任务!

news/2024/4/26 21:26:29/文章来源:https://www.cnblogs.com/trunks2008/p/16778323.html

原创:微信公众号 码农参上,欢迎分享,转载请保留出处。

哈喽大家好啊,我是Hydra。

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

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

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

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

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

分析

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

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

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

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

https://github.com/xuxueli/xxl-job/https://github.com/xuxueli/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;@Autowiredprivate JobGroupService jobGroupService;@Autowiredprivate JobInfoService jobInfoService;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext=applicationContext;}@Overridepublic 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>() {@Overridepublic 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");
}

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

再打开调度中心的任务管理页面,可以看到同时添加了两个注解的任务也已经自动完成了注册:

从页面上手动执行任务进行测试,可以执行成功:

到这里,starter的编写和测试过程就算基本完成了,项目中引入后,以后也能省出更多的时间来摸鱼学习了~

最后

项目的完整代码已经传到了我的github上,小伙伴们如果有需要的可以自行下载。公众号【码农参上】后台回复【xxl】获取项目git地址,也欢迎来给我点个star支持一下~

那么,这次的分享就到这里,我是Hydra,我们下篇再见。

作者简介,码农参上,一个热爱分享的公众号,有趣、深入、直接,与你聊聊技术。欢迎添加好友,进一步交流。

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

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

12个小细节让普源示波器使用更加高效(上)

俗话说细节决定成败&#xff0c;示波器作为电子测量的第一工具&#xff0c;虽然使用简单&#xff0c;但并不是每个人都能注意到细节。运用好细节&#xff0c;可以使你的示波器使用更加的便捷。以下由安泰测试带来普源示波器测量相关的12个小细节可作为示波器常识快速自检的小文…

Spring Boot(4):@Import注解和@Conditional注解

说明&#xff1a;基于atguigu学习笔记。 在了解spring boot自动配置原理前&#xff0c;再来了解下两个注解Import注解和Conditional注解。 Import Import注解主要用于导入某些特殊的Bean&#xff0c;这些特殊的Bean和Bean Definitaion 有关。 主要用于导入Configuration 类…

Python实现桌面挂件,做一只可爱的桌面宠物~

文章目录嗨嗨&#xff0c;大家好 ~ 我是小圆相关文件开发工具相关模块&#xff1a;环境搭建安装原理简介1.初始化一个窗口组件&#xff1a;效果2.设置一下窗口的属性&#xff1a;随机导入一张图片&#xff0c;看效果随机导入一个宠物的所有图片的函数代码3.宠物随机出现在桌面上…

服务端渲染的探索与实践

服务端渲染(SSR)近两年炒得很火热,相信各位同学对这个名词多少有所耳闻。本节我们将围绕“是什么”(服务端渲染的运行机制)、“为什么”(服务端渲染解决了什么性能问题 )、“怎么做”(服务端渲染的应用实例与使用场景)这三个点,对服务端渲染进行探索。 服务端渲染是一…

GOM引擎登录器的研究,逆向技术在这款GOM20151108引擎上是一个大舞台

最近遇到一个逆向类课题&#xff0c;是关于GOM20151108版本对应登录器研究。刚接触传奇的时候是2002年&#xff0c;那时候网吧玩SF&#xff0c;都是手动用IP直接连接&#xff0c;当时的一款 联创传奇 很好玩&#xff0c;有传送戒子&#xff0c;木域戒指&#xff0c;土域戒指&am…

不会 Vue,但不影响我学 diff 算法

前言 现在社会各行各业大都面临着寒冬&#xff0c;互联网行业最近还出现了裁员潮&#xff0c;导致前端是越来越卷&#xff0c;普通学校的应届生不仅要跟985、211毕业的学生以及研究生进行竞争&#xff0c;甚至还需要和最近刚被裁的、有了几年工作经验的程序员竞争&#xff0c;…

page.json

uni-app需要给page.json文件需要进行配置路由,否则会不报错,也跳转不过去

【数模/启发式算法】蚁群算法

文章目录简介符号说明核心思想流程图文章使用到的测试函数基本步骤蚁群算法代码简介 蚁群算法是一种用来寻找优化路径的概率型算法。它由Marco Dorigo于1992年在他的博士论文中提出&#xff0c;其灵感来源于蚂蚁在寻找食物过程中发现路径的行为。 这种算法具有分布计算、信息正…

Arduino播放声音

玩软件有点虚无&#xff0c;没有实际东西&#xff0c;所以接下来要体验下硬件与软件结合。 1 Arduino Arduino是一种包含硬件&#xff08;各种型号的Arduino板&#xff09;和软件&#xff08;Arduino IDE&#xff09;的开源电子平台。硬件部分是可以用来做电路连接的Arduino电…

小白学习Java第四十三天

Git概述 &#xff08;一&#xff09;什么是Git Git是一个开源的分布式版本控制系统&#xff0c;可以有效、高速地处理从很小到非常大的项目版本管理。版本控制是指对软件开发过程中各种程序代码、配置文件及说明文档等文件变更的管理&#xff0c;是软件配置管理的核心思想之一…

设计模式学习笔记(五) - 观察者模式 Observer

目录 观察者模式 Observer 一、背景描述 Version 1 (面向过程) Version 2 (面向对象) Version 3 (单个观察者) Version 4 (多个观察者) Version 5 (分离观察者与被观察者) 二、不同事件下的观察者模式 三、事件本身也可以形成继承体系 四、观察者常用场景 观察者模式…

Selenium基础 — 鼠标操作

1、鼠标事件介绍 前面例子中我们已经学习到可以用click()来模拟鼠标的单击操作&#xff0c;而我们在实际的web产品测试中发现&#xff0c;有关鼠标的操作&#xff0c;不单单只有单击&#xff0c;有时候还要用到右击&#xff0c;双击&#xff0c;拖动等操作&#xff0c;这些操作…

【Nginx】认识与基本使用 Nginx 实现反向代理、配置负载均衡

文章目录1. Nginx 概述1.1 Nginx 介绍1.2 Nginx 下载和安装1.3 Nginx 目录结构2. Nginx 命令3. Nginx 配置文件结构4. Nginx 具体应用4.1 部署静态资源4.2 反向代理4.2.1 介绍4.2.2 配置反向代理4.3 负载均衡4.3.1 介绍4.3.2 配置负载均衡4.3.3 负载均衡策略1. Nginx 概述 1.1…

Ubuntu开机界面出现“error found when loading /root/.profile”

原因 今天一开始按照一篇文章&#xff0c;想把普通用户的权限提高到最高权限&#xff0c;修改了**/etc/passwd**文件&#xff0c;然后重启&#xff0c;发现之前的用户进不去了&#xff0c;一开机就出现如下信息 解决方法 1、重启虚拟机进入recovery模式&#xff08;长按shi…

计算机网络-第一章 | 王道考研

目录 一、基本介绍 定义 功能 组成 分类 标准化工作 标准的分类 标准化工作相关组织 二、性能指标 ※ 速率 带宽 ※吞吐量 时延 时延带宽积 往返时延RTT 利用率 三、分层结构 ※ 分层基本规则 正式认识分层 7层OSI参考模型 怎么来的 怎么分的 怎么传的…

<特殊类设计与单例模式>——《C++高阶》

目录 1.请设计一个类&#xff0c;不能被拷贝 2. 请设计一个类&#xff0c;只能在堆上创建对象 3. 请设计一个类&#xff0c;只能在栈上创建对象 4. 请设计一个类&#xff0c;不能被继承 5. 请设计一个类&#xff0c;只能创建一个对象(单例模式) 后记&#xff1a;●由于…

GD32F307VC+WIN10+VSCODE+GCC+JLINK环境build

为了构建Cortex M系列单片机免费开源的开发环境&#xff0c;网络上了解来看VSCODEGCCJLINK是一套比较高效的组合方式&#xff0c;下面记录环境搭建的流程。 我这边的PC环境为 WIN10专业版64bit。 工具准备 1. arm-none-eabi-gcc下载及安装 官网下载链接&#xff1a;Downloa…

c++数据结构:数组和向量

线性表&#xff1a; 在数据元素的非空有限集中 存在唯一的一个被叫做“第一个”的数据元素存在唯一的一个被叫做“最后一个”的数据元素除第一个之外&#xff0c;集合中的每个数据元素均只有一个前驱除最后一个之外&#xff0c;每个集合元素均只有一个后继数据结构中线性结构指…

文字识别检测入门(1)

CTPN 优点&#xff1a;对水平文字检测效果超级好 缺点&#xff1a;对扭曲的文字不好 RRPN 在faster的基础上改进 RPN改为RRPN ROI pooling改进为RROI pooling 能解决旋转&#xff0c;但是解决不了弯曲的曲面问题 EAST Anchor free 特征合并&#xff0c;检测不同尺度文本 检测各…

刷爆leetcode第三期 0007~0010

刷爆leetcode第三期 0007~0010 题目一 反转链表解法一解法二题目二 链表的中间节点题目三 链表的倒数第K个节点题目四 合并两个有序链表题目一 反转链表 解法一 给定单链表的头节点 head &#xff0c;请反转链表&#xff0c;并返回反转后的链表的头节点。 示例 1&#xff1a…