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.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Switch Between Spring @Scheduled and XXL‑JOB Dynamically with a Configurable Starter

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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpring BootXXL-Jobauto-configurationscheduled tasksStarter
Code Ape Tech Column
Written by

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

0 followers
Reader feedback

How this landed with the community

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.