Job Scheduling with Quartz Tutorial

Quartz is an open-source job scheduling library that can be used to schedule and run a task at a pre-determined time or when the scheduled time arrives. For example: A task to run in exactly every 2 hours, a task to run on every weekdays except holidays at 12:30 PM, a task to run in every 2 days at 10:30 PM.

Quartz can be used to create both simple and complex schedules for executing jobs. Jobs are where the tasks are defined as standard Java components that are capable of executing anything that we program them to do.


Features of Quartz

  • Quartz can be integrated within any stand-alone Java application.
  • Quartz provide support for features such as JTA transaction and clustering.
  • Quartz can run task repeatedly at a given time interval.
  • It has fail-over and load balance capabilities for job executions.
  • It supports XA transactions(two phase commit protocol supported by most databases).
  • It supports RMI (Remote Method Invocation), thus allowing it to be called from another application running on different JVM.
  • It manages JTA transactions by using JobStoreCMT, a subclass of JDBCJobStore.
  • The clustering features makes the availibility of the scheduler very high as it uses fail-over and load-balancing functionality.

Quartz Maven Dependency

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.5.0-rc1</version>
</dependency>

Quartz Gradle Dependency

implementation group: 'org.quartz-scheduler', name: 'quartz', version: '2.5.0-rc1'

You can find the latest version of Quartz in the Quartz Maven Repository.


The Quartz API

The key interfaces of the Quartz API are the following:

  • Scheduler: It is the main API to interact with the scheduler.
  • Job: It is an interface to be implemented by components that you wish to have executed by the scheduler of the Quartz Framework.
  • JobDetail: It is used to define instances of Jobs.
  • Trigger: It is a component that is used to define schedule for executing Jobs.
  • JobBuilder: It is used to build JobDetail instances, which defines instances of Jobs.
  • TriggerBuilder: It is used to build Trigger instances.

Scheduler

The scheduler needs to be instantiated before it can be used. For doing this, you use the SchedulerFactory:

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

A scheduler can be started, placed in stand-by-mode, and shutdown after it is instantiated. It is important to note that once a scheduler is shutdown, it cannot be restarted without being reinstantiated.

A Scheduler's life-cycle is bounded by it's creation via a SchedulerFactory and a call to its shutdown method.

The Scheduler interface can be used to add, remove, lists Jobs and Triggers, and perform other scheduling related operations such as pausing a trigger. However, the Scheduler will not act on any triggers until it has been started with the Scheduler's start() method:


Jobs

A Job is a class that implements the Job interface. The Job has only one method:

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

public class SampleJob implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("Executing Quartz Job");
        // Write your task here
    }
}

The Job's execute() method is invoked by one of the scheduler's worker threads when the Job's trigger fires.

The JobExecutionContext object parameter that is passed to the execute() method provides the Job instance with various environment information such as a handle to the Scheduler that executed it, a handle to the Trigger that triggered the execution, the JobDetail object and a few other items.

The JobDetail object is created by our program at the time the Job is added to the Scheduler. The JobDetail contains property settings for the Job and a JobDataMap which can be used to store state information for a given instance of your Job class:

JobDetail jobDetail = JobBuilder.newJob(SampleJob.class)
.withIdentity("myJob", "group1")
.build();


JobDataMap

The JobDataMap can be used to hold any number of data objects which you wish to make available to the Job instance when it executes. JobDataMap is an implementation of the Java Map interface with some added convenience methods for storing and retrieving data of primitive types.

Here's a sample code of putting data into the JobDataMap before adding the job to the scheduler:

JobDetail jobDetail = JobBuilder.newJob(SampleJob.class)
.withIdentity("myJob", "group1")
.usingJobData("trackingId", "DH321")
.usingJobData("code", 55794)
.build();

Here's an example of retrieving the data from the JobDataMap during the Job's execution:

import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class SampleJob implements Job {

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

        System.out.println("Executing Quartz Job");

        JobDataMap dataMap = context.getJobDetail().getJobDataMap();

        String trackingId = dataMap.getString("trackingId");
        int code = dataMap.getInt("code");

        System.out.println("My trackingId is = " + trackingId + " and code = " + code);
    }

}


Triggers

Trigger objects are used to trigger the execution of jobs.

To schedule a job, you need to instantiate a trigger and set the properties for your scheduling needs. For example:

Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger", "group1")
.startNow()
.withSchedule(SimpleScheduleBuilder
        .simpleSchedule()
        .withIntervalInSeconds(40)
        .repeatForever())
.build();

Triggers may also have a JobDataMap associated with them for passing parameters to a Job that are specific to the firing of the trigger.

Quartz comes with different types of triggers for different scheduling needs. All Trigger types have TriggerKey properties for tracking their identities. Some TriggerKey properties are common to all trigger types and can be set using the TriggerBuilder.

Following is the list of common properties to all trigger types:

  • The jobKey property: It indicates the identity of the job that should be executed when the trigger fires.
  • The startTime property: It indicates when the trigger's schedule first come into effect. The value is a java.util.Date object that defines a moment in time on a given calendar date. For some trigger types, the trigger will fire at the start time and for some it simply marks the time that the schedule should start.
  • The endTime: indicates when the trigger's schedule must no longer be in effect.

The most commonly used triggers are SimpleTrigger and CronTrigger.

SimpleTrigger

SimpleTrigger is useful if you wish one-shot execution of a job at a given moment in time or if you need to execute a job at a given time and repeat it for N times, with a delay of T intervals between executions:

DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date triggerStartTime = df.parse("2021-01-18 09:16:00");

SimpleTrigger trigger = TriggerBuilder.newTrigger()
        .withIdentity("myTrigger", "group1")
                .startAt(triggerStartTime)
                .withSchedule(SimpleScheduleBuilder
                        .simpleSchedule()
                        .withIntervalInSeconds(40)
                        .repeatForever())
                .build();


CronTrigger

CronTrigger is useful if you need to execute a job based on calendar-like schedules such as triggering job at every Monday at 10:20 on the 20th day of every month.

Here's an example of a trigger that will fire on daily at 11:30 am:

CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger", "group1")
.withSchedule(CronScheduleBuilder
        .cronSchedule("0 11 30 * * ?"))
.forJob("myJob", "group1")
.build();


Here's another example of a trigger that will fire on Friday at 11:30 am, in a timezone other than the system's default:

CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger", "group1")
.withSchedule(CronScheduleBuilder
        .weeklyOnDayAndHourAndMinute(DateBuilder.FRIDAY, 11, 30)
        .inTimeZone(TimeZone.getTimeZone("America/Los_Angeles")))
.forJob("myJob", "group1")
.build();


Priority

This is another property of a trigger. When there are many triggers with the same fire time but few worker threads in your Quartz thread pool, Quartz may not have enough resources to fire all the triggers at the same time. In this case, you may want some triggers to gets triggered first. To control this, you can set priority properties on a Trigger. The triggers with the highest priority will be executed first. If no priority is set, the default priority of 5 is used. You can use both positive and negative integer value for priority.

Here's an example where we have three triggers with different priority. If Quartz resources are not enough to fire all the triggers at the same time, trigger3 will be fired first followed by trigger2 and so on:

Trigger trigger1 = TriggerBuilder.newTrigger()
.withIdentity("myTrigger1", "group1")
.startNow()
.withPriority(6)
.withSchedule(SimpleScheduleBuilder
        .simpleSchedule()
        .withIntervalInSeconds(40)
        .repeatForever())
.build();

Trigger trigger2 = TriggerBuilder.newTrigger()
.withIdentity("myTrigger2", "group1")
.startNow()
.withPriority(10)
.withSchedule(SimpleScheduleBuilder
        .simpleSchedule()
        .withIntervalInSeconds(40)
        .repeatForever())
.build();

Trigger trigger3 = TriggerBuilder.newTrigger()
.withIdentity("myTrigger3", "group1")
.startNow()
.withPriority(15)
.withSchedule(SimpleScheduleBuilder
        .simpleSchedule()
        .withIntervalInSeconds(40)
        .repeatForever())
.build();


Misfire Instructions

The Misfire Instructions is an important property of a trigger. A misfire occurs when persistent trigger misses its firing time because of the scheduler being shutdown or if there are no available threads in Quartz's thread pool to fire the job.

By default a trigger uses a smart policy instruction to search and update any persistent triggers that have misfired. So if no misfire instruction is set on a trigger, the withMisfireHandlingInstructionFireNow() misfire instruction method is called.

Here's an example where we have set what misfire instruction to use when a misfire occurs. This job immediately gets executed after the scheduler discovers the misfire:

Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger1", "group1")
.startNow()
.withSchedule(SimpleScheduleBuilder
        .simpleSchedule()
        .withIntervalInHours(4)
        .withMisfireHandlingInstructionFireNow()
        .repeatForever())
.build();


Summary

In this tutorial, you have learned how to build a scheduler for triggering a job. For using Quartz with Spring, we recommend you to read this.

You can read more about the Quartz on their official site.