Flexible Switching Between Spring @Scheduled and XXL‑JOB for Scheduled Tasks
This article explains how to implement a configuration‑driven mechanism that dynamically switches between Spring's native @Scheduled tasks and XXL‑JOB execution, automatically registers jobs, disables Spring's scheduler when needed, and forwards logs to the XXL‑JOB console, providing a complete starter solution for backend developers.
Background
When using XXL‑JOB for scheduled tasks, deployment constraints sometimes force the use of Spring's built‑in scheduler. To retain flexibility without modifying existing @Scheduled code, we explore a configuration‑driven approach that can switch between Spring and XXL‑JOB implementations while also synchronizing task‑log management.
Problem Analysis
The overall direction is straightforward: identify the required steps, then focus on concrete implementation details.
Implementation Details
Determine Whether to Enable XXL‑JOB
Like most third‑party starters, we use Spring Boot's auto‑configuration and read a property (e.g., xxl.job.enable ) to decide whether to register our custom classes. Different Spring Boot versions have slightly different property handling.
Auto‑configuration class:
/**
* Auto‑configuration class
*/
@Configuration
@ConditionalOnProperty(name = "xxl.job.enable", havingValue = "true")
@ComponentScan("com.teoan.job.auto.core")
public class XxlJobAutoConfiguration {
}If xxl.job.enable is false, nothing is assembled and Spring's native scheduler remains active.
Scanning and Reading Annotation Values
Spring Boot scans annotations such as @Service and @Component . We apply the same principle to scan @Scheduled methods after the application is ready.
Spring's @EventListener Annotation
Using @EventListener (optionally with @Async ) we can execute logic after the application is fully started without affecting startup time.
Pseudo‑code:
@Component
@Slf4j
public class JobAutoRegister {
@EventListener(ApplicationReadyEvent.class)
@Async
public void onApplicationEvent(ApplicationReadyEvent event) {
// Scan annotations and auto‑register XXL‑JOB tasks
}
}Scanning @Scheduled Methods
We retrieve beans annotated with @Component , then locate methods marked with @Scheduled to obtain their metadata.
private void addJobInfo() {
List
beanList = applicationContext.getBeansWithAnnotation(Component.class).values().stream().toList();
beanList.forEach(bean -> {
Map
annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(),
(MethodIntrospector.MetadataLookup
) method -> AnnotatedElementUtils.findMergedAnnotation(method, Scheduled.class));
annotatedMethods.forEach((k, v) -> {
// Stop Spring's native task
// Register task to XXL‑JOB
});
});
}Disabling Spring's Native Scheduler
The class ScheduledAnnotationBeanPostProcessor handles @Scheduled . By invoking its postProcessBeforeDestruction method we can cancel all tasks of a specific bean.
@Override
public void postProcessBeforeDestruction(Object bean, String beanName) {
Set
tasks;
synchronized (this.scheduledTasks) {
tasks = this.scheduledTasks.remove(bean);
}
if (tasks != null) {
for (ScheduledTask task : tasks) {
task.cancel();
}
}
}We pass the bean containing the @Scheduled method to this method to stop its native execution.
Registering Tasks to XXL‑JOB
After extracting annotation information, we convert it to XXL‑JOB's job definition and register it via the admin API.
Register JobHandler
XXL‑JOB registers a method‑mode handler via @XxlJob . The core registration method is:
protected void registJobHandler(XxlJob xxlJob, Object bean, Method executeMethod) {
if (xxlJob == null) return;
String name = xxlJob.value();
Class
clazz = bean.getClass();
String methodName = executeMethod.getName();
// validation omitted for brevity
executeMethod.setAccessible(true);
registJobHandler(name, new MethodJobHandler(bean, executeMethod, initMethod, destroyMethod));
}We reuse this logic to register the scanned @Scheduled method as a XXL‑JOB handler.
Auto‑Register Executor and Job Info
Since XXL‑JOB does not provide an OpenAPI like PowerJob, we call its HTTP admin endpoints to create executor groups and job entries.
public boolean autoRegisterGroup() {
String url = adminAddresses + "/jobgroup/save";
HttpRequest httpRequest = HttpRequest.post(url)
.form("appname", appName)
.form("title", title)
.form("addressType", addressType);
// additional logic omitted
HttpResponse response = httpRequest.cookie(jobLoginService.getCookie()).execute();
// check response code
return true;
}Job information is added similarly, with duplicate‑check logic based on handler name.
Redirecting Log Output to XXL‑JOB
XXL‑JOB provides XxlJobHelper for logging. By implementing a custom Logback Appender , we forward both normal and error logs to the XXL‑JOB console.
@Component
public class XxlJobLogAppender extends AppenderBase
{
@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
Adding the Starter Dependency
com.xuxueli
xxl-job-core
${xxl-job.version}
com.teoan
xxl-job-auto-spring-boot-starter
${project.version}Configuration in application.yml
server:
port: 8080
spring:
application:
name: xxlJobAuto
xxl:
job:
enable: true # switch between Spring and XXL‑JOB
accessToken:
admin:
addresses: http://localhost:8080/xxl-job-admin
username: admin
password: password
executor:
appname: ${spring.application.name}
addressType: 0 # 0 = auto register, 1 = manual list
title: ${spring.application.name}
logretentiondays: 3Spring Boot Configuration Class
@Configuration
@Slf4j
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;
}
}Defining a Sample @Scheduled Job
@Slf4j
@Component
public class XxlJobAutoSamplesJob {
@Scheduled(fixedRate = 10000)
public void samplesJob() {
log.info("samplesJob executor success!");
}
}Run the application with xxl.job.enable=false to use Spring's scheduler, then set it to true to see the tasks automatically registered in the XXL‑JOB console.
Considerations During Implementation
Updating Task Information
We decided not to implement dynamic updates of job metadata (e.g., cron changes) because most scenarios treat auto‑registration as a one‑time startup step; subsequent changes are performed directly in the XXL‑JOB console.
Database vs. HTTP Registration
Direct DB manipulation would require additional data‑source configuration and coupling, so we chose the HTTP API approach for better portability.
Adding @Scheduled to Auto‑Configuration Class
Although we could automatically add @Scheduled to the auto‑configuration class, we kept it optional to avoid unexpected side effects for users.
Starter Dependency on XXL‑JOB
The starter does not embed XXL‑JOB core dependencies, mirroring the design of optional Spring Boot starters.
Conclusion
This experiment demonstrates how to extend a middleware (XXL‑JOB) with a custom Spring Boot starter, providing dynamic task switching, automatic registration, and unified logging. The source code is open‑sourced on GitHub for anyone interested.
Project Repository
https://github.com/Teoan/xxl-job-auto
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.