哈喽咱们好啊,我是Hydra。
xxl-job是一款非常优异的使命调度中间件,轻量级、使用简略、支撑分布式等优点,让它广泛应用在咱们的项目中,处理了不少守时使命的调度问题。
咱们都知道,在使用进程中需求先到xxl-job的使命调度中心页面上,装备执行器executor和具体的使命job,这一进程假如项目中的守时使命数量不多还好说,假如使命多了的话还是挺费工夫的。
假设项目中有上百个这样的守时使命,那么每个使命都需求走一遍绑定jobHander
后端接口,填写cron
表达式这个流程…
我就想问问,填多了谁能不迷糊?
于是出于功用优化(偷闲)这一动机,前几天我萌生了一个主意,有没有什么办法能够告别xxl-job的管理页面,能够让我不再需求到页面上去手动注册执行器和使命,完结让它们主动注册到调度中心呢。
分析
分析一下,其实咱们要做的很简略,只需在项目发动时主动注册executor
和各个jobHandler
到调度中心就能够了,流程如下:
有的小伙伴们可能要问了,我在页面上创立执行器的时候,不是有一个选项叫做主动注册吗,为什么咱们这儿还要自己增加新执行器?
其实这儿有个误区,这儿的主动注册指的是会依据项目中装备的xxl.job.executor.appname
,将装备的机器地址主动注册到这个执行器的地址列表中。但是假如你之前没有手动创立过执行器,那么是不会给你主动增加一个新执行器到调度中心的。
已然有了主意咱们就直接开干,先到github上拉一份xxl-job的源码下来:
github.com/xuxueli/xxl…
整个项目导入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上,码字不易,也欢迎来给我点个star支撑一下,感谢~
github.com/trunks2008/…
那么,这次的共享就到这儿,我是Hydra,咱们下篇再会。
作者简介,
码农参上
,一个酷爱共享的公众号,风趣、深化、直接,与你聊聊技能。欢迎增加好友,进一步沟通。
本文正在参与「金石方案」