Auto‑Register XXL‑Job Executors and Tasks with a Spring Boot Starter
This guide explains how to create a Spring Boot starter that automatically registers XXL‑Job executors and job handlers to the scheduling center, eliminating manual configuration through the admin UI and streamlining task management for large‑scale Java projects.
Hello everyone, I’m Su San.
XXL‑Job is a lightweight, easy‑to‑use distributed task scheduling middleware that many projects rely on for handling timed jobs.
Normally you must open the XXL‑Job admin console to register an executor and each job manually, which becomes tedious when you have dozens or hundreds of tasks.
Analysis
The solution is to register the executor and all job handlers automatically when the application starts. The required steps are:
Register the executor and each jobHandler via the admin APIs.
Use the xxl.job.executor.appname property for automatic registration of the executor address.
First, clone the XXL‑Job source code from GitHub:
https://github.com/xuxueli/xxl-job/
After importing the project into IDEA, the main modules are: xxl-job-admin: the scheduling center with the management UI. xxl-job-core: the common dependency used by client projects. xxl-job-executor-samples: sample executors (Spring Boot and plain Java).
To discover the APIs for executor and job handler registration, capture the network request when you add a new executor or job in the UI. The key endpoints are: /jobgroup/pageList – query executor list. /jobgroup/save – add a new executor. /jobinfo/pageList – query job list. /jobinfo/add – add a new job.
All these endpoints require authentication. After logging in via /login, the server returns a cookie named XXL_JOB_LOGIN_IDENTITY. Store this cookie and include it in subsequent API calls.
Transformation
We create a starter that performs the following:
On application startup, automatically register the executor.
Scan the Spring context for beans annotated with @XxlJob and our custom @XxlRegister.
For each method, check whether the corresponding executor and job handler already exist; if not, call the admin APIs to create them.
Add the required dependencies:
<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. API Calls
Login service obtains the authentication cookie and caches it:
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(c -> c.getName().equals("XXL_JOB_LOGIN_IDENTITY"))
.findFirst();
if (!cookieOpt.isPresent())
throw new RuntimeException("get xxl-job cookie error!");
loginCookie.put("XXL_JOB_LOGIN_IDENTITY", cookieOpt.get().getValue());
}Subsequent requests retrieve the cookie, retrying login up to three times if necessary:
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!");
}2. Executor Registration
Service methods query and create executors:
public List<XxlJobGroup> getJobGroup() {
String url = adminAddresses + "/jobgroup/pageList";
HttpResponse response = HttpRequest.post(url)
.form("appname", appName)
.form("title", title)
.cookie(jobLoginService.getCookie())
.execute();
JSONArray array = JSONUtil.parse(response.body()).getByPath("data", JSONArray.class);
return array.stream()
.map(o -> JSONUtil.toBean((JSONObject) o, XxlJobGroup.class))
.collect(Collectors.toList());
}
public boolean preciselyCheck() {
return getJobGroup().stream()
.anyMatch(g -> g.getAppname().equals(appName) && g.getTitle().equals(title));
}
public boolean autoRegisterGroup() {
String url = adminAddresses + "/jobgroup/save";
HttpResponse response = HttpRequest.post(url)
.form("appname", appName)
.form("title", title)
.cookie(jobLoginService.getCookie())
.execute();
return JSONUtil.parse(response.body()).getByPath("code").equals(200);
}3. Job Registration
Job service queries and adds jobs:
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();
JSONArray array = JSONUtil.parse(response.body()).getByPath("data", JSONArray.class);
return array.stream()
.map(o -> JSONUtil.toBean((JSONObject) o, XxlJobInfo.class))
.collect(Collectors.toList());
}
public Integer addJobInfo(XxlJobInfo jobInfo) {
String url = adminAddresses + "/jobinfo/add";
HttpResponse response = HttpRequest.post(url)
.form(BeanUtil.beanToMap(jobInfo))
.cookie(jobLoginService.getCookie())
.execute();
JSON json = JSONUtil.parse(response.body());
if (json.getByPath("code").equals(200)) {
return Convert.toInt(json.getByPath("content"));
}
throw new RuntimeException("add jobInfo error!");
}4. Custom Annotation
Define @XxlRegister to supply additional metadata such as cron expression, description, author, and trigger status:
@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;
}5. Auto‑Registration Core
The core listener runs after the Spring context is ready, registers the executor if needed, then scans all beans for methods annotated with both @XxlJob and @XxlRegister. For each method it checks whether the job already exists (using a fuzzy query followed by an exact match) and registers it if not:
@Component
public class XxlJobAutoRegister implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
@Autowired private JobGroupService jobGroupService;
@Autowired private JobInfoService jobInfoService;
private ApplicationContext applicationContext;
@Override public void setApplicationContext(ApplicationContext ctx) { this.applicationContext = ctx; }
@Override public void onApplicationEvent(ApplicationReadyEvent event) {
addJobGroup();
addJobInfo();
}
private void addJobGroup() {
if (jobGroupService.preciselyCheck()) return;
if (jobGroupService.autoRegisterGroup())
log.info("auto register xxl-job group success!");
}
private void addJobInfo() {
List<XxlJobGroup> groups = jobGroupService.getJobGroup();
XxlJobGroup group = groups.get(0);
String[] beanNames = applicationContext.getBeanNamesForType(Object.class, false, true);
for (String beanName : beanNames) {
Object bean = applicationContext.getBean(beanName);
Map<Method, XxlJob> methods = MethodIntrospector.selectMethods(
bean.getClass(),
method -> AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class)
);
for (Map.Entry<Method, XxlJob> entry : methods.entrySet()) {
Method method = entry.getKey();
XxlJob xxlJob = entry.getValue();
if (method.isAnnotationPresent(XxlRegister.class)) {
XxlRegister reg = method.getAnnotation(XxlRegister.class);
List<XxlJobInfo> existing = jobInfoService.getJobInfo(group.getId(), xxlJob.value());
if (!existing.isEmpty()) {
boolean already = existing.stream()
.anyMatch(info -> info.getExecutorHandler().equals(xxlJob.value()));
if (already) continue;
}
XxlJobInfo info = createXxlJobInfo(group, xxlJob, reg);
jobInfoService.addJobInfo(info);
}
}
}
}
private XxlJobInfo createXxlJobInfo(XxlJobGroup group, XxlJob xxlJob, XxlRegister reg) {
XxlJobInfo info = new XxlJobInfo();
info.setJobGroup(group.getId());
info.setExecutorHandler(xxlJob.value());
info.setJobCron(reg.cron());
info.setJobDesc(reg.jobDesc());
info.setAuthor(reg.author());
info.setTriggerStatus(reg.triggerStatus());
// other required fields omitted for brevity
return info;
}
}6. Auto‑Configuration
Register the component scan so the starter is picked up automatically:
@Configuration
@ComponentScan(basePackages = "com.xxl.job.plus.executor")
public class XxlJobPlusConfig {}Add the configuration class to META-INF/spring.factories:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xxl.job.plus.executor.config.XxlJobPlusConfigTesting
In a new Spring Boot project, add the starter dependency:
<dependency>
<groupId>com.cn.hydra</groupId>
<artifactId>xxljob-autoregister-spring-boot-starter</artifactId>
<version>0.0.1</version>
</dependency>Configure XXL‑Job properties in application.properties (admin address, token, executor app name, etc.) and add the additional starter‑specific settings (admin username/password, executor title).
Define jobs using both annotations:
@XxlJob("testJob")
@XxlRegister(cron = "0 0 0 * * ? *", author = "hydra", jobDesc = "test job")
public void testJob() {
System.out.println("#公众号:码农参上");
}
@XxlJob("testJob2")
@XxlRegister(cron = "59 1-2 0 * * ?", triggerStatus = 1)
public void testJob2() {
System.out.println("#作者:Hydra");
}Run the application; the executor registers automatically, and the tasks appear in the XXL‑Job admin UI ready for manual execution.
Both the newly registered executor and the annotated jobs are visible in the admin console and can be triggered successfully.
The starter is now ready for reuse in other projects, saving considerable time when managing large numbers of scheduled tasks.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
