Quartz with MongoDB - Java Spring Boot Example

In this tutorial, you'll learn how to build a scheduler application with Spring Quartz and MongoDB in Java Spring Boot.

Quartz is an open-source job scheduling library that can be integrated with any size of Java applications.

Spring Boot offers spring-boot-starter-quartz 'Starter' which makes working with the Quartz easier and faster.

By default, Quartz only supports relational databases. However, we can integrate Quartz with MongoDB in a Spring Boot application.

To use Quartz with MongoDB, there is a MongoDB-based store for Quartz Scheduler library that we need to add to our application.

In this example, we will build a sample scheduler application using Quartz, Spring Boot and MongoDB for scheduling and unscheduling jobs via Spring Boot REST APIs.

Follow the steps below to complete this example:

Create a Spring Boot Application

  1. Go to Spring Initializr at https://start.spring.io and create a Spring Boot application with details as follows:
    • Project: Choose Gradle Project or Maven Project.
    • Language: Java
    • Spring Boot: Latest stable version of Spring Boot is selected by default. So leave it as is.
    • Project Metadata: Provide group name in the Group field. The group name is the id of the project. In Artifact field, provide the name of your project. In the package field, provide package name for your project. Next, select your preferred version of Java that is installed on your computer and is available on your local environment.
    • Dependencies: Add dependencies for Spring Web, Spring Boot DevTools, Quartz Scheduler, and Spring Data MongoDB.

    Refer to the image below for example:

  2. Click the GENERATE button and save/download the project zip bundle.
  3. Extract the project to your preferred working directory.
  4. Import the project in your preferred Java development IDE such as Eclipse or IntelliJ IDEA.

Add MongoDB-based Job store for Quartz Scheduler

To add MongoDB-based Job store for Quartz Scheduler library to your application, do the following:

  1. Add repository definition.
  2. If you are using Gradle, add the following repository definition to your build.gradle file:

    
    repositories {
        maven {
            url "https://dl.bintray.com/michaelklishin/maven/"
        }
    }
    

    With Maven, add the following to the pom.xml file:

    
    
    

    Now, add MongoDB-based Job store dependency:

    With Gradle:

    
    compile "com.novemberain:quartz-mongodb:2.2.0-rc2"
    

    With Maven:

    
    
    

Add Application Configurations

Open the application.properties file and copy the configurations listed below. Do not forget to update the values to make them relevant to your project:

src/main/resources/application.properties

# Server port
server.port = 8080

# Mongo Configuration
spring.data.mongodb.host = localhost
spring.data.mongodb.port = 27107
spring.data.mongodb.database = spring_quartz_test
spring.data.mongodb.username = test
spring.data.mongodb.password = 12345678

#Quartz Log level 
logging.level.org.springframework.scheduling.quartz=DEBUG
logging.level.org.quartz=DEBUG

Add Quartz Configurations

Quartz uses a properties file called quartz.properties . This file contains the most basic configuration of Quartz.

Create a quartz.properties file inside the src/main/resources/ folder of your project, copy and update the MySQL port number, database name, username, and password with credentials relevant to your project.

src/main/resources/quartz.properties

# Use the MongoDB store
org.quartz.jobStore.class=com.novemberain.quartz.mongodb.MongoDBJobStore

# MongoDB URI (optional if 'org.quartz.jobStore.addresses' is set)
org.quartz.jobStore.mongoUri=mongodb://admin:password@localhost:27107/database_name

# comma separated list of mongodb hosts/replica set seeds (optional if 'org.quartz.jobStore.mongoUri' is set)
#org.quartz.jobStore.addresses=host1
# database name
org.quartz.jobStore.dbName=quartz

# Will be used to create collections like mycol_jobs, mycol_triggers, mycol_calendars, mycol_locks
org.quartz.jobStore.collectionPrefix=mycol

# thread count setting is ignored by the MongoDB store but Quartz requries it
org.quartz.threadPool.threadCount=1

# turn clustering on:
org.quartz.jobStore.isClustered=true

# Must be unique for each node or AUTO to use autogenerated:
org.quartz.scheduler.instanceId=AUTO

# org.quartz.scheduler.instanceId=node1

# The same cluster name on each node:
org.quartz.scheduler.instanceName=clusterName

Here is an explanation of the above configurations:

  • org.quartz.jobStore.class - You need to tell Quartz which JobStore to use for storing scheduling information such as details of jobs, triggers, calendars. The com.novemberain.quartz.mongodb.MongoDBJobStore will tell Quartz to use MongoDB database for helding Quartz`s data.
  • org.quartz.jobStore.dbName - This value of this the database name.
  • org.quartz.scheduler.instanceId - This value of this can be any string but must be unique for all schedulers within a cluster. You can use the value AUTO if you wish the Id to be auto-generated for you. Or you can also use the value SYS_PROP if you wish the Id to come from the system property.
  • org.quartz.scheduler.instanceName - The value of this can be any string that will help you to distinguish schedulers when multiple instances are used within the same program. Incase of using the clustering features, same name must be used for every instance in the cluster which is logically the same Scheduler. In this example, we are naming our scheduler as clusterName.

Create a Quartz Configurations Java Class

Spring's SchedulerFactoryBean is a FactoryBean for creating and configuring a Quartz Scheduler. It also manages the scheduler's life cycle and exposes the scheduler as bean reference, so lets create a QuartzConfig.java Java class for creating a bean reference of SchedulerFactoryBean and JobFactory. A JobFactory is responsible for producing Job instances.

This QuartzConfig.java class should be annotated with @Configuration annotation. The @Configuration annotation indicates that this is a configuration class and will be used by the Spring application context to create beans for your application.

src/main/java/com/example/config/QuartzConfig.java

package com.example.config;

import java.io.IOException;
import java.util.Properties;
import org.quartz.spi.JobFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

@Configuration
public class QuartzConfig {

    @Autowired
    ApplicationContext applicationContext;

    @Bean
    public JobFactory jobFactory() {
        AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
        jobFactory.setApplicationContext(applicationContext);
        return jobFactory;
    }

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() throws IOException {

        SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
        schedulerFactory.setQuartzProperties(quartzProperties());
        schedulerFactory.setWaitForJobsToCompleteOnShutdown(true);
        schedulerFactory.setAutoStartup(true);
        schedulerFactory.setJobFactory(jobFactory());
        return schedulerFactory;
    }

    public Properties quartzProperties() throws IOException {
        PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
        propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
        propertiesFactoryBean.afterPropertiesSet();
        return propertiesFactoryBean.getObject();
    }

}

Create a AutowiringSpringBeanJobFactory Class

To make autowiring from inside the Job class possible, we need to create a Java class that should extend SpringBeanJobFactory and implement ApplicationContextAware, so lets create a AutowiringSpringBeanJobFactory.java Java class to bring Spring and Quartz together.

src/main/java/com/example/config/AutowiringSpringBeanJobFactory.java

package com.example.config;

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;

public class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory
implements ApplicationContextAware{

    AutowireCapableBeanFactory beanFactory;
    
    @Override
    public void setApplicationContext(final ApplicationContext context) {
        beanFactory = context.getAutowireCapableBeanFactory();
    }

    @Override
    protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
        final Object job = super.createJobInstance(bundle);
        beanFactory.autowireBean(job);
        return job;
    }
}

Create an Entity class

The application will save counter data in a MongoDB database collection and will increase the count value by 1 every two minutes. For this purpose, lets create an entity class which is an object wrapper for a database table. The attributes of the entity class are mapped to the columns of the database table. A MongoDB entity class must be annotated with @Document(collection = "table name") annotation.

src/main/java/com/example/model/Counter.java

package com.example.model;

import java.time.LocalDateTime;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.mongodb.core.mapping.Document;

@Document(collection = "counter")
public class Counter {
    @Id
    private String id;
    private long count = 0;
    private Long startTime;
    private boolean deleted = false;
    @CreatedDate
    private LocalDateTime created;
    @LastModifiedDate
    private LocalDateTime modified;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public long getCount() {
        return count;
    }

    public void setCount(long count) {
        this.count = count;
    }

    public Long getStartTime() {
        return startTime;
    }

    public void setStartTime(Long startTime) {
        this.startTime = startTime;
    }

    public boolean isDeleted() {
        return deleted;
    }

    public void setDeleted(boolean deleted) {
        this.deleted = deleted;
    }

    public LocalDateTime getCreated() {
        return created;
    }

    public void setCreated(LocalDateTime created) {
        this.created = created;
    }

    public LocalDateTime getModified() {
        return modified;
    }

    public void setModified(LocalDateTime modified) {
        this.modified = modified;
    }

}

Create a Data Transfer Object

Create a CounterDto.java Java class. This class should only contains getter/setter methods with serialization and deserialization mechanism but should not contain any business logic. Using this class, you can decide which data to return and which data to not return in remote calls to promote security and loose coupling.

src/main/java/com/example/dto/CounterDto.java

package com.example.dto;

import java.time.LocalDateTime;

public class CounterDto {
    private String id;
    private long count;
    private Long startTime;
    private LocalDateTime created;
    private LocalDateTime modified;
    private String status;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public long getCount() {
        return count;
    }

    public void setCount(long count) {
        this.count = count;
    }

    public Long getStartTime() {
        return startTime;
    }

    public void setStartTime(Long startTime) {
        this.startTime = startTime;
    }

    public LocalDateTime getCreated() {
        return created;
    }

    public void setCreated(LocalDateTime created) {
        this.created = created;
    }

    public LocalDateTime getModified() {
        return modified;
    }

    public void setModified(LocalDateTime modified) {
        this.modified = modified;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

}

Create Repository interface

To perform CRUD (Create Read Update Delete) operations on the table, create an interface and extend it with MongoRepository interface. The MongoRepository interface provides generic CRUD operations on a repository for a specific type. For this example, we will create CounterRepository.java interface.

src/main/java/com/example/repository/CounterRepository.java

package com.example.respository;

import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import com.example.model.Counter;

@Repository
public interface CounterRepository extends MongoRepository<Counter, String> {

}

Create a Job class

A Job can be any Java class that implements the Job interface of Quartz, so lets create a SampleJob.java Java class. The Job interface has a single execute method where the actual work for that particular job is written.

Inside the execute method, you can retrieve data that you may wish an instance of that job instance must have while it executes. The data are passed to a Job instance via the JobDataMap class which is part of the JobDetail object. The data are stored in JobDataMap prior to adding the job to the scheduler.

src/main/java/com/example/job/SampleJob.java

package com.example.job;

import java.util.Optional;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.example.model.Counter;
import com.example.respository.CounterRepository;

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

    @Autowired
    private CounterRepository counterRepository;

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

        /* Get counter id recorded by scheduler during scheduling */
        JobDataMap dataMap = context.getJobDetail().getJobDataMap();

        String counterId = dataMap.getString("counterId");

        log.info("Executing job for counter id {}", counterId);


        Optional<Counter> counterOpt = counterRepository.findById(counterId);
        if (counterOpt.isPresent()) {

            Counter counter = counterOpt.get();
            //increate count by 1
            counter.setCount(counter.getCount() + 1);
            counterRepository.save(counter);

        } else {
            log.info("No counter found while running job");
        }
    }

}

Create a Web Controller

Create a controller class with methods to schedule and unschedule jobs via REST APIs as shown below:

src/main/java/com/example/controller/CounterSchedulingController.java

`package com.example.controller;

import java.io.IOException;
import java.util.Date;
import java.util.Optional;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.SimpleTrigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.example.config.QuartzConfig;
import com.example.dto.CounterDto;
import com.example.job.SampleJob;
import com.example.model.Counter;
import com.example.respository.CounterRepository;

@RestController
@RequestMapping(path = "/counter")
public class CounterSchedulingController {

    @Autowired
    private QuartzConfig quartzConfig;
    @Autowired
    private CounterRepository counterRepository;

    @PostMapping(path = "/schedule")
    public @ResponseBody  CounterDto scheduleCounter(@RequestBody  CounterDto counterDto) {
        try {
            // save counter in table
            Counter counter = new Counter();
            counter.setCount(0);
            counter.setStartTime(counterDto.getStartTime());
            counter = counterRepository.save(counter);

            // Creating JobDetail instance
            String id = counter.getId();
            JobDetail jobDetail = JobBuilder.newJob(SampleJob.class).withIdentity(id).build();

            // Adding JobDataMap to jobDetail
            jobDetail.getJobDataMap().put("counterId", id);

            // Scheduling time to run job
            Date triggerJobAt = new Date(counter.getStartTime());

            SimpleTrigger trigger =
                    TriggerBuilder.newTrigger().withIdentity(id).startAt(triggerJobAt)
                            .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                                    .withMisfireHandlingInstructionFireNow()
                                    .withIntervalInMinutes(2).repeatForever())
                            .build();
            // Getting scheduler instance
            Scheduler scheduler = quartzConfig.schedulerFactoryBean().getScheduler();
            scheduler.scheduleJob(jobDetail, trigger);
            scheduler.start();

            counterDto.setStatus("SUCCESS");

        } catch (IOException | SchedulerException e) {
            // scheduling failed
            counterDto.setStatus("FAILED");
            e.printStackTrace();
        }
        return counterDto;
    }


    @DeleteMapping(path = "/{counterId}/unschedule")
    public @ResponseBody  CounterDto unscheduleCounter(
            @PathVariable(name = "counterId") String counterId) {

        CounterDto counterDto = new CounterDto();

        Optional<Counter> counterOpt = counterRepository.findById(counterId);
        if (!counterOpt.isPresent()) {
            counterDto.setStatus("Counter Not Found");
            return counterDto;
        }

        Counter counter = counterOpt.get();
        counter.setDeleted(true);
        counterRepository.save(counter);

        String id = counter.getId();

        try {
            Scheduler scheduler = quartzConfig.schedulerFactoryBean().getScheduler();

            scheduler.deleteJob(new JobKey(id));
            TriggerKey triggerKey = new TriggerKey(id);
            scheduler.unscheduleJob(triggerKey);
            counterDto.setStatus("SUCCESS");

        } catch (IOException | SchedulerException e) {
            counterDto.setStatus("FAILED");
            e.printStackTrace();
        }
        return counterDto;
    }
}

The controller has a POST method and a DELETE method. The POST method is mapped to /counter/schedule and the DELETE method is mapped to /counter/{counterId}/unschedule.

Enable Mongo Auditing

To benefit from @CreatedBy, @LastModifiedBy, @CreatedDate, @LastModifiedDate functionality, we need to enable Mongo auditing by annotating the Spring Boot main application class with @EnableMongoAuditing. The following code shows how to do so:

src/main/java/com/example/quartzmysql/QuartzMongodbSampleApplication.java

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.mongodb.config.EnableMongoAuditing;

@SpringBootApplication
@EnableMongoAuditing
public class QuartzMongodbSampleApplication {

	public static void main(String[] args) {
		SpringApplication.run(QuartzMongodbSampleApplication.class, args);
	}

}

Run the Application

Run your scheduler application and test scheduling and unscheduling of jobs using an HTTP requester tool, such as Postman or any other similar tools. The following examples shows how to do so:

Scheduling Example

Quartz Tutorial

NOTE: startTime is the schedule time in milliseconds for executing the job.


Unscheduling Example

Quartz Tutorial

Summary

Congratulations! you have learned how to schedule and unschedule jobs using Quartz Scheduler with MongoDB in Java Spring Boot.