How to Schedule Jobs With Quartz in Spring Boot

Written by yaf | Published 2022/07/24
Tech Story Tags: java | spring | task-scheduler | spring-boot | spring-framework | framework | programming | java-programming

TLDRQuartz is the de facto standard of scheduling libraries for Java applications. Quartz supports running jobs at a particular time, repeating job executions, storing jobs in a database, and Spring integration. The easiest way to use Quartz in Spring applications is to use the `@Scheduled` annotation. For the Quartz application to work, you need to add a configuration with the @EnableScheduling annotation. The result will be a text output in the console every five seconds. To work with Quartz, it is not necessary to define a configuration.via the TL;DR App

In this article, we will look at how to schedule tasks using the Quartz framework. Quartz is the de facto standard of scheduling libraries for Java applications. Quartz supports running jobs at a particular time, repeating job executions, storing jobs in a database, and Spring integration.

Spring-annotations for scheduling

The easiest way to use Quartz in Spring applications is to use the @Scheduled annotation. Next, we will consider an example of a Spring Boot application. Let's add the necessary dependency in build.gradle

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

and consider an example

package quartzdemo.tasks;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class PeriodicTask {

    @Scheduled(cron = "0/5 * * * * ?")
    public void everyFiveSeconds() {
        System.out.println("Periodic task: " + new Date());
    }

}

Also, for the @Scheduled annotation to work, you need to add a configuration with the @EnableScheduling annotation.

package quartzdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class QuartzDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(QuartzDemoApplication.class, args);
    }

}

The result will be a text output in the console every five seconds.

Periodic task: Thu Jul 07 18:24:50 EDT 2022
Periodic task: Thu Jul 07 18:24:55 EDT 2022
Periodic task: Thu Jul 07 18:25:00 EDT 2022
...

The @Scheduled annotation supports the following parameters:

  • fixedRate - Allows you to run a task at a specified fixed interval.

  • fixedDelay - Execute a task with a fixed delay between the completion of the last invocation and the start of the next.

  • initialDelay - The parameter is used with fixedRate and fixedDelay to wait before the first execution of the task with the specified delay.

  • cron - Set the task execution schedule using the cron-string. Also supports macros @yearly (or @annually), @monthly, @weekly, @daily (or @midnight), and @hourly.

By default, fixedRate, fixedDelay and initialDelay are set in milliseconds. This can be changed using the timeUnit parameter, setting the value from NANOSECONDS to DAYS.

Furthermore, you can use properties in @Scheduled annotation:

application.properties

cron-string=0/5 * * * * ?

PeriodicTask.java

@Component
public class PeriodicTask {

    @Scheduled(cron = "${cron-string}")
    public void everyFiveSeconds() {
        System.out.println("Periodic task: " + new Date());
    }

}

To use properties, you can utilize fixedRateString, fixedDelayString, and initialDelayString parameters instead of fixedRate, fixedDelay and initialDelay accordingly.

Directly use Quartz

In the previous example, we executed scheduled tasks, but at the same time, we could not dynamically set the start time of the job, or pass parameters to it. To solve these problems, you can directly use Quartz.

The main Quartz interfaces are listed below:

  • Job is an interface to be implemented by the classes that contain the business logic that we wish to have executed

  • JobDetails defines Job instances and data that are related to it

  • Trigger describes the schedule of job execution

  • Scheduler is the main Quartz interface that provides all manipulation and searching operations for jobs and triggers

To work with Quartz directly, it is not necessary to define a configuration with the @EnableScheduling annotation, and instead of the org.springframework.boot:spring-boot-starter-quartz dependency, you can use org.quartz-scheduler:quartz.

Let's define SimpleJob class:

package quartzdemo.jobs;

import org.quartz.Job;
import org.quartz.JobExecutionContext;

import java.text.MessageFormat;

public class SimpleJob implements Job {

    @Override
    public void execute(JobExecutionContext context) {
        System.out.println(MessageFormat.format("Job: {0}", getClass()));
    }

}

To implement Job interface, you need to implement only one execute method that accepts a parameter of JobExecutionContext type. JobExecutionContext contains information about the job instance, trigger, scheduler, and other information about the job execution.

Now let's define an instance of the job:

JobDetail job = JobBuilder.newJob(SimpleJob.class).build();

And create a trigger that will trigger after five seconds:

Date afterFiveSeconds = Date.from(LocalDateTime.now().plusSeconds(5)
        .atZone(ZoneId.systemDefault()).toInstant());
Trigger trigger = TriggerBuilder.newTrigger()
        .startAt(afterFiveSeconds)
        .build();

Also, create a scheduler:

SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();

The Scheduler is initialized in “stand-by” mode, so we have to invoke start method:

scheduler.start();

Now we can schedule the execution of the job:

scheduler.scheduleJob(job, trigger);

Farther, when creating JobDetails, let's add additional data and use it when executing a Job:

QuartzDemoApplication.java

@SpringBootApplication
public class QuartzDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(QuartzDemoApplication.class, args);
        onStartup();
    }

    private static void onStartup() throws SchedulerException {
        JobDetail job = JobBuilder.newJob(SimpleJob.class)
                .usingJobData("param", "value") // add a parameter
                .build();

        Date afterFiveSeconds = Date.from(LocalDateTime.now().plusSeconds(5)
                .atZone(ZoneId.systemDefault()).toInstant());
        Trigger trigger = TriggerBuilder.newTrigger()
                .startAt(afterFiveSeconds)
                .build();

        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Scheduler scheduler = schedulerFactory.getScheduler();
        scheduler.start();
        scheduler.scheduleJob(job, trigger);
    }
}

SimpleJob.java

public class SimpleJob implements Job {

    @Override
    public void execute(JobExecutionContext context) {
        JobDataMap dataMap = context.getJobDetail().getJobDataMap();
        String param = dataMap.getString("param");
        System.out.println(MessageFormat.format("Job: {0}; Param: {1}",
                getClass(), param));
    }

}

It is important to note that all values that are added to JobDataMap must be serializable.

JobStore

Quartz stores data about JobDetail, Trigger, and other information in JobStore. By default, the in-memory JobStore is used. This means that if we have scheduled tasks and shut down the application (for example, when restarting or crashing) before they are fired, then they will never be executed again. Quartz also supports JDBC-JobStore for storing information in a database.

Before using JDBC-JobStore, it is necessary to create tables in the database that Quartz will use. By default, these tables are prefixed with QRTZ_.

The Quartz source code contains SQL scripts for creating tables for various databases, such as Oracle, Postgres, MS SQL Server, MySQL, and others, and also has a ready-made XML file for Liquibase.

Also, QRTZ tables can be automatically created when launching the application by specifying the spring.quartz.jdbc.initialize-schema=always property.

For simplicity, we will use the second method and the H2 database. Let's configure a data source, use JDBCJobStore and create QRTZ tables in application.properties:

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

spring.quartz.job-store-type=jdbc
spring.quartz.jdbc.initialize-schema=always

For these settings to be taken into account, the Scheduler must be created as a Spring bean:

package quartzdemo;

import org.quartz.*;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import quartzdemo.jobs.SimpleJob;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;

@SpringBootApplication
@EnableScheduling
public class QuartzDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(QuartzDemoApplication.class, args);
    }

    @Bean()
    public Scheduler scheduler(SchedulerFactoryBean factory) throws SchedulerException {
        Scheduler scheduler = factory.getScheduler();
        scheduler.start();
        return scheduler;
    }

    @Bean
    public CommandLineRunner run(Scheduler scheduler) {
        return (String[] args) -> {
            JobDetail job = JobBuilder.newJob(SimpleJob.class)
                    .usingJobData("param", "value") // add a parameter
                    .build();

            Date afterFiveSeconds = Date.from(LocalDateTime.now().plusSeconds(5)
                    .atZone(ZoneId.systemDefault()).toInstant());
            Trigger trigger = TriggerBuilder.newTrigger()
                    .startAt(afterFiveSeconds)
                    .build();

            scheduler.scheduleJob(job, trigger);
        };
    }

}

Thread pool configuration

Quartz runs each task in a separate thread, and you can configure a thread pool for schedulers. It should also be noted that by default, tasks launched via the @Scheduled annotation and directly via Quartz are launched in different thread pools. We can make sure of this:

PeriodicTask.java

@Component
public class PeriodicTask {

    @Scheduled(cron = "${cron-string}")
    public void everyFiveSeconds() {
        System.out.println(MessageFormat.format("Periodic task: {0}; Thread: {1}",
                new Date().toString(), Thread.currentThread().getName()));
    }

}

SimpleJob.java

public class SimpleJob implements Job {

    @Override
    public void execute(JobExecutionContext context) {
        JobDataMap dataMap = context.getJobDetail().getJobDataMap();
        String param = dataMap.getString("param");
        System.out.println(MessageFormat.format("Job: {0}; Param: {1}; Thread: {2}",
                getClass(), param, Thread.currentThread().getName()));
    }

}

the output will be:

Periodic task: Thu Jul 07 19:22:45 EDT 2022; Thread: scheduling-1
Job: class quartzdemo.jobs.SimpleJob; Param: value; Thread: quartzScheduler_Worker-1
Periodic task: Thu Jul 07 19:22:50 EDT 2022; Thread: scheduling-1
Periodic task: Thu Jul 07 19:22:55 EDT 2022; Thread: scheduling-1
Periodic task: Thu Jul 07 19:23:00 EDT 2022; Thread: scheduling-1

The thread pool for @Scheduled tasks contain only one thread.

Let's change the scheduler settings for @Scheduled tasks:

package quartzdemo;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

@Configuration
public class SchedulingConfiguration implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();

        threadPoolTaskScheduler.setPoolSize(10);
        threadPoolTaskScheduler.setThreadNamePrefix("my-scheduled-task-pool-");
        threadPoolTaskScheduler.initialize();

        taskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
    }

}

the output will now be like this:

Periodic task: Thu Jul 07 19:44:10 EDT 2022; Thread: my-scheduled-task-pool-1
Job: class quartzdemo.jobs.SimpleJob; Param: value; Thread: quartzScheduler_Worker-1
Periodic task: Thu Jul 07 19:44:15 EDT 2022; Thread: my-scheduled-task-pool-1
Periodic task: Thu Jul 07 19:44:20 EDT 2022; Thread: my-scheduled-task-pool-2

As you can see, these settings affected only the tasks set using annotations.

Now let's change the settings of the scheduler that we work with Quartz directly. This can be done in two ways: through the properties file or by creating a bean SchedulerFactoryBeanCustomizer.

Let's use the first method. If we hadn't initialized Quartz via Spring, we would have to register properties in the quartz.properties file. In our case, we need to register properties in application.properties, adding the prefix spring.quartz.properties. to them.

application.properties

spring.quartz.properties.org.quartz.threadPool.threadNamePrefix=my-scheduler_Worker
spring.quartz.properties.org.quartz.threadPool.threadCount=25

Let's launch the application. Now the output will be like this:

Periodic task: Sat Jul 23 10:45:55 MSK 2022; Thread: my-scheduled-task-pool-1
Job: class quartzdemo.jobs.SimpleJob; Param: value; Thread: my-scheduler_Worker-1

Now the thread in which the task is started is called my-scheduler_Worker-1.

Multiple schedulers

If you need to create several schedulers with different parameters, you must define several SchedulerFactoryBeans. Let's look at an example.

package quartzdemo;

import quartzdemo.jobs.SimpleJob;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

import javax.sql.DataSource;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.Properties;

@SpringBootApplication
@EnableScheduling
public class QuartzDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(QuartzDemoApplication.class, args);
    }

    @Bean("customSchedulerFactoryBean1")
    public SchedulerFactoryBean customSchedulerFactoryBean1(DataSource dataSource) {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        Properties properties = new Properties();
        properties.setProperty("org.quartz.threadPool.threadNamePrefix", "my-custom-scheduler1_Worker");
        factory.setQuartzProperties(properties);
        factory.setDataSource(dataSource);
        return factory;
    }

    @Bean("customSchedulerFactoryBean2")
    public SchedulerFactoryBean customSchedulerFactoryBean2(DataSource dataSource) {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        Properties properties = new Properties();
        properties.setProperty("org.quartz.threadPool.threadNamePrefix", "my-custom-scheduler2_Worker");
        factory.setQuartzProperties(properties);
        factory.setDataSource(dataSource);
        return factory;
    }

    @Bean("customScheduler1")
    public Scheduler customScheduler1(@Qualifier("customSchedulerFactoryBean1") SchedulerFactoryBean factory) throws SchedulerException {
        Scheduler scheduler = factory.getScheduler();
        scheduler.start();
        return scheduler;
    }

    @Bean("customScheduler2")
    public Scheduler customScheduler2(@Qualifier("customSchedulerFactoryBean2") SchedulerFactoryBean factory) throws SchedulerException {
        Scheduler scheduler = factory.getScheduler();
        scheduler.start();
        return scheduler;
    }

    @Bean
    public CommandLineRunner run(@Qualifier("customScheduler1") Scheduler customScheduler1,
                                 @Qualifier("customScheduler2") Scheduler customScheduler2) {
        return (String[] args) -> {
            Date afterFiveSeconds = Date.from(LocalDateTime.now().plusSeconds(5).atZone(ZoneId.systemDefault()).toInstant());

            JobDetail jobDetail1 = JobBuilder.newJob(SimpleJob.class).usingJobData("param", "value1").build();
            Trigger trigger1 = TriggerBuilder.newTrigger().startAt(afterFiveSeconds).build();
            customScheduler1.scheduleJob(jobDetail1, trigger1);

            JobDetail jobDetail2 = JobBuilder.newJob(SimpleJob.class).usingJobData("param", "value2").build();
            Trigger trigger2 = TriggerBuilder.newTrigger().startAt(afterFiveSeconds).build();
            customScheduler2.scheduleJob(jobDetail2, trigger2);
        };
    }

}

Output:

Job: class quartzdemo.jobs.SimpleJob; Param: value2; Thread: my-custom-scheduler2_Worker-1
Job: class quartzdemo.jobs.SimpleJob; Param: value1; Thread: my-custom-scheduler1_Worker-1

The full source code is available over on GitHub.

Conclusion

Quartz is a powerful framework for automating scheduled tasks. It can be used both with the help of simple and intuitive Spring annotations and with fine customization and tuning, which provides a solution to complex problems.


Written by yaf | 10+ years Java developer
Published by HackerNoon on 2022/07/24