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.
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.
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.
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);
};
}
}
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
.
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.
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.