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

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

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

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

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

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

    原创:微信公众号 码农参上,欢迎分享,转载请保留出处. 哈喽大家好啊,我是Hydra. xxl-job是一款非常优秀的任务调度中间件,轻量级.使用简单.支持分布式等优点,让它广泛应用在我们的项目中,解 ...

  2. webpack手动配置以及自动配置

    本次文章主要是为了自己以后复习用的,如果能够帮到你就更好. 首先我们需要了解前端工程化 概念:以模块化.组件化.规范化.自动化为基础,进行前端项目开发的方式. 1.模块化:js的模块化,css的模块化 ...

  3. 手动配置和自动配置ODBC数据源(C++)

    一般来说,我们在使用ODBC时,都是手动配置数据源,也就是使用电脑自带的ODBC数据源窗口进行手动配置.当我们在进行项目开发的时候,编好的代码给用户时,用户需要重新配置数据源,十分麻烦,为了方便,我们 ...

  4. 一个实用技巧,告别手动画图,自动生成数据库 ER 图|原创

    来自:程序通事 忙,真忙 这两个星期真是巨忙,年前有个项目因为各种莫名原因,一直拖到这个月才开始真正测试.然后上周又接到新需求,马不停蹄进入开发. 一边开发着新需求,一边改着旧项目的 Bug.再加上这 ...

  5. OneNav一为主题魔改教程(五):点赞后自动加入到首页“我的导航”--洞五洞洞幺

    今日主题: 点赞后自动加入到首页"我的导航" 声明:修改之前做好笔记或备份. 嫌"我的导航"没内容, 在网站中发现了常用的网站不能快速的到达. 所以我们来改进这 ...

  6. CentOS 6/7手动安装和魔改教程

    安装BBR 1.安装BBR内核 #CentOS 6系统 #导入ELRepo公钥 wget https://www.elrepo.org/RPM-GPG-KEY-elrepo.org rpm --imp ...

  7. butterfly配置 hexo_Hexo博客之butterfly主题优雅魔改系列(持续更新)

    为了便于各位博主了解到是否新增了魔改方案,请查看更新记录页 直接体验魔改效果可以访问我的个人博客 小康博客 此文更新可能不及时,如果更新记录有新的方案后,本文没有那么请移至我的个人博客进行查找. 前置 ...

  8. CobaltStrike魔改与增强

    文章为匿名投稿,该文章仅限提供思路,具体实现请自行研究使用. 文章内用到的代码源码 详见末尾 由于传播.利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担 ...

  9. 小米android系统分享,重温的手机分享 篇一:90hz高刷?45w快充?双扬声器?你不知道的酷安重制版小米9魔改教程...

    重温的手机分享 篇一:90hz高刷?45w快充?双扬声器?你不知道的酷安重制版小米9魔改教程 2021-05-14 14:47:30 6点赞 13收藏 10评论 大家上午好啊,我是重温,有幸能拿到这款 ...

最新文章

  1. linux下查看已经安装的jdk 并卸载jdk
  2. (转)Lucene.NET 使用
  3. university of liverpool writing techniques
  4. U盘安装BackTrack5,提示Could not find kernel image: linux
  5. 对象的软、弱、和虚引用
  6. 虚析构函数? vptr? 指针偏移?多态数组? delete 基类指针 内存泄漏?崩溃?...
  7. 学习计算机游戏编程,在线游戏学编程,游戏编程汇总
  8. 从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 二十二║Vue实战:个人博客第一版(axios+router)...
  9. PHP安装ZIP扩展
  10. 自考计算机专业,自考计算机
  11. 常用电脑的人养眼绝招
  12. 基于51单片机简易计算器LCD1602显示
  13. MATLAB的矩阵输入
  14. 还在使用Window原始的CMD界面?教你一招进行界面完美优化
  15. 实时可视化大数据项目01 -- 项目介绍
  16. 5-TAMRA标记LYS赖氨酸,5-TAMRA-Lysine结构式及光谱图分享
  17. Autoware1.14-摄像头目标检测YOLO-V3
  18. 语义通信(Semantic Communication)
  19. XSS跨站脚本攻击与CSRF跨站请求伪造攻击的学习总结
  20. IE8:像用Windows一样用浏览器

热门文章

  1. 前端 xxx = xxx xxx , xxx = xxx || xxx到底什么意思
  2. 成为数据科学家,需具备这些技能
  3. 大数据用户画像方法与实践
  4. Eclipse调试找不到源的解决办法
  5. 用示波器测量I2C进行时序图、波形等分析
  6. [LeetCode] LeetCode中与有向图相关的题目总结
  7. 虚拟主机的安全吗?如何选择安全的虚拟主机?
  8. java 字符串 转 文件路径_连接用户输入字符串转换为完整的文件路径(Java)
  9. 新闻快讯 联合国将使用区块链资源
  10. 【Python 3.7】喜欢的图书:编写一个名为 favorite_book() 的函数,其中包含一个名为 title 的形参。