StackTips
 15 minutes

Working with Quartz Scheduler in Spring Boot

By Nilanchala @nilan, On Feb 23, 2024 Spring Boot 671 Views

Quartz Scheduler is an open-source job scheduling library that allows developers to schedule jobs to run at a certain time or based on specific events. This library eliminates the limitations in the Spring scheduler.

Quartz allows developers to:

  • Schedule jobs to run at a specific time, or repeat at intervals - Store jobs and their triggers in a database, allowing scheduled jobs to persist between application restarts.
  • Run in a clustered environment, allowing jobs to be distributed across a cluster of servers for load balancing or redundancy.
  • Allows transaction management to ensure the jobs are only executed after transactions are successfully committed.

Using Quartz Scheduler Spring Boot

In the course of this article, we will cover different aspects of the Quartz Scheduler and integrate it with Spring Boot application.

Let us first create a simple scheduler to import content from a CSV file using basic configurations. The sample CSV file containing list of books.

Quartz Starter Dependency

Before we begin, we need to add the Quartz starter dependency to our Spring boot application. For Maven add the following dependency to your pom.xml file.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

If you're using Gradle build tool, you can add the the following to your build.gradle file:

implementation 'org.springframework.boot:spring-boot-starter-quartz'`

The CSV content maps to the following Book model. Please note, I am using Java17 record for this purpose. If you're using lower version of Java, you can write a simple POJO class.

public record Book(  
        String id,  
        String isbn,  
        String isbn13,  
        String authors,  
        String publicationYear,  
        String title,  
        String languageCode,  
        Double averageRating,  
        String imageUrl) {  
}

To keep the scope of this article limited to the task scheduler, we will not perform any database operations. Here is how our ImportService looks like:

@Service  
public class ImportService {  

    private static final Logger log = LoggerFactory.getLogger(ImportService.class);  

    public void readBooks() throws IOException, CsvException {  
        File file = new File("src/data/books.csv");  
        log.info("Importer started!");  

        try (CSVReader csvReader = new CSVReader(new FileReader(file))) {  
            final List<String[]> rows = csvReader.readAll();  
            List<Book> books = rows.stream()  
                    .skip(1)  
                    .map(row -> new Book(row[0], row[1], row[2], row[3], row[4],  
                            row[5], row[6], Double.parseDouble(row[7]), row[8]))  
                    .toList();  
            log.info("Imported {} books", books.size());  
        }  

        log.info("Importer completed!");  
    }  
}

The ImporterService is responsible for reading the CSV file, you can extend this service to do whatever logic required for your application.

Let us now dive into Quartz scheduler.

Creating Quartz Job

Define a job class that implements the Quartz Job interface. This class will contain the logic for reading the CSV file and loading its contents into the database.

public class CsvImportJob implements Job {

    private static final Logger log = LoggerFactory.getLogger(CsvImportJob.class);
    private final ImportService importService;

    public CsvImportJob(ImportService importService) {
        this.importService = importService;
    }

    @Override  
    public void execute(JobExecutionContext context) throws JobExecutionException {
        try {
            JobDataMap dataMap = context.getJobDetail().getJobDataMap();
            String param = dataMap.getString("arg1");

            log.info("CsvImportJob started with param: {}", param);
            importService.readBooks();

        } catch (IOException e) {
            log.error("IOException thrown while running job", e);
            throw new RuntimeException(e);
        } catch (CsvException e) {
            log.error("CsvException thrown while running job", e);
            throw new RuntimeException(e);
        }
    }  
}

Cron Based Trigger

Now that we have created the CsvImportJob, we can leverage the Spring's configuration and dependency injection capability to trigger the job.

The spring-boot-starter-quartz dependency automatically configures a SchedulerFactoryBean.

The SchedulerFactoryBean is a Quartz's standard factory implementation that is responsible for creating a Scheduler instance.

@Configuration  
public class QuartzConfig {  

    @Bean
    public JobDetail csvImportJob() {
        return JobBuilder.newJob(CsvImportJob.class)
                .withIdentity("csvImportJob")          
                .build();
    }  

    @Bean
    public Trigger csvImportJobTrigger(JobDetail csvImportJob) {
        return TriggerBuilder.newTrigger()
                .forJob(csvImportJob)
                .withIdentity("cronTrigger")
                .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?"))
                .build();
    }
}

The Quartz Scheduler is aware of all JobDetail and Trigger beans defined in the Spring context and automatically schedules the jobs based on the triggers associated with them.

We have used cron expression that triggers the job every 5 seconds ("0/5 * * * * ?").

Interval Based Trigger

Along with the cron trigger, we can also schedule job to start at a specific moment in time, and optionally, repeat it at a specified interval a fixed number of times using the SimpleScheduleBuilder.

When defining a interval based trigger, we need to specify the start time, repeat interval, and optionally the number of repeats.

@Bean  
    public Trigger csvImportJobTrigger(JobDetail csvImportJob) {  
        // Initial delay
        Date afterFiveSeconds = Date.from(LocalDateTime.now().plusSeconds(5)  
                .atZone(ZoneId.systemDefault()).toInstant());  

        return TriggerBuilder.newTrigger()  
                .forJob(csvImportJob)  
                .startAt(afterFiveSeconds) // Initial delay 
                .withIdentity("simpleTrigger") 
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()  
                        .withIntervalInSeconds(60)  
                        .repeatForever())  
                .build();  
    }

[!NOTE] If you're using the Job interface for tasks, these jobs cannot be interrupted before completion. Once started, these jobs will run to completion unless an unhandled exception occurs.

Interruptable Jobs in Quartz

The InterruptableJob interface extends the Job interface and adds the ability for the job to be interrupted in the middle of execution. This is useful for long-running tasks that might need to be stopped before they complete normally.

To create an Interruptible Job, we need to implement the InterruptableJob interface and implement the interrupt() method. The interrupt() method is called by the Quartz Scheduler when a user interrupts the Job.

public class CsvImportInterruptableJob implements InterruptableJob {  

    private static final Logger log = LoggerFactory.getLogger(CsvImportInterruptableJob.class);  

    private final ImportService importService;  

    public CsvImportInterruptableJob(ImportService importService) {  
        this.importService = importService;  
    }  

    private volatile boolean toStop = false;  

    @Override  
    public void execute(JobExecutionContext context) throws JobExecutionException {  
        while (!toStop) {  

            try {  
                JobDataMap dataMap = context.getJobDetail().getJobDataMap();  
                String param = dataMap.getString("param");  

                log.info("CsvImportInterruptableJob started with parameter: {}", param);  
                importService.readBooks();  

            } catch (IOException e) {  
                log.error("IOException thrown while running job", e);  
                throw new RuntimeException(e);  
            } catch (CsvException e) {  
                log.error("Exception thrown while running job", e);  
                throw new RuntimeException(e);  
            }  

            if (Thread.interrupted()) {  
                // Perform any cleanup tasks and terminate  
                toStop = true;  
            }  
        }  

    }  

    @Override  
    public void interrupt() throws UnableToInterruptJobException {  
        toStop = true;  
    }  
}

The interrupt() method allows the job to handle interruption requests, such as cleaning up resources or rolling back transactions, before the job stops.

This article covers the basics of Scheduler using Quartz. In the next article we will cover more advance configurations and dynamic scheduling using Spring Boot actuators.

For the complete project source code check out the download link.

If you have any questions, write down in the comment section below. Will be happy to respond, soon as I can.

nilan avtar

Nilanchala

I'm a blogger, educator and a full stack developer. Mainly focused on Java, Spring and Micro-service architecture. I love to learn, code, make and break things.