Switch Between Spring @Scheduled and XXL‑JOB Dynamically with a Configurable Starter
This article explains how to create a Spring Boot starter that lets you toggle between the native @Scheduled scheduler and XXL‑JOB at runtime via a configuration property, automatically scanning annotated methods, disabling Spring's tasks, and registering them as XXL‑JOB jobs with proper logging.
Problem Analysis
When using XXL‑JOB for scheduled tasks, deployment constraints sometimes require falling back to Spring's built‑in @Scheduled implementation. The goal is to achieve flexibility by switching implementations through configuration without modifying existing task code, while also synchronizing log management.
Implementation Overview
Enable XXL‑JOB via Configuration
Use Spring Boot's auto‑configuration and @ConditionalOnProperty to load a configuration class only when xxl.job.enable=true. The auto‑configuration class reads the property and registers the necessary beans.
@Configuration
@ConditionalOnProperty(name = "xxl.job.enable", havingValue = "true")
@ComponentScan("com.teoan.job.auto.core")
public class XxlJobAutoConfiguration {
}Scanning @Scheduled Annotations After Startup
Leverage @EventListener(ApplicationReadyEvent.class) with @Async to scan the application context for beans annotated with @Component and then locate methods marked with @Scheduled. The pseudocode is:
@Component
@Slf4j
public class JobAutoRegister {
@EventListener(ApplicationReadyEvent.class)
@Async
public void onApplicationEvent(ApplicationReadyEvent event) {
// Scan beans, find @Scheduled methods, process them
}
}Disable Spring's Native Scheduling
Obtain the ScheduledAnnotationBeanPostProcessor bean and invoke its postProcessBeforeDestruction method for each bean containing @Scheduled methods, effectively stopping Spring's own scheduling.
@Override
public void postProcessBeforeDestruction(Object bean, String beanName) {
Set<ScheduledTask> tasks;
synchronized (this.scheduledTasks) {
tasks = this.scheduledTasks.remove(bean);
}
if (tasks != null) {
for (ScheduledTask task : tasks) {
task.cancel();
}
}
}Register Tasks to XXL‑JOB
Convert the metadata of each @Scheduled method (cron or fixed rate) into an XXL‑JOB job definition. Register a MethodJobHandler for the method and add the executor information to the admin center via HTTP API calls.
private void registJobHandler(String handlerName, Method executeMethod) {
executeMethod.setAccessible(true);
Method initMethod = null;
Method destroyMethod = null;
Object bean = applicationContext.getBean(executeMethod.getDeclaringClass());
XxlJobExecutor.registJobHandler(handlerName, new MethodJobHandler(bean, executeMethod, initMethod, destroyMethod));
}Auto‑Register Executor
public boolean autoRegisterGroup() {
String url = adminAddresses + "/jobgroup/save";
HttpRequest httpRequest = HttpRequest.post(url)
.form("appname", appName)
.form("title", title)
.form("addressType", addressType);
if (addressType.equals(1) && Strings.isBlank(addressList)) {
throw new RuntimeException("Manual mode requires addressList");
}
HttpResponse response = httpRequest.cookie(jobLoginService.getCookie()).execute();
Object code = JSONUtil.parse(response.body()).getByPath("code");
if (!code.equals(200)) {
log.error("xxl-job auto register group fail! msg[{}]", JSONUtil.parse(response.body()).getByPath("msg"));
return false;
}
return true;
}Add Job Information
private void addJobInfo() {
List<XxlJobGroup> jobGroups = jobGroupService.getJobGroup();
XxlJobGroup xxlJobGroup = jobGroups.get(0);
List<Object> beanList = applicationContext.getBeansWithAnnotation(Component.class).values().stream().toList();
beanList.forEach(bean -> {
Map<Method, Scheduled> annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(),
(MethodIntrospector.MetadataLookup<Scheduled>) method -> AnnotatedElementUtils.findMergedAnnotation(method, Scheduled.class));
annotatedMethods.forEach((k, v) -> {
stopScheduled(k.getDeclaringClass());
String handlerName = StringUtils.joinWith("#", k.getDeclaringClass().getName(), k.getName());
registJobHandler(handlerName, k);
Optional<XxlJobInfo> first = jobInfoService.getJobInfo(xxlJobGroup.getId(), handlerName).stream()
.filter(info -> info.getExecutorHandler().equals(handlerName)).findFirst();
XxlJobInfo xxlJobInfo = createXxlJobInfo(xxlJobGroup, v, handlerName);
if (first.isEmpty()) {
Integer jobInfoId = jobInfoService.addJobInfo(xxlJobInfo);
if (ObjectUtils.isNotEmpty(jobInfoId)) {
log.info("xxl-job auto add jobInfo success! JobInfoId[{}] JobInfo[{}]", jobInfoId, JSONUtil.toJsonStr(xxlJobInfo));
}
}
});
});
}Log Forwarding to XXL‑JOB
Implement a custom Logback AppenderBase that forwards log messages to XxlJobHelper.log, ensuring task logs appear in the XXL‑JOB console.
@Component
public class XxlJobLogAppender extends AppenderBase<ILoggingEvent> {
@Override
protected void append(ILoggingEvent event) {
if (XxlJobHelper.getJobId() == -1) return;
if (Level.ERROR.equals(event.getLevel())) {
ThrowableProxy tp = (ThrowableProxy) event.getThrowableProxy();
if (tp != null) {
XxlJobHelper.log(tp.getThrowable());
} else {
XxlJobHelper.log(event.getMessage());
}
} else {
XxlJobHelper.log(event.getMessage());
}
}
}Starter Usage
Add the starter dependency (which does not bring XXL‑JOB itself) and configure the required properties in application.yml:
server:
port: 8080
spring:
application:
name: xxlJobAuto
xxl:
job:
enable: true
accessToken:
admin:
addresses: http://localhost:8080/xxl-job-admin
username: admin
password: password
executor:
appname: ${spring.application.name}
ip:
address:
logpath:
logretentiondays: 3
port: 0
addressList:
addressType: 0
title: ${spring.application.name}Executor Bean Configuration
@Configuration
public class XxlJobConfig {
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.address}")
private String address;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
log.info(">>>> xxl-job config init.");
XxlJobSpringExecutor executor = new XxlJobSpringExecutor();
executor.setAdminAddresses(adminAddresses);
executor.setAppname(appname);
executor.setAddress(address);
executor.setIp(ip);
executor.setPort(port);
executor.setAccessToken(accessToken);
executor.setLogPath(logPath);
executor.setLogRetentionDays(logRetentionDays);
return executor;
}
}Sample @Scheduled Job
@Slf4j
@Component
public class XxlJobAutoSamplesJob {
@Scheduled(fixedRate = 10000)
public void samplesJob() {
log.info("samplesJob executor success!");
}
}Verification
Run the application with xxl.job.enable=false to use Spring's scheduler; logs show the job executing. Then set xxl.job.enable=true; the starter registers the executor and job handler via the admin API, and logs confirm successful registration. The XXL‑JOB console displays the task and its logs.
Design Considerations
Task Information Update
Updating job definitions on annotation changes was deemed unnecessary because most adjustments are performed directly in the XXL‑JOB admin UI after initial registration.
Database vs API Registration
Direct database manipulation was avoided to keep the starter independent of the XXL‑JOB database location; instead, HTTP API calls are used.
Adding @Scheduled to Auto‑Configuration Class
The starter does not automatically annotate its own classes with @Scheduled to avoid unexpected side effects; users configure scheduling as needed.
Dependency Management
The starter is optional and does not embed XXL‑JOB core dependencies, mirroring the approach of utility libraries that keep core dependencies separate.
Conclusion
The tutorial demonstrates how to build a Spring Boot starter that transparently switches between native @Scheduled tasks and XXL‑JOB, automatically registers executors and jobs, forwards logs, and provides a clean configuration‑driven experience.
Project Repository
https://github.com/Teoan/xxl-job-auto
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.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.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.
