Spring's built-in @Scheduled annotation handles simple scheduling but lacks visual monitoring, distributed execution, and centralized control for production workloads. SchedulerX takes over your existing Spring scheduled jobs without modifying business logic, and provides execution chain tracking, log queries, and alerting through the MSE console.
Prerequisites
Before you begin, make sure that you have:
SchedulerX client version 1.8.13 or later
A Spring Boot application connected to SchedulerX. For details, see Connect a Spring Boot application to SchedulerX
Add the Maven dependency
Add the schedulerx2-spring-boot-starter dependency to your pom.xml. Replace ${schedulerx2.version} with the latest version from the agent release notes.
<dependency>
<groupId>com.aliyun.schedulerx</groupId>
<artifactId>schedulerx2-spring-boot-starter</artifactId>
<version>${schedulerx2.version}</version>
<!-- Exclude Log4j dependencies if you use Logback -->
<exclusions>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>Regardless of whether you use scheduled Spring jobs for the first time, keep the @EnableScheduling annotation enabled in your main class:
@SpringBootApplication
@EnableScheduling // Enable scheduled Spring jobs
public class SchedulerXWorkerApplication {
public static void main(String[] args) {
SpringApplication.run(SchedulerXWorkerApplication.class, args);
}
}
@Service
public class SpringScheduledProcessor {
@Scheduled(cron = "0/2 * * * * ?")
public void hello() {
logger.info(DateUtil.now() + " hello world. start");
logger.info(DateUtil.now() + " hello world. end");
}
}At this point, SchedulerX does not take over your existing Spring scheduled jobs. They continue to run under the Spring container.
Configure SchedulerX to take over Spring jobs
Add the following properties to your application configuration file.
Required: application access and takeover
# Application access
spring.schedulerx2.endpoint=${endpoint}
spring.schedulerx2.namespace=${namespace}
spring.schedulerx2.groupId=${groupId}
spring.schedulerx2.appKey=${appKey}
# Enable SchedulerX to take over Spring scheduled jobs
spring.schedulerx2.task.scheduling.scheduler=schedulerxTo get the access parameters, log on to the MSE console and go to the SchedulerX Version page. In the left-side navigation pane, click Applications. Find your application and click Access Config in the Operation column. If this is your first time connecting Spring jobs to SchedulerX, create an application first.
Optional: automatic job synchronization
If you have many existing @Scheduled jobs, enable automatic synchronization to avoid creating each job manually:
spring.schedulerx2.task.scheduling.sync=true
spring.schedulerx2.regionId=<region-id>
spring.schedulerx2.aliyunAccessKey=<your-access-key>
spring.schedulerx2.aliyunSecretKey=<your-secret-key>| Placeholder | Description | Example |
|---|---|---|
<region-id> | Region ID for job synchronization. For details, see Endpoints. | cn-hangzhou |
<your-access-key> | Alibaba Cloud AccessKey ID | LTAI5tXxx |
<your-secret-key> | Alibaba Cloud AccessKey Secret | xXxXxXx |
To guarantee that an auto-synchronized job adheres to the identical execution pattern as native Spring jobs in a cluster, auto-synchronized jobs default to Broadcast run execution mode, which runs the job on every worker node simultaneously. To run a job on only one node, change the Execution mode to Stand-alone operation in the SchedulerX console. See Create a scheduled job manually for parameter details.
Create a scheduled job manually
Skip the manual creation steps below if you enabled automatic job synchronization.
Log on to the MSE console and go to the SchedulerX Version page.
In the left-side navigation pane, click Tasks.
On the Tasks page, click Create task. In the Create task panel, select SpringSchedule from the Task type drop-down list and enter the class name and method name.
Parameter Description Task name The name of the job. Description A description to help with searching and managing jobs. Application ID The application group that the job belongs to. Select a value from the drop-down list. Task type The job type. Valid values: Java, Shell, Python, Golang, Http, Node.js, XXL-JOB, and DataWorks. Set this to SpringSchedule for Spring scheduled jobs. spring scheduleConfiguration The fully qualified class name and method name of the job. Execution mode How the job runs across the cluster. Stand-alone operation runs the job on one random worker. Broadcast run runs the job on all workers simultaneously, and the system waits until all workers complete the job. The advanced configuration options vary based on the selected execution mode. Priority The job priority within the same application. Higher-priority jobs run first when multiple jobs compete for resources on the same worker. However, if multiple jobs run on different workers, jobs with lower priorities may run first because they are scheduled to different workers. SchedulerX uses preemptible queues to enforce priority ordering. For details, see Application-level throttling through job priority queues. Task parameters A custom string accessible from the job context at runtime. Configure the trigger frequency.
NoteThe trigger frequency set in the SchedulerX console takes precedence over the
@Scheduledannotation in your code. The annotation remains in the code but has no effect once SchedulerX manages the job.Parameter Description Time type The trigger mechanism: none (triggered by a workflow), cron (cron expression), api (triggered by API call), fixed_rate (fixed interval), second_delay (1--60 second delay), one_time (runs once at a specified time). cron expression The cron expression for scheduling. Enter one manually or use the built-in cron generator. Fixed frequency The interval in seconds. Available only when Time type is fixed_rate. Must be greater than 60. Fixed delay The delay in seconds (1--60). Available only when Time type is second_delay. The following table describes the advanced timing parameters.
Parameter Description Time offset The offset between the data processing timestamp and the job trigger time. Accessible from the job context at runtime. Time zone The time zone for scheduling. Select a country or region, or a GMT offset. Calendar Select Workday or Financial day to apply calendar-based scheduling. Configure alert conditions and notification methods. For details, see Notification contacts and notification contact groups.
After configuration, SchedulerX manages your Spring scheduled jobs with visual management, job log queries, execution chain views, and alert notifications.
Verify the connection
Start your Spring application. In the MSE console, go to the SchedulerX Version page and click Applications in the left-side navigation pane. Check the Total number of instances column for your application. A value greater than 0 confirms that the application is connected.
In the left-side navigation pane, click Tasks. Find the job for your application and click Run once in the Operation column. A successful execution confirms that the setup is complete.
FAQ
Why does the original Spring scheduler still run after SchedulerX takes over?
This happens when your application defines a custom scheduler. Check whether any class implements org.springframework.scheduling.annotation.SchedulingConfigurer and calls setScheduler on ScheduledTaskRegistrar. If so, comment out that code so SchedulerX can replace the scheduler.
How do I access the job context in a Spring job?
Retrieve the JobContext from the container pool:
JobContext jobContext = ContainerFactory.getContainerPool().getContext();Can a Spring job return a processing result?
Yes, if the agent version is later than 1.10.11. Return either a ProcessResult object or a String:
@Scheduled(cron = "0/5 * * * * ?")
public ProcessResult helloStandalone1() {
try {
logger.info(DateUtil.now() + " " + Thread.currentThread().getName() + " hello world. start");
TimeUnit.SECONDS.sleep(2L);
logger.info(DateUtil.now() + " " + Thread.currentThread().getName() + " hello world. end");
} catch (Exception e) {
e.printStackTrace();
logger.info(DateUtil.now() + " " + Thread.currentThread().getName() + " hello world. exception end..");
}
return new ProcessResult(true, "Processing result");
}
@Scheduled(cron = "0/5 * * * * ?")
public String helloStandalone2() {
try {
logger.info(DateUtil.now() + " " + Thread.currentThread().getName() + " hello world. start");
TimeUnit.SECONDS.sleep(2L);
logger.info(DateUtil.now() + " " + Thread.currentThread().getName() + " hello world. end");
} catch (Exception e) {
e.printStackTrace();
logger.info(DateUtil.now() + " " + Thread.currentThread().getName() + " hello world. exception end..");
}
return "Processing result";
}Why does the original Spring timer still run after SchedulerX takes over scheduled Spring jobs?
If a custom scheduler is specified in your application, SchedulerX overwrites the custom scheduler. Check whether the class that implements the org.springframework.scheduling.annotation.SchedulingConfigurer interface exists in your application project, and whether the setScheduler method of ScheduledTaskRegistrar is called to overwrite the default scheduler. If the class exists or the default scheduler is overwritten, comment out the related code.
How do I obtain the context for a Spring job?
Add the following code to your application project code to obtain the context:
JobContext jobContext = ContainerFactory.getContainerPool().getContext();Does a Spring job return a processing result?
If the agent version is later than 1.10.11, Spring jobs can return processing results. The processing results are returned based on the specified scheduling methods.
@Scheduled(cron = "0/5 * * * * ?")
public ProcessResult helloStandalone1() {
try {
logger.info(DateUtil.now() + " " + Thread.currentThread().getName() + " hello world. start");
TimeUnit.SECONDS.sleep(2L);
logger.info(DateUtil.now() + " " + Thread.currentThread().getName() + " hello world. end");
} catch (Exception e) {
e.printStackTrace();
logger.info(DateUtil.now() + " " + Thread.currentThread().getName() + " hello world. exception end..");
}
return new ProcessResult(true, "Processing result");
}
@Scheduled(cron = "0/5 * * * * ?")
public String helloStandalone2() {
try {
logger.info(DateUtil.now() + " " + Thread.currentThread().getName() + " hello world. start");
TimeUnit.SECONDS.sleep(2L);
logger.info(DateUtil.now() + " " + Thread.currentThread().getName() + " hello world. end");
} catch (Exception e) {
e.printStackTrace();
logger.info(DateUtil.now() + " " + Thread.currentThread().getName() + " hello world. exception end..");
}
return "Processing result";
}