Backend Development 25 min read

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.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Flexible Switching Between Spring @Scheduled and XXL‑JOB for Scheduled Tasks

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: 3

Spring 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

backendJavaloggingSpringBootxxl-jobAutoConfigurationScheduledTasks
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.