Introduce about Scheduled Tasks
In the world of Spring Boot, scheduling tasks is an essential part of managing various background processes. Quartz is a powerful library that simplifies task scheduling in Spring Boot applications. In this blog, we will explore the use of Quartz for scheduling tasks.
Why We Need Scheduled Tasks
Scheduled tasks play a crucial role in modern software applications. Here’s why they are indispensable:
- Automation: Scheduled tasks automate routine processes, reducing manual intervention and the risk of human error.
- Efficiency: They enable efficient resource utilization by running tasks at non-peak hours or during idle periods.
- Notification: Scheduled tasks can trigger notifications, alerts, and reports, keeping stakeholders informed.
- Maintenance: They aid in system maintenance, such as database cleanup, log rotation, and backups.
Foe example, imagine you have an e-commerce application, and you want to cancel those orders which are not paid in 15 minutes. You can’t ask consumers to use your API on time after 15 minutes. To achieve this, you can schedule a Job
which will start on time and cancel the order.
Of course, this is just the tip of the iceberg of its many uses.
Quartz Operation Mechanism
Basic Understanding
Before diving into the usage of Quartz, it’s essential to have a basic understanding of the three fundamental modules used in Quartz.
- Job: What need to be done
- Trigger: What time to do your jobs
- Scheduler: Link jobs and triggers
Processes
Here is the basic processes about how does a job been executed. We create a Job class implementing the Job
interface which can execute the JobExecuteContext
, create a JobDetail
instance and a Trigger instance, and create a Scheduler
instance. Then, bind the job to the trigger and send them to scheduler. When the scheduler starts and the set trigger time arrives, the job will be executed.
Besides, a job can be triggered by multiple triggers, and the job will be executed multiple times.
Basic Usage
Let’s start with the fundamental aspects of using Quartz in Spring Boot.
Import Maven Dependency
To get started, you need to include the Quartz dependency in your Spring Boot project. Just like other dependencies, you need to import the dependency in your pom.xml
file.
<!-- quartz -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
Job Class
Quartz jobs are at the heart of task scheduling. It is a class file implementing the Job interface. In this class, you are required to implement a method named “execute”.
For example:
public class YourJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("Your Job Is Been Executing");
}
}
Trigger
Triggers determine when and how often a job should run. I usually use SimpleTrigger
and CronTrigger
, which can cover the majority of my use cases.
The simple trigger uses date as the Date Object as the parameter, while the cron trigger uses cron expression as the parameter.
Here are the examples of them:
// Simple Trigger
Date date = new Date();
SimpleTrigger simpleTrigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity("simpleTriggerName", "simpleTriggerGroupName")
.startAt(new Date date.setTime(date.getTime() + 5000)) // 5s later
.build();
// Cron Trigger
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("CronTriggerName", "CronTriggerGroupName")
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 10 * * ?")) // Every day at 10 AM.
.build();
JobDetail
The creation of JobDetail
object is similar to trigger.
Here is the example:
JobDetail jobDetail = JobBuilder.newJob(YourJob.class)
.withIdentity("JobDetailName", "JobDetailGroupName")
.build();
Result
Finally, let’s put the job and the trigger into scheduler.
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.start();
scheduler.scheduleJob(jobDetail, simpleTrigger);
If the scheduler
is running in the ApplicationTests
, you should add these code below, unless the scheduler
will stop with the ApplicationTests
.
try {
Thread.sleep(6000); // slepp 6s
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Then, you will see Your Job Is Been Executing
in the terminal.
Advanced Usage
Once you’ve understood the basic part, it’s time to explore the advanced Quartz usage.
Encapsulation
Learn how to encapsulate your Quartz and make your code more maintainable and organized.
In our recent project, we encapsulated some methods we needed into SchedulerUtil
, like addJob
, resetJobTrigger
, removeJob
, isJobExist
, and so on.
Here is the simple version of our SchedulerUtil
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.KeyMatcher;
import org.quartz.impl.triggers.CronTriggerImpl;
import org.springframework.lang.Nullable;
import java.util.Date;
@Slf4j
public class SchedulerUtil {
private static final StdSchedulerFactory schedulerFactory = new StdSchedulerFactory();
public static Scheduler getScheduler() throws SchedulerException {
return schedulerFactory.getScheduler();
}
public static void addJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName, Class<? extends org.quartz.Job> jobClass, @Nullable JobDataMap jobDataMap, String cron) throws SchedulerException {
Scheduler scheduler = getScheduler();
JobDetail jobDetail = JobBuilder.newJob(jobClass)
.withIdentity(jobName, jobGroupName)
.setJobData(jobDataMap)
.build();
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity(triggerName, triggerGroupName)
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule(cron))
.build();
scheduler.scheduleJob(jobDetail, trigger);
}
public static void addJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName, Class<? extends org.quartz.Job> jobClass, @Nullable JobDataMap jobDataMap, Date date) throws SchedulerException {
Scheduler scheduler = getScheduler();
JobDetail jobDetail = JobBuilder.newJob(jobClass)
.withIdentity(jobName, jobGroupName)
.setJobData(jobDataMap)
.build();
SimpleTrigger simpleTrigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity(triggerName, triggerGroupName)
.startAt(date)
.build();
scheduler.scheduleJob(jobDetail, simpleTrigger);
}
public static void resetJobTrigger(String triggerName, String triggerGroupName, String cron) throws Exception {
Scheduler scheduler = getScheduler();
TriggerKey triggerKey = new TriggerKey(triggerName, triggerGroupName);
CronTriggerImpl trigger = (CronTriggerImpl) scheduler.getTrigger(triggerKey);
if (!trigger.getCronExpression().equalsIgnoreCase(cron)) {
trigger.setCronExpression(cron);
scheduler.rescheduleJob(triggerKey, trigger);
}
}
public static void resetJobTrigger(String triggerName, String triggerGroupName, Date date) throws Exception {
Scheduler scheduler = getScheduler();
TriggerKey triggerKey = new TriggerKey(triggerName, triggerGroupName);
SimpleTrigger trigger = (SimpleTrigger) scheduler.getTrigger(triggerKey);
if (!trigger.getStartTime().equals(date)) {
SimpleTrigger simpleTrigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity(triggerKey)
.startAt(date)
.build();
scheduler.rescheduleJob(triggerKey, simpleTrigger);
}
}
public static void removeJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName) throws SchedulerException {
Scheduler scheduler = getScheduler();
if (!isJobExist(jobName, jobGroupName, triggerName, triggerGroupName)) {
log.info("Job: {} is not exist.", jobName);
return;
}
TriggerKey triggerKey = new TriggerKey(triggerName, triggerGroupName);
scheduler.pauseTrigger(triggerKey);
scheduler.unscheduleJob(triggerKey);
scheduler.deleteJob(new JobKey(jobName, jobGroupName));
}
public static Boolean isJobExist(String jobName, String jobGroupName, String triggerName, String triggerGroupName) throws SchedulerException {
Scheduler scheduler = getScheduler();
return scheduler.checkExists(new JobKey(jobName, jobGroupName)) && scheduler.checkExists(new TriggerKey(triggerName, triggerGroupName));
}
}
In our project, it is not allowed to directly create JobDetail
or any kinds of trigger object in the service classes, because they will make the original business code verbose. In addition, we employ method polymorphism, which makes the use of SchedulerUtil
more concise and user-friendly.
Start with Spring Boot
In our project, we have nothing to do with manual operation and every job is under control. As a result, let the Quartz scheduler start with Spring Boot Application can be convenient.
In the project, I write the starter in SchedulerConfig
.
@Slf4j
@Configuration
public class SchedulerConfig implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
try {
StdSchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.start();
} catch (SchedulerException e) {
throw new RuntimeException();
}
}
}
When your Spring Boot Application starts, it will run the config class and start your scheduler instance.
Persistence with MySQL
To make your scheduled tasks more resilient, Quartz can store its tasks and triggers into database, like MySQL. For example, your application is going to update and has to restart, but some jobs are still in the scheduler and are stored in your RAM. When you shut down your application, all of the jobs will lose.
Before you start to store your jobs into your disks, you need to create a few tables in your database and improve the relative JDBC
dependency. You can find the SQL file for your database here -> jdbcjobstore
Secondly, write Quartz properties and I prefer to use yml
.
spring:
datasource:
quartz:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/quartz
username: root
password: 123456
quartz:
job-store-type: jdbc
scheduler-name: MyScheduler
wait-for-jobs-to-complete-on-shutdown: false
jdbc:
initialize-schema: never
properties:
org:
quartz:
jobStore:
dataSource: quartzDataSource
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_
threadPool:
threadCount: 25 # default 10 。
threadPriority: 5
class: org.quartz.simpl.SimpleThreadPool
Then, complete the SchedulerConfig
configuration class.
@Configuration
@EnableScheduling
public class SchedulerConfig {
@Value("${spring.datasource.url}")
private String dataSourceUrl;
@Value("${spring.datasource.username}")
private String dataSourceUsername;
@Value("${spring.datasource.password}")
private String dataSourcePassword;
@Value("${spring.datasource.driver-class-name}")
private String dataSourceDriverClassName;
@Value("${spring.quartz.scheduler-name}")
private String schedulerName;
@Value("${spring.quartz.wait-for-jobs-to-complete-on-shutdown}")
private boolean waitForJobsToCompleteOnShutdown;
@Bean
public DataSource quartzDataSource() {
DataSource dataSource = DataSourceBuilder.create()
.driverClassName(dataSourceDriverClassName)
.url(dataSourceUrl)
.username(dataSourceUsername)
.password(dataSourcePassword)
.build();
return dataSource;
}
@Bean
public SchedulerFactoryBean schedulerFactoryBean(@Qualifier("quartzDataSource") DataSource quartzDataSource) {
SchedulerFactoryBean factoryBean = new SchedulerFactoryBean();
factoryBean.setDataSource(quartzDataSource);
factoryBean.setQuartzProperties(quartzProperties());
return factoryBean;
}
@Bean
public Scheduler scheduler(@Qualifier("schedulerFactoryBean") SchedulerFactoryBean schedulerFactoryBean) throws SchedulerException {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
scheduler.start();
return scheduler;
}
private Properties quartzProperties() {
Properties properties = new Properties();
properties.setProperty("org.quartz.jobStore.dataSource", "quartzDataSource");
properties.setProperty("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
properties.setProperty("org.quartz.jobStore.tablePrefix", "QRTZ_");
properties.setProperty("org.quartz.threadPool.threadCount", "25");
properties.setProperty("org.quartz.threadPool.threadPriority", "5");
return properties;
}
}
Here, most of the configuration is fixed, but there may be occasional changes with Quartz updates. Please refer to the official documentation for accuracy.
The End
In conclusion, Quartz is a powerful tool for scheduling tasks in Spring Boot, offering both basic and advanced features to suit your needs. Whether you’re scheduling simple periodic tasks or complex, dynamic workflows, Quartz can help you achieve reliable and efficient job scheduling.