Optimizing Job Scheduling in Spring Boot with Quartz: Best Practices
In the realm of everyday engineering challenges, periodic execution of processes or jobs is a common necessity. While Spring Boot's straightforward @Scheduled annotation serves well in simpler scenarios, the complexity of certain use cases demands a more robust solution.
What does Quartz offer?
Quartz provides these robust job scheduling functionalities - precise time-based job execution, recurrent job runs, persistent job storage in databases, and seamless incorporation into the Spring framework.
Core interfaces in Quartz
Job
Definition: An interface in Quartz to be implemented by classes representing tasks or jobs that need to be executed.
Analogy: Think of it as a job role in an organization – a set of responsibilities waiting to be fulfilled.
JobDetails
Definition: A container holding the data that defines a specific job. Common properties include jobName, jobClass, jobDataMap, and a unique identity (key).
Analogy: Similar to drafting a job description – specifying the name of the role, the class that defines it, additional data, and a unique identifier.
Trigger
Definition: Configuration defining the schedule for a job, determining when it should be executed and how often.
Analogy: Imagine it as setting the schedule for a recurring task – specifying when it should kick off and at what intervals.
Scheduler
Definition: The main driver for Quartz, responsible for maintaining scheduling, listening for events, managing jobs, and handling triggers.
Analogy: The captain of the ship – overseeing and orchestrating everything, ensuring that tasks run smoothly according to their designated schedules.
The Flow
Imagine your software as a busy workspace where tasks need to be done regularly. Quartz is like the timekeeper that ensures everything runs smoothly. When you have a job (a specific task), you create a JobDetail, like a task description. Then, you set a Trigger, specifying when and how often the task should be done. Now, the Scheduler steps in, taking the JobDetail and Trigger, and makes sure the task gets done at the right time. It's like a manager handling a to-do list.
Here's the flow: You define a job, create its details, set when it should run, and hand it over to the Scheduler. The Scheduler, acting like your task manager, uses a threadpool to execute each job when it's time. Simple, right? Quartz simplifies the choreography of your software tasks, making sure they dance to the right tune at the right moment.
Let's Build
A very simple application to demonstrate the working of Quartz scheduling.
- Job Example: HelloWorldJob
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
public class HelloWorldJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("Hello, Quartz World!");
}
}
Explanation: This is a basic job class implementing the Quartz Job
interface. It defines the task to be executed when the job is triggered.
- JobDetail Configuration: JobConfig
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JobConfig {
@Bean
public JobDetail helloWorldJobDetail() {
return JobBuilder.newJob(HelloWorldJob.class)
.withIdentity("helloWorldJob")
.storeDurably()
.build();
}
}
Explanation: This configuration class defines a bean for the JobDetail
that specifies the characteristics of the HelloWorldJob
.
- Trigger Configuration: TriggerConfig
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.SimpleScheduleBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TriggerConfig {
@Bean
public Trigger helloWorldJobTrigger() {
return TriggerBuilder.newTrigger()
.forJob("helloWorldJob") // This refers to the identity of the job
// A group can also be provided in identity, by default DEFAULT group is used
// Groups are a good way to "group" the jobs for different use cases.
.withIdentity("helloWorldTrigger")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(5)
.withRepeatCount(9)) // Run 10 times (0-based index)
.build();
}
}
Explanation: This configuration class defines a bean for the Trigger
that determines when and how often the job should be executed.
- Scheduler Configuration: SchedulerConfig
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SchedulerConfig {
@Bean
public Scheduler scheduler() throws SchedulerException {
// Create a scheduler factory
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
// No need to start the scheduler here, it will be started when needed
return scheduler;
}
}
Explanation: This configuration class creates and starts the Quartz scheduler.
The explicit call to
scheduler.start()
is needed in scenarios where you want to start the scheduler outside the context of job scheduling, typically when configuring Quartz programmatically or when using Quartz standalone without Spring.In Spring applications with Quartz integration, especially when using the
SchedulerFactoryBean
, the scheduler is automatically started when the Spring context is initialized. This is due to theautoStartup
property of theSchedulerFactoryBean
, which is set totrue
by default.However, if you are not using Spring to manage the Quartz scheduler, or if you've set
autoStartup
tofalse
in the configuration, you would need to explicitly callscheduler.start()
to initiate the scheduling process.
- Main Application Class: QuartzDemoApplication
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class QuartzDemoApplication {
public static void main(String[] args) {
SpringApplication.run(QuartzDemoApplication.class, args);
}
}
Explanation: This is the main Spring Boot application class.
- Maven Dependency:
<!-- Add Quartz and Spring dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
Storage
By default, Quartz provides RAM or in-memory storage, meaning that the scheduler's state, including configured jobDetails, jobs that are currently being executed, etc., is stored in memory. As a result, on application restart, all the stored data is lost, and Quartz starts with a clean slate.
This in-memory storage is suitable for scenarios where you don't require persistent storage of job scheduling information and can afford to lose the scheduling state upon application restart.
However, Quartz supports persistent storage options like JDBC JobStore. When using JDBC JobStore, Quartz stores job scheduling information in a relational database, allowing the scheduler to recover its state after a restart. This ensures that scheduled jobs, triggers, and other related data persist across application sessions, providing durability and reliability in more complex use cases. To use JDBC JobStore, you need to configure Quartz to use a JDBC-backed datasource and set the appropriate properties in your configuration, including the JDBC URL, driver class, username, and password.
Bonus
Maven Dependency for JDBC Starter:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <version>2.4.1</version> </dependency>
This Maven dependency is included to use the Spring Boot starter for JDBC in the project.
Application Properties Configuration:
using.spring.schedulerFactory=true spring.quartz.job-store-type=jdbc spring.datasource.jdbc-url=jdbc:mysql://${MYSQL_HOST:localhost}:3306/quartz_schema spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver spring.datasource.username=root spring.datasource.password=root # A separate sql script containing all DDLs must be run once for this - can be found in documentation # spring.quartz.jdbc.initialize-schema=never
using.spring.schedulerFactory=true
: Indicates the usage of the SpringSchedulerFactory
.spring.quartz.job-store-type=jdbc
: Configures Quartz to use JDBC as the job store.
Quartz Configuration:
@Configuration @EnableAutoConfiguration public class QuartzConfiguration { @Bean @QuartzDataSource @ConfigurationProperties(prefix = "spring.datasource") public DataSource quartzDataSource() { return DataSourceBuilder.create().build(); } }
Additional Notes:
Schema Initialization Script: The comment mentions a separate SQL script containing all DDLs that must be run once for Quartz. This script initializes the necessary database schema for Quartz tables.
DTO Serialization: Any DTO being stored in the database must implement the
Serializable
interface to support serialization.
These configurations set up Quartz to use JDBC for storage, connecting to a MySQL database specified in the properties.
Conclusion
To sum it up, Quartz offers a powerful and flexible solution for job scheduling in Spring Boot applications. From precise time-based executions to recurrent job runs, Quartz seamlessly integrates into the Spring framework, providing the necessary tools for managing complex scheduling requirements. As you delve into the world of Quartz, consider exploring advanced features and customization options to tailor the scheduling behavior to your specific needs. Happy scheduling!