Quartz

Java Quartz Architecture Example

1. Introduction

In this post, we will take a closer look at the architecture of Quartz, a very popular open source job scheduling library that can be used in Java applications. We will see an architectural diagram and learn all the main and optional components of Quartz by providing code examples.

2. Project Setup

To run the code examples of this post, we will use the following technologies:

  • Java 8
  • Quartz 2.2.1
  • SLF4J 1.7.26
  • Logback 1.2.3
  • Maven 3.3.3
  • Eclipse 4.10.0

3. Quartz Architecture

In a nutshell, the main concept of Quartz is that a scheduler holds a list of jobs, in a persistence store, that are triggered at specific times or repeatedly. You can also register job or trigger listeners to the scheduler which perform some operations before and after the completion of jobs or triggers. The below diagram shows the process of scheduling jobs in a Quartz application.

Java Quartz Architecture - Architectural Diagram
Quartz Architectural Diagram

4. Quartz Main Components

Let’s see in the following sections the main components that are involved in the Quartz scheduling process.

4.1 Scheduler

The org.quartz.Scheduler is the main interface of a Quartz Scheduler. A Scheduler maintains a registry of JobDetails and Triggers. Once registered, the Scheduler is responsible for executing Jobs when their associated Triggers fire when their scheduled time arrives.

4.2 Scheduler Factory

The org.quartz.SchedulerFactory is the interface responsible for creating Scheduler instances. Any class that implements this interface must implement the following methods:

  • Scheduler getScheduler() throws SchedulerException
  • Scheduler getScheduler(String schedName) throws SchedulerException
  • Collection<Scheduler> getAllSchedulers() throws SchedulerException

The first two methods return a Scheduler instance with the default or a given name. The third method returns all known Schedulers.

There are two implementations of the SchedulerFactory:

  • StdSchedulerFactory – org.quartz.impl.StdSchedulerFactory
  • DirectSchedulerFactory – org.quartz.impl.DirectSchedulerFactory

The StdSchedulerFactory creates a Scheduler instance based on the contents of a properties file which is by default named quartz.properties and is loaded from the current working directory. On the other hand, the DirectSchedulerFactory is a more simple implementation of SchedulerFactory and it is also a singleton.

Find below an example of how to create a Scheduler instance:

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

From the above code, a new StdSchedulerFactory instance is created which returns a Scheduler instance by calling the getScheduler() method.

4.3 Job

The org.quartz.Job is the most crucial interface to be implemented by classes, as it represents a job to be performed. Below we see an example of a class implementing this interface:

public class SimpleJob implements Job {
    
    private final Logger log = LoggerFactory.getLogger(SimpleJob.class);

    public void execute(JobExecutionContext context) throws JobExecutionException {
        log.info("SimpleJob executed!");
    }
}

From the above code, we see that the SimpleJob class implements the execute(JobExecutionContext context) method of the Job interface by printing just a single line of code.

4.4 Job Detail

The org.quartz.JobDetail conveys the detail properties of a given Job instance. Quartz does not store an actual instance of a Job class, but instead allows you to define an instance of one, through the use of a JobDetail. Let’s see how this is done:

JobDetail job = JobBuilder.newJob(SimpleJob.class)
                .withIdentity("myJob", "myGroup")
                .build();

In the example above, we define a new job and tie it to the SimpleJob class we created previously. Note that JobDetails are created using the org.quartz.JobBuilder class.

4.5 Trigger

The org.quartz.Trigger is the base interface with properties common to all Triggers. Triggers are the mechanism by which Jobs are scheduled. Many Triggers can point to the same Job, but a single Trigger can only point to one Job. The org.quartz.TriggerBuilder is used to instantiate Triggers.

There are various implementations of Trigger. The most commonly used ones are:

  • SimpleTrigger – org.quartz.SimpleTrigger
  • CronTrigger – org.quartz.CronTrigger

The SimpleTrigger is used to fire a Job at a given moment in time, and optionally repeated at a specified interval. The CronTrigger is used to fire a Job at given moments in time, defined with Unix cron-like schedule definitions.

The following example shows how to create a Trigger using TriggerBuilder:

Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("myTrigger", "myGroup")
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(3)
                        .repeatForever())
                .build();

In the above code, we use the TriggerBuilder helper class to create a Trigger which runs every 3 seconds indefinitely. So far we haven’t tied any Job to the Trigger. This is what the Scheduler does.

4.6 Quartz Scheduler

The org.quartz.core.QuartzScheduler is the heart of Quartz, an indirect implementation of the Scheduler interface, containing methods to schedule Jobs using Triggers. The above code schedules a Job using the Scheduler instance we created in a previous section:

scheduler.scheduleJob(job, trigger);

From the above code, we see that we don’t pass a Job to the Scheduler, but a JobDetail, in which we tie the Job. We also pass a Trigger which schedules the Job to run at specific times. Finally to start the Scheduler call:

scheduler.start();

And to shutdown the Scheduler:

scheduler.shutdown(boolean waitForJobsToComplete);

The waitForJobsToComplete argument is self-explanatory; it allows the Scheduler to wait the shutdown process until all jobs are completed.

5. Quartz Optional Components

Let’s now see some optional components that we can use for additional functionality in Quartz.

5.1 Job Store

The org.quartz.spi.JobStore is the interface to be implemented by classes that want to provide a Job and Trigger storage mechanism for the QuartzScheduler‘s use. There are two implementations of the JobStore interface:

  • RAMJobStore – org.quartz.simpl.RAMJobStore
  • JobStoreSupport – org.quartz.impl.jdbcjobstore.JobStoreSupport

The RAMJobStore is the default JobStore which utilizes RAM as its storage device. The ramification of this is that access is extremely fast, but the data is completely volatile – therefore this JobStore should not be used if true persistence between program shutdowns is required. The JobStoreSupport contains base functionality for JDBC-based JobStore implementations.

You can enable the JobStoreSupport using JDBC, via the Quartz properties file:

org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.dataSource=quartzDataSource
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
org.quartz.dataSource.quartzDataSource.driver=com.mysql.jdbc.Driver
org.quartz.dataSource.quartzDataSource.URL=jdbc:mysql://localhost:3306/quartz_schema
org.quartz.dataSource.quartzDataSource.user=root
org.quartz.dataSource.quartzDataSource.password=change_me

From the above, the JobStoreTX class (which extends the JobStoreSupport class) is used as the JobStore. For a more detailed explanation of how to use Quartz with JDBC see here.

5.2 Job Listener

The org.quartz.JobListener is the interface to be implemented by classes that want to be informed when a JobDetail executes. The job listeners are attached to the scheduler and have methods that are called before and after the execution of jobs. In the following example we create a new JobListener class:

public class MyJobListener implements JobListener {
    
    private final Logger log = LoggerFactory.getLogger(MyJobListener.class);
 
    public String getName() {
        return MyJobListener.class.getSimpleName();
    }
 
    public void jobToBeExecuted(JobExecutionContext context) {
        log.info("{} is about to be executed", context.getJobDetail().getKey().toString());
    }
 
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        log.info("{} finised execution", context.getJobDetail().getKey().toString());
    }
 
    public void jobExecutionVetoed(JobExecutionContext context) {
        log.info("{} was about to be executed but a JobListener vetoed it's execution", context.getJobDetail().getKey().toString());
    }
}

From the JobListener we create above, the ordering of the methods that would be executed is:

MyJobListener.jobToBeExecuted() -> MyJob.execute() -> MyJobListener.jobWasExecuted()

Finally we register the MyJobListener to the Scheduler:

scheduler.getListenerManager().addJobListener(new MyJobListener());

5.3 Trigger Listener

Similarly to JobListener, the org.quartz.TriggerListener is the interface to be implemented by classes that want to be informed when a Trigger fires. In the following example we create a new TriggerListener class:

public class MyTriggerListener implements TriggerListener {

    private final Logger log = LoggerFactory.getLogger(MyTriggerListener.class);

    @Override
    public String getName() {
        return MyTriggerListener.class.getSimpleName();
    }

    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {
        log.info("{} trigger is fired", getName());
    }

    @Override
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
        log.info("{} was about to be executed but a TriggerListener vetoed it's execution", context.getJobDetail().getKey().toString());
        return false;
    }

    @Override
    public void triggerMisfired(Trigger trigger) {
        log.info("{} trigger was misfired", getName());
    }

    @Override
    public void triggerComplete(Trigger trigger, JobExecutionContext context,
            CompletedExecutionInstruction triggerInstructionCode) {
        log.info("{} trigger is complete", getName());
    }
}

From the TriggerListener we create above, the ordering of the methods that would be executed is:

MyTriggerListener.triggerFired() -> MyJob.execute() -> MyJobListener.triggerComplete()

Finally we register the MyTriggerListener to the Scheduler:

scheduler.getListenerManager().addTriggerListener(new MyTriggerListener());

6. Java Quartz Architecture – Conclusion

In this post, we examined in more details the architecture of Quartz. We saw how the scheduling of jobs works by providing an architectural diagram. We also took a closer look at the main and optional components of Quartz such as the Scheduler, Job, Trigger etc.

7. Download the Eclipse project

Download
You can download the full source code of the above examples here: Java Quartz Architecture Example

Lefteris Karageorgiou

Lefteris is a Lead Software Engineer at ZuluTrade and has been responsible for re-architecting the backend of the main website from a monolith to event-driven microservices using Java, Spring Boot/Cloud, RabbitMQ, Redis. He has extensive work experience for over 10 years in Software Development, working mainly in the FinTech and Sports Betting industries. Prior to joining ZuluTrade, Lefteris worked as a Senior Java Developer at Inspired Gaming Group in London, building enterprise sports betting applications for William Hills and Paddy Power. He enjoys working with large-scalable, real-time and high-volume systems deployed into AWS and wants to combine his passion for technology and traveling by attending software conferences all over the world.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button