Multiple Job Scheduling in Spring with Quartz

AbigZero
7 min readFeb 11, 2021

--

Job Scheduling

“Quartz is a richly featured, open source job scheduling library that can be integrated within virtually any Java application — from the smallest stand-alone application to the largest e-commerce system. Quartz can be used to create simple or complex schedules for executing tens, hundreds, or even tens-of-thousands of jobs; jobs whose tasks are defined as standard Java components that may execute virtually anything you may program them to do. The Quartz Scheduler includes many enterprise-class features, such as support for JTA transactions and clustering.” [2]

What Can Quartz Do For You?

If your application has tasks that need to occur at given moments in time, or if your system has recurring maintenance jobs then Quartz may be your ideal solution.

Sample uses of job scheduling with Quartz [2]:

  • Driving Process Workflow: As a new order is initially placed, schedule a Job to fire in exactly 2 hours, that will check the status of that order, and trigger a warning notification if an order confirmation message has not yet been received for the order, as well as changing the order’s status to ‘awaiting intervention’.
  • System Maintenance: Schedule a job to dump the contents of a database into an XML file every business day (all weekdays except holidays) at 11:30 PM.
  • Providing reminder services within an application.

Quartz has many features such as job scheduling, job execution, job persistence, transactions, clustering, listeners and plugin, friendly runtime environments, etc.

You will find the official documentation here. Details explanation is given in this link for a single job and single trigger using spring boot framework.

As I was not good at exploring quartz, I faced some problems to configure for multiple jobs and triggers. You can easily create a scheduler for multiple jobs and triggers by changing just a few lines of Code!

Let’s dive into the concept first.

Key Components of the Quartz API

Quartz consists of several basic components that can be combined as required. Some components are common to every job: Job, JobDetail, Trigger and Scheduler.

Each individual component can be configured in two ways: the Quartz way or the Spring way (using its convenience classes). As my application is written in spring boot, hence I will cover the multiple job scheduling using Spring way i.e. using spring’s convenience classes [1].

  1. Job

The API provides a Job interface having 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 [1].

In this quick example — the job delegates the task to a service class:

@Component
public class SampleJobOne implements Job {
Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private SampleJobService jobService;
@Override
public void execute(JobExecutionContext context) {
logger.info("JobOne ** {} ** fired @ {}",context.getJobDetail().getKey().getName(), context.getFireTime());
jobService.executeSampleJob();
logger.info("Next jobOne scheduled @ {}", context.getNextFireTime());
}
}

You can have multiple jobs. Just you need to implement your jobs from the Job interface. In my source code, I have created two jobs named SampleJobOne and SampleJobTwo.

2. JobDetail

While the job is the workhorse, Quartz does not 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 [1].

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 [1]:

public JobDetailFactoryBean jobDetail() {
JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean();
jobDetailFactory.setJobClass(SampleJobOne.class);
jobDetailFactory.setName("Qrtz_Job_One_Detail");
jobDetailFactory.setDescription("Invoke Sample Job service...");
jobDetailFactory.setDurability(true);
return jobDetailFactory;
}

A new instance of JobDetail is created for every execution of the job. The JobDetail object conveys the detailed properties of the job. Once the execution is completed, references to the instance are dropped.

Here comes the trick of multiple JobDetails, you just need to mention a bean name when creating the JobDetailFactoryBean like this.

@Bean(name = "jobOne")
public JobDetailFactoryBean jobDetailOne() {
JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean();
jobDetailFactory.setJobClass(SampleJobOne.class);
jobDetailFactory.setName("Qrtz_Job_One_Detail");
jobDetailFactory.setDescription("Invoke Sample Job service...");
jobDetailFactory.setDurability(true);
return jobDetailFactory;
}
@Bean(name = "jobTwo")
public JobDetailFactoryBean jobDetailTwo() {
JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean();
jobDetailFactory.setJobClass(SampleJobTwo.class);
jobDetailFactory.setName("Qrtz_Job_Two_Detail");
jobDetailFactory.setDescription("Invoke Sample Job service...");
jobDetailFactory.setDurability(true);
return jobDetailFactory;
}

So, when your application will run, it will create two JobDetailFactoryBean and will save them all in a Map<String, JobDeatil> data structure. This concept is important as I need to spend a lot of time to understand the concepts :(

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) [1].

In addition to Job, the trigger also needs a type that can be chosen based on the scheduling requirements.

Let’s say, we want to schedule our task to execute once every hour, indefinitely — we can use Spring’s SimpleTriggerFactoryBean to do so.

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 [1]:

@Bean
public SimpleTriggerFactoryBean trigger(JobDetail job) {
SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean();
trigger.setJobDetail(job);
trigger.setRepeatInterval(3600000);
trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
return trigger;
}

We can create multiple SimpleTriggerFactoryBean as needed. Now the question is, how to map them with the Jobs? Don’t worry, ‘Qualifier’ is there to save you! Lets see an example from my source code.

@Bean
public SimpleTriggerFactoryBean triggerOne(@Qualifier("jobOne") JobDetail job) {
SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean();
trigger.setJobDetail(job);
int frequencyInSec = 10;
logger.info("Configuring trigger to fire every {} seconds", frequencyInSec);
trigger.setRepeatInterval(frequencyInSec * 1000);
trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
trigger.setName("Qrtz_Trigger_Job_One");
return trigger;
}
@Bean
public SimpleTriggerFactoryBean triggerTwo(@Qualifier("jobTwo") JobDetail job) {
SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean();
trigger.setJobDetail(job);
int frequencyInSec = 20;
logger.info("Configuring trigger to fire every {} seconds", frequencyInSec);
trigger.setRepeatInterval(frequencyInSec * 1000);
trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
trigger.setName("Qrtz_Trigger_Job_Two");
return trigger;
}

4. Configuring the JobStore

JobStore provides the storage mechanism for the Job and Trigger, and is responsible for maintaining all the data relevant to the job scheduler. The API supports both in-memory and persistent stores.

Please go through this tutorial for detailed explanation.

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, Jobs and Triggers can be registered with it. Initially, the Scheduler is in “stand-by” mode, and its start method must be invoked to start the threads that fire the execution of jobs [1].

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 [1].

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 like so:

@Bean
public SpringBeanJobFactory springBeanJobFactory() {
AutoWiringSpringBeanJobFactory jobFactory = new AutoWiringSpringBeanJobFactory();
jobFactory.setApplicationContext(applicationContext);
return jobFactory;
}

Spring SchedulerFactoryBean:

Spring’s SchedulerFactoryBean provides bean-style usage for configuring a Scheduler, manages its life-cycle within the application context, and exposes the Scheduler as a bean for dependency injection [1]:

@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;
}

Above example covers a single Job and Trigger as in the method parameter we send (Trigger trigger, JobDetail job, …)

In the line schedulerFactory.setJobDetails(job) and schedulerFactory.setTriggers(trigger), we set the Job and Trigger in the SchedulerFactoryBean. If you want to make a scheduler for multiple jobs then you need to change just in these three lines.

First, change the parameter type from a single Trigger and JobDetail to the Trigger and JobDetail Map which is collected by SpringBeanJobFactory.

Then you need to set the JobDetails and Triggers in the SchedulerFactoryBean.

Besides, if you explore the setJobDetails and setTriggers methods from the java’s built-in abstract class SchedulerAccessor then you see the parameters () are arrays of JobDetails and Triggers.

public void setJobDetails(JobDetail... jobDetails) {
// Use modifiable ArrayList here, to allow for further adding of
// JobDetail objects during autodetection of JobDetail-aware Triggers.
this.jobDetails = new ArrayList<>(Arrays.asList(jobDetails));
}
public void setTriggers(Trigger... triggers) {
this.triggers = Arrays.asList(triggers);
}

Note: Those who have no idea about the (…) syntax please have a look here.

Hence, we must convert the map data of JobDetails and Triggers to an array of JobDetails and array of Triggers if we want to set it in the Scheduler.

Example code of scheduling multiple jobs with triggers:

@Bean
public SchedulerFactoryBean scheduler(Map<String, JobDetail> jobMap, Map<String, Trigger> triggers, DataSource quartzDataSource) {
SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
schedulerFactory.setConfigLocation(new ClassPathResource("quartz.properties"));
logger.debug("Setting the Scheduler up");
schedulerFactory.setJobFactory(springBeanJobFactory());
JobDetail[] jobs = jobMap.values().toArray(new JobDetail[0]);
Trigger[] tr = triggers.values().toArray(new Trigger[0]);
schedulerFactory.setJobDetails(jobs);
schedulerFactory.setTriggers(tr);
// Comment the following line to use the default Quartz job store.
schedulerFactory.setDataSource(quartzDataSource);
return schedulerFactory;
}

Summary

The main concept for scheduling your multiple jobs are -

First, you need to create multiple Job classes.

Then, you should pass the Job classes to the JobDetails to create an individual JobDetailFactoryBean.

After, you should create a/some trigger(s) to fire the JobDetails using SimpleTriggerFactoryBean.

Finally, you must send all the JobDetails in the scheduler along with the triggers to run the jobs using a scheduled manner.

That’s it. Very much easy. We have just built our first basic multi job scheduler using Spring’s convenience classes.

We can schedule as many jobs with different triggers as we want just changing a few lines of code!

The complete source code for the example is available in this github project. It is a Gradle project which can be imported and run as-is. In my project, the quartz based API is not working as I have not completed it fully. My first concern is to complete the task using Spring API classes.

Sample output of the source code is -

com.example.quartz.jobs.SampleJobOne     : JobOne ** Qrtz_Job_One_Detail ** fired @ Thu Feb 11 15:43:20 BDT 2021c.e.quartz.service.SampleJobService      : The sample job has begun...com.example.quartz.jobs.SampleJobTwo     : JobTwo ** Qrtz_Job_Two_Detail ** fired @ Thu Feb 11 15:43:20 BDT 2021c.e.quartz.service.SampleJobService      : The sample job has begun...c.e.quartz.service.SampleJobService      : Sample job has finished...com.example.quartz.jobs.SampleJobOne     : Next jobOne scheduled @ Thu Feb 11 15:43:29 BDT 2021c.e.quartz.service.SampleJobService      : Sample job has finished...com.example.quartz.jobs.SampleJobTwo     : Next jobTwo scheduled @ Thu Feb 11 15:43:39 BDT 2021

--

--