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 the autoStartup property of the SchedulerFactoryBean, which is set to true by default.

However, if you are not using Spring to manage the Quartz scheduler, or if you've set autoStartup to false in the configuration, you would need to explicitly call scheduler.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 Spring SchedulerFactory.

    • 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!