使用它给 ​xxl-job 添加任务,太爽了

a91090e3b57bfd6d5705a0ad4ea67736.jpeg

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

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

c9f3c5f03aaef3c5fc1624d8b20d4bf4.png

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

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

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

分析

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

a893e5478dbc3ae35050b8547cd9bf8d.jpeg

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

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

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

https://github.com/xuxueli/xxl-job/https://github.com/xuxueli/xxl-job/

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

b16739454008e21ce4666c7098c0aa3b.png

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

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

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

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

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

f284cabb50f9d4d0875b68b0ee65f691.png

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

3a2a3baaa314026b6b826fd77b94a287.png

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

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

  • /jobgroup/save:添加执行器

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

  • /jobinfo/add:添加任务

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

4b2d7a1ba30a9df289351bbde1184056.png

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

58c57908af6258529ab607528bc4dbcc.png

至于这条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("xxx");
}@XxlJob(value = "testJob222")
@XxlRegister(cron = "59 1-2 0 * * ?",triggerStatus = 1)
public void testJob2(){System.out.println("yyy");
}@XxlJob(value = "testJob444")
@XxlRegister(cron = "59 59 23 * * ?")
public void testJob4(){System.out.println("hello xxl job");
}

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

f918721cfaf4b15c2cece24e7d8286df.png

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

a91d54b7648165c0af3da464a7be1e13.png

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

f5efd57bd2211e692205f6ae2c075f57.png

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

最后

项目的完整代码已经传到了我的github上,小伙伴们如果有需要的可以自行下载,也欢迎来点个star支持一下~

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

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

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

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

相关文章

dubboSPI机制浅谈

2019独角兽企业重金招聘Python工程师标准>>> &#xfeff;&#xfeff;&#xfeff;本文重点讲述SPI机制&#xff0c;从jdk和dubbo 1、jdk spi机制 2、dubbo spi实现 首先spi是什么&#xff1f; SPI是为某个接口寻找服务实现的机制。为了实现在模块装配的时候能不在…

python 类中静态变量_Python中的类或静态变量

python 类中静态变量Python类/静态变量 (Python Class / Static Variables) Class or Static variables are class-related variables that are shared among all objects but the instance or non-static variable is unique for each object. In Python, there is no need fo…

彻底搞懂 SpringBoot 中的 starter 机制

前言我们都知道&#xff0c;Spring的功能非常强大&#xff0c;但也有些弊端。比如&#xff1a;我们需要手动去配置大量的参数&#xff0c;没有默认值&#xff0c;需要我们管理大量的jar包和它们的依赖。为了提升Spring项目的开发效率&#xff0c;简化一些配置&#xff0c;Sprin…

android四大组件之Service 注册广播接收者

广播的注册一共有两种&#xff0c;一种就是用清单文件注册&#xff0c;还有另外一种就是用代码注册&#xff0c;代码注册比较灵活&#xff0c;可以在需要的时候注册&#xff0c;不需要的时候解除注册 用服务注册广播首先要开启服务&#xff0c; 然后在服务oncreate方法里注册广…

c ++向量库_将向量复制到C ++中的另一个向量

c 向量库The ways that we are using to copy vectors in C, are: 我们用于在C 中复制向量的方法是&#xff1a; Copy one vectors elements to another (Simple approach) 将一个向量的元素复制到另一个(简单方法) Copy vector by using an assignment operator 通过使用赋值…

Java 中验证时间格式的 4 种方法

大家好&#xff0c;今天咱们来讲一下&#xff0c;Java 中如何检查一个字符串是否是合法的日期格式&#xff1f;为什么要检查时间格式&#xff1f;后端接口在接收数据的时候&#xff0c;都需要进行检查。检查全部通过后&#xff0c;才能够执行业务逻辑。对于时间格式&#xff0c…

FreeSWITCH的TLS加密

听着很高大上&#xff08;实际也很实用&#xff09;的加密机制&#xff0c;在FreeSWITCH里配置支持竟然这么简单&#xff01; Greate FreeSWITCH and Greate Programmer&#xff01; ① cd /usr/local/freeswitch/bin&#xff08;以默认的安装路径为例&#xff09; ② 产生root…

kotlin 查找id_Kotlin程序查找Sphere的体积

kotlin 查找idFormula to find volume of Sphere: volume (4/3)*PI*r^3 查找球体体积的公式&#xff1a; volume (4/3)* PI * r ^ 3 Given the value of radius, we have to find the volume of Sphere. 给定半径的值&#xff0c;我们必须找到球体的体积。 Example: 例&#…

Redis 实现分布式锁的 7 种方案

前言日常开发中&#xff0c;秒杀下单、抢红包等等业务场景&#xff0c;都需要用到分布式锁。而Redis非常适合作为分布式锁使用。本文将分七个方案展开&#xff0c;跟大家探讨Redis分布式锁的正确使用方式。如果有不正确的地方&#xff0c;欢迎大家指出哈&#xff0c;一起学习一…

css复选框样式_使用CSS样式复选框

css复选框样式Introduction: 介绍&#xff1a; Sometimes we want to develop a website or web page that would contain a form and through that form, we want to get some information from the user. Now that information could be of any type depending on the kind …

大数据计算平台Spark内核全面解读

1、Spark介绍 Spark是起源于美国加州大学伯克利分校AMPLab的大数据计算平台&#xff0c;在2010年开源&#xff0c;目前是Apache软件基金会的顶级项目。随着Spark在大数据计算领域的暂露头角&#xff0c;越来越多的企业开始关注和使用。2014年11月&#xff0c;Spark在Daytona Gr…

javascript对话框_JavaScript中的对话框

javascript对话框JavaScript对话框 (JavaScript Dialog Boxes) Dialog boxes are a great way to provide feedback to the user when they submit a form. In JavaScript, there are three kinds of Dialog boxes, 对话框是向用户提交表单时提供反馈的好方法。 在JavaScript中…

排查死锁的 4 种工具,秀~

作者 | 磊哥来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;死锁&#xff08;Dead Lock&#xff09;指的是两个或两个以上的运算单元&#xff08;进程、线程或协程&#xff09;&#xf…

MySQL 常见的 9 种优化方法

大家好&#xff0c;我是磊哥&#xff01;今天给大家分享一些简单好用的数据库优化方式&#xff01;1、选择最合适的字段属性Mysql是一种关系型数据库&#xff0c;可以很好地支持大数据量的存储&#xff0c;但是一般来说&#xff0c;数据库中的表越小&#xff0c;在它上面执行的…

oracle中dbms_DBMS中的实例和架构

oracle中dbms1)实例 (1) Instances) What is the Instance? If we look towards it in real life, we refer instance as an occurrence of something at a particular moment of time. In Database Management system, there are a lot of changes occurring over time to th…

acess() 判断目录是否存在

acess()功能描述&#xff1a; 检查调用进程是否可以对指定的文件执行某种操作。 <pre lang"c" escaped"true">#include <unistd.h>int access(const char *pathname, int mode); </pre>参数说明&#xff1a;pathname: 需要测试的文件路径…

过滤器和拦截器的 5 个区别!

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;过滤器&#xff08;Filter&#xff09;和拦截器&#xff08;Interceptor&#xff09;都是基于 AOP&#xff08;Aspec…

简单的求和(打表)

简单的求和 Time Limit: 1 Sec Memory Limit: 128 MB Submit: 130 Solved: 20SubmitStatusWeb BoardDescription 定义f(i)代表i的所有因子和(包括1和i)&#xff0c;给定一个l,r。求f(l)f(l1)...f(r)。 Input 第一行输入一个t(t<1000)&#xff0c;代表有t组测试数据&#x…

chroot函数使用_PHP chroot()函数与示例

chroot函数使用PHP chroot()函数 (PHP chroot() function) The full form of chroot is "Change Root", the function chroot()" is used to change the root directory, and, also changes the current working directory to "/". chroot的完整格式为…

面试突击第一季完结:共 91 篇!

感谢各位读者的支持与阅读&#xff0c;面试突击系列第一季到这里就要和大家说再见了。希望所写内容对大家有帮助&#xff0c;也祝你们找到满意的工作。青山不改&#xff0c;细水长流&#xff0c;我们下一季再见&#xff01;91&#xff1a;MD5 加密安全吗&#xff1f;90&#xf…