魔改xxl-job,彻底告别手动配置任务!
原创:微信公众号
码农参上
,欢迎分享,转载请保留出处。
哈喽大家好啊,我是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版本和不使用框架的版本
为了弄清楚注册和查询executor
和jobHandler
调用的是哪些接口,我们先从页面上去抓一个请求看看:
好了,这样就能定位到xxl-job-admin
模块中/jobgroup/save
这个接口,接下来可以很容易地找到源码位置:
按照这个思路,可以找到下面这几个关键接口:
/jobgroup/pageList
:执行器列表的条件查询/jobgroup/save
:添加执行器/jobinfo/pageList
:任务列表的条件查询/jobinfo/add
:添加任务
但是如果直接调用这些接口,那么就会发现它会跳转到xxl-job-admin
的的登录页面:
其实想想也明白,出于安全性考虑,调度中心的接口也不可能允许裸调的。那么再回头看一下刚才页面上的请求就会发现,它在Headers
中添加了一条名为XXL_JOB_LOGIN_IDENTITY
的cookie
:
至于这条cookie
,则是在通过用户名和密码调用调度中心的/login
接口时返回的,在返回的response
可以直接拿到。只要保存下来,并在之后每次请求时携带,就能够正常访问其他接口了。
到这里,我们需要的5个接口就基本准备齐了,接下来准备开始正式的改造工作。
改造
我们改造的目的是实现一个starter
,以后只要引入这个starter
就能实现executor
和jobHandler
的自动注册,要引入的关键依赖有下面两个:
<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
模块中的XxlJobInfo
和XxlJobGroup
这两个类拿到我们的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;
}
我们在后面要根据配置文件中的appName
和title
判断当前执行器是否已经被注册到调度中心过,如果已经注册过那么则跳过,而/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
,根据执行器id
,jobHandler
名称查询任务列表,和上面一样,也是模糊查询:
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();//注册任务
}
}
自动注册执行器的代码非常简单,根据配置文件中的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);
}
}
}
}
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,我们下篇再见。
作者简介,
码农参上
,一个热爱分享的公众号,有趣、深入、直接,与你聊聊技术。欢迎添加好友,进一步交流。
魔改xxl-job,彻底告别手动配置任务!的更多相关文章
- 神奇!这款 Vue 后台框架居然不用手动配置路由
前言 做 Vue 开发脱离不了路由,尤其是中大型项目,页面多且杂,在配置路由的时候总是会变得逐渐暴躁,因为费时,并且又没有什么太多技术含量,总觉得是在浪费时间. 另外如果接手了别人的项目,当业务有变更 ...
- Smart3D系列教程7之 《手动配置S3C索引加载全部的瓦片数据》
一.前言 迄今为止,Wish3D已经出品推出了6篇系列教程,从倾斜摄影的原理方法.采集照片的技巧.Smart3D各模块的功能应用.小物件的照片重建.大区域的地形重建到DSM及正射影像的处理生产,立足于 ...
- 魔改——MFC SDI程序 转换为 MDI程序
==================================声明================================== 本文原创,转载在正文中显要的注明作者和出处,并保证文章的完 ...
- spring手动配置
本文总结自:https://www.cnblogs.com/V1haoge/p/7183408.html SpringBoot中免除了大部分配置,但是对于一些特定的情况,还是需要我们进行手动配置的. ...
- [Android Studio系列(五)] Android Studio手动配置Gradle的方法
1 问题 (1) android sutdio第一次打开一个工程巨慢怎么办? (2) 手动配置Gradle Home为什么总是无效? (3) 明明已经下载了Gradle,配置了gradle home, ...
- c++篇 cad.grx 入门,手动配置编译环境
安装vs2010+sp1补丁; 安装浩辰2018(64位版本); 下载浩辰Grx开发的SDK,注意对应版本年份., 解压到E盘目录下, E:\grxsdk 在他们的官方用户群下载,搜sdk, 找到gr ...
- Hexo博客美化之蝴蝶(butterfly)主题魔改
Hexo是轻量级的极客博客,因为它简便,轻巧,扩展性强,搭建部署方便深受广大人们的喜爱.各种琳琅满路的Hexo主题也是被各种大佬开发出来,十分钦佩,向大佬仰望,大声称赞:流批!!! 我在翻看各种主 ...
- 如何通过倾斜摄影数据手动配置s3c索引文件?
如何通过倾斜摄影数据手动配置s3c索引文件? 大家知道,倾斜摄影数据最常见的是OSGB格式,并且是由一个一个的Tile分级文件夹构成的Data文件夹.结构一般如下图所示: 那么,如何才能把模型的各个瓦 ...
- Hibernate3.3.2 手动配置annotation环境
简单记录Hibernate3.3.2如何快速配置环境 一.下载hibernate-distribution-3.3.2.GA-dist.zip文件,建立User libraries. 打开window ...
随机推荐
- websocket理解
简介 在实际开发中,可能会出现一个需求场景,要求网页的数据可以实时更新.在这种情况下,我们一般会采用轮询的方式,间隔性获取数据,即通过定时器间隔性请求相应接口获取数据,此方式由于是不断请求服务器,资源 ...
- 调和级数为什么是 O(logn) 的
目录 调和级数 正片 调和级数 调和级数(Harmonic series)定义为 \[H(n)=\sum_{i=1}^n\dfrac 1i \] \(H\) 发散,证明看百度 . 正片 首先我们把 \ ...
- 自动提交本地git仓库脚本
#! /bin/bash git_user_name=`git config user.name` git_user_mail=`git config user.email` branch_name= ...
- 论文解读(PPNP)《Predict then Propagate: Graph Neural Networks meet Personalized PageRank》
论文信息 论文标题:Predict then Propagate: Graph Neural Networks meet Personalized PageRank论文作者:Johannes Gast ...
- 对于Java中的Loop或For-each,哪个更快
Which is Faster For Loop or For-each in Java 对于Java中的Loop或Foreach,哪个更快 通过本文,您可以了解一些集合遍历技巧. Java遍历集合有 ...
- HCIA-Datacom 2.1 实验一:IPv4编址及IPv4路由基础实验
实验目的 掌握接口IPv4地址的配置方法 理解LoopBack接口的作用与含义 理解直连路由的产生原则 掌握静态路由的配置方法并理解其生效的条件 掌握通过PING工具测试网络层联通性 掌握 ...
- Vue3 + Socket.io + Knex + TypeScript 实现可以私聊的聊天室
前言 下文只在介绍实现的核心代码,没有涉及到具体的实现细节,如果感兴趣可以往下看,在文章最后贴上了仓库地址.项目采用前后端模式,前端使用 Vite + Vue3 + TS:后端使用 Knex + Ex ...
- Word 段前分页是什么?怎么设置?
描述 这两个标题在第一个标题的页中,且两个标题都没有独立分页.要让每一个标题独立分页,需要对标题的格式进行修改. 段前分页指的是标题与标题之间不在同一个页中,每一个标题都在独立的页中. 设置段前分页 ...
- C++大数据的读写
当一个文件1G以上的这种,使用内存文件映射会提高读写效率: 下边时段出自<windows核心编程>,读取一个大文件,然后统计里边字符出现次数的函数: __int64 CountOs(voi ...
- 垃圾收集器 参阅<<深入理解JAVA虚拟机>>
一.新生代 1.Serial收集器 新生代单线程复制算法GC(暂停工作线程)---------- 支持组合老年代Serial odl和CMS 2.ParNew Serial多线程版本 支持组合cms| ...