1. Overview
In this tutorial, we’ll build a simple Scheduler in Spring with Quartz.
We’ll begin with a simple goal in mind, to easily configure a new scheduled job.
1.1. Key Components of the Quartz API
Quartz has a modular architecture. It consists of several basic components that we can combine as required. In this tutorial, we’ll focus on the ones that are common to every job: Job, JobDetail, Trigger and Scheduler.
Although we’ll use Spring to manage the application, each individual component can be configured in two ways: the Quartz way or the Spring way (using its convenience classes).
We’ll cover both options as far as possible, for the sake of completeness, but we may adopt either. Now let’s start building, one component at a time.
2. Job and JobDetail
2.1. Job
The API provides a Job interface that has just one method, execute. It must be implemented by the class that contains the actual work to be done, i.e. the task. When a job’s trigger fires, the scheduler invokes the execute method, passing it a JobExecutionContext object.
The JobExecutionContext provides the job instance with information about its runtime environment, including a handle to the scheduler, a handle to the trigger, and the job’s JobDetail object.
In this quick example, the job delegates the task to a service class:
@Component
public class SampleJob implements Job {
@Autowired
private SampleJobService jobService;
public void execute(JobExecutionContext context) throws JobExecutionException {
jobService.executeSampleJob();
}
}
2.2. JobDetail
While the job is the workhorse, Quartz doesn’t store an actual instance of the job class. Instead, we can define an instance of the Job using the JobDetail class. The job’s class must be provided to the JobDetail, so that it knows the type of the job to be executed.
2.3. Quartz JobBuilder
The Quartz JobBuilder provides a builder-style API for constructing JobDetail entities:
@Bean
public JobDetail jobDetail() {
return JobBuilder.newJob().ofType(SampleJob.class)
.storeDurably()
.withIdentity("Qrtz_Job_Detail")
.withDescription("Invoke Sample Job service...")
.build();
}
2.4. Spring JobDetailFactoryBean
Spring’s JobDetailFactoryBean provides bean-style usage for configuring JobDetail instances. It uses the Spring bean name as the job name, if not otherwise specified:
@Bean
public JobDetailFactoryBean jobDetail() {
JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean();
jobDetailFactory.setJobClass(SampleJob.class);
jobDetailFactory.setDescription("Invoke Sample Job service...");
jobDetailFactory.setDurability(true);
return jobDetailFactory;
}
Every execution of the job creates a new instance of JobDetail. The JobDetail object conveys the detailed properties of the job. Once the execution is complete, references to the instance are dropped.
3. Trigger
A Trigger is the mechanism to schedule a Job, i.e. a Trigger instance “fires” the execution of a job. There’s a clear separation of responsibilities between the Job (notion of task) and Trigger (scheduling mechanism).
In addition to a Job, the trigger also needs a type, which we can choose based on the scheduling requirements.
Let’s say we want to schedule our task to execute once every hour indefinitely, then we can use Quartz’s TriggerBuilder or Spring’s SimpleTriggerFactoryBean to do so.
3.1. Quartz TriggerBuilder
TriggerBuilder is a builder-style API for constructing the Trigger entity:
@Bean
public Trigger trigger(JobDetail job) {
return TriggerBuilder.newTrigger().forJob(job)
.withIdentity("Qrtz_Trigger")
.withDescription("Sample trigger")
.withSchedule(simpleSchedule().repeatForever().withIntervalInHours(1))
.build();
}
3.2. Spring SimpleTriggerFactoryBean
SimpleTriggerFactoryBean provides bean-style usage for configuring SimpleTrigger. It uses the Spring bean name as the trigger name, and defaults to indefinite repetition if not otherwise specified:
@Bean
public SimpleTriggerFactoryBean trigger(JobDetail job) {
SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean();
trigger.setJobDetail(job);
trigger.setRepeatInterval(3600000);
trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
return trigger;
}
4. Configuring the JobStore
JobStore provides the storage mechanism for the Job and Trigger. It’s also responsible for maintaining all the data relevant to the job scheduler. The API supports both in-memory and persistent stores.
4.1. In-Memory JobStore
For our example, we’ll use the in-memory RAMJobStore, which offers blazing-fast performance and simple configuration via quartz.properties:
org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore
The obvious drawback of the RAMJobStore is that it’s volatile in nature. All the scheduling information is lost between shutdowns. If we need to keep job definitions and schedules between shutdowns, we can use the persistent JDBCJobStore instead.
To enable an in-memory JobStore in Spring*,* we’ll set this property in our application.properties:
spring.quartz.job-store-type=memory
4.2. JDBC JobStore
There are two types of JDBCJobStore: JobStoreTX and JobStoreCMT. They both do the same job of storing scheduling information in a database.
The difference between the two is how they manage the transactions that commit the data. The JobStoreCMT type requires an application transaction to store data, whereas the JobStoreTX type starts and manages its own transactions.
There are several properties to set for a JDBCJobStore. At a minimum, we must specify the type of JDBCJobStore, the data source, and the database driver class. There are driver classes for most databases, but StdJDBCDelegate covers most cases:
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.dataSource=quartzDataSource
Setting up a JDBC JobStore in Spring takes a few steps. First, we’ll set the store type in our application.properties:
spring.quartz.job-store-type=jdbc
Then we’ll need to enable auto-configuration and give Spring the data source needed by the Quartz scheduler. The @QuartzDataSource annotation does the hard work in configuring and initializing the Quartz database for us:
@Configuration
@EnableAutoConfiguration
public class SpringQrtzScheduler {
@Bean
@QuartzDataSource
public DataSource quartzDataSource() {
return DataSourceBuilder.create().build();
}
}
5. Scheduler
The Scheduler interface is the main API for interfacing with the job scheduler.
A Scheduler can be instantiated with a SchedulerFactory. Once created, we can register Jobs and Triggers with it. Initially, the Scheduler is in “stand-by” mode, and we must invoke its start method to start the threads that fire the execution of jobs.
5.1. Quartz StdSchedulerFactory
By simply invoking the getScheduler method on the StdSchedulerFactory, we can instantiate the Scheduler, initialize it (with the configured JobStore and ThreadPool), and return a handle to its API:
@Bean
public Scheduler scheduler(Trigger trigger, JobDetail job, SchedulerFactoryBean factory)
throws SchedulerException {
Scheduler scheduler = factory.getScheduler();
scheduler.scheduleJob(job, trigger);
scheduler.start();
return scheduler;
}
5.2. Spring SchedulerFactoryBean
Spring’s SchedulerFactoryBean provides bean-style usage for configuring a Scheduler, managing its life-cycle within the application context, and exposing the Scheduler as a bean for dependency injection:
@Bean
public SchedulerFactoryBean scheduler(Trigger trigger, JobDetail job, DataSource quartzDataSource) {
SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
schedulerFactory.setConfigLocation(new ClassPathResource("quartz.properties"));
schedulerFactory.setJobFactory(springBeanJobFactory());
schedulerFactory.setJobDetails(job);
schedulerFactory.setTriggers(trigger);
schedulerFactory.setDataSource(quartzDataSource);
return schedulerFactory;
}
5.3. Configuring SpringBeanJobFactory
The SpringBeanJobFactory provides support for injecting the scheduler context, job data map, and trigger data entries as properties into the job bean while creating an instance.
However, it lacks support for injecting bean references from the application context. Thanks to the author of this blog post, we can add auto-wiring support to SpringBeanJobFactory:
@Bean
public SpringBeanJobFactory springBeanJobFactory() {
AutoWiringSpringBeanJobFactory jobFactory = new AutoWiringSpringBeanJobFactory();
jobFactory.setApplicationContext(applicationContext);
return jobFactory;
}
6. Conclusion
In this article, we built our first basic scheduler using the Quartz API, as well as Spring’s convenience classes.
The key takeaway is that we’re able to configure a job with just a few lines of code, without using any XML-based configuration.
The complete source code for the example is available in this github project. This is a Maven project, so we can import it and run it as-is. The default setting uses Spring’s convenience classes, but we can easily switch it to Quartz API with a run-time parameter (refer to the README.md in the repository).