一、项目背景:
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
整个项目导入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
的自动注册,要引入的关键依赖有下面两个:
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> </dependency>
<dependency> <groupId>com.xuxueli</groupId> <artifactId>xxl-job-core</artifactId> <version>${xxl-job.version}</version> </dependency>
|
1.接口调用
在调用调度中心的接口前,先把xxl-job-admin
模块中的XxlJobInfo
和XxlJobGroup
这两个类拿到我们的starter项目中,用于接收接口调用的结果。
登录接口
创建一个JobLoginService
,在调用业务接口前,需要通过登录接口获取cookie
,并在获取到cookie
后,缓存到本地的Map
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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次。
1 2 3 4 5 6 7 8 9 10
| 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
查询执行器列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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
接口是一个模糊查询接口,所以在查询列表的结果列表中,还需要再进行一次精确匹配。
1 2 3 4 5 6 7 8
| 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
到调度中心:
1 2 3 4 5 6 7 8 9 10
| 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
名称查询任务列表,和上面一样,也是模糊查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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
注解进行使用,填写这几个字段的信息:
1 2 3 4 5 6 7 8 9
| @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; String executorRouteStrategy() default "ROUND"; }
|
最后,额外添加了triggerStatus
属性和executorRouteStrategy
,表示任务的默认调度状态,0为停止状态,1为运行状态和调度策略默认为轮询ROUND,FIRST为第一个。
3、自动注册核心
基本准备工作做完后,下面实现自动注册执行器和jobHandler
的核心代码。核心类实现ApplicationListener
接口,在接收到ApplicationReadyEvent
事件后开始执行自动注册逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @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
精确匹配查看调度中心是否已有执行器被注册过了,如果存在则跳过,不存在则新注册一个:
1 2 3 4 5 6 7
| 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
,调用接口注册到调度中心
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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
:
1 2 3 4
| @Configuration @ComponentScan(basePackages = "com.xxl.job.plus.executor") public class XxlJobPlusConfig { }
|
将它添加到META-INF/spring.factories
文件:
1 2
| org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.xxl.job.plus.executor.config.XxlJobPlusConfig
|
到这里starter
的编写就完成了,可以通过maven发布jar包到本地或者私服:
1
| mvn clean install/deploy
|
四、使用
基于springboot项目,引入我们在上面打好的包:
1 2 3 4 5
| <dependency> <groupId>com.hsyk</groupId> <artifactId>xxljob-autoregister-spring-boot-starter</artifactId> <version>0.0.1</version> </dependency>
|
在application.yml
中配置xxl-job的信息,首先是原生的配置内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| xxl: job: accessToken: default_token admin: addresses: http://192.168.129.36:30080/xxl-job-admin password: 123456 username: admin executor: address: appname: bright-sso-server ip: logpath: logretentiondays: 30 port: 9999 title: sso-executor
|
其中包含starter要求的新配置内容:
1 2 3 4 5 6
| username: admin
password: 123456
title: sso-executor
|
完成后在代码中配置一下XxlJobSpringExecutor
,然后在测试接口上添加原生@XxlJob
注解和我们自定义的@XxlRegister
注解:
1 2 3 4 5 6 7 8 9 10
| @XxlJob("demoJobHandler") @XxlRegister(cron = "0 0 0 * * ? *",author = "fanxin",jobDesc = "测试job") public void demoJobHandler() throws Exception { XxlJobHelper.log("XXL-JOB, Hello World."); for (int i = 0; i < 5; i++) { XxlJobHelper.log("beat at:" + i); TimeUnit.SECONDS.sleep(2); } }
|
启动项目,可以看到执行器自动注册成功

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

