This article demonstrates how to implement and manage Quartz Scheduler using Rest API with Spring Boot and MongoDB

Introduction

Quartz is a job scheduling library that can be integrated into a wide variety of Java applications. Quartz is generally used for enterprise-class applications to support process workflow, system management actions and to provide timely services within the applications And quartz supports clustering.

MongoDB is a cross-platform document-oriented database program. Classified as a NoSQL database program, MongoDB uses JSON-like documents with optional schemas. MongoDB is developed by MongoDB Inc. and licensed under the Server Side Public License.

Step 1 – Create a Spring boot project with the required dependencies.

Imitate spring boot project we can user Spring Initializr. I’m using IntelliJ Idea to create the project.

I’m using Java 11 Gradle project.

Spring Initializr

Use below dependencies

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
    implementation 'org.springframework.boot:spring-boot-starter-quartz'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation("org.mongodb:mongodb-driver-sync:4.0.5")
    compile "com.novemberain:quartz-mongodb:2.2.0-rc2"
    implementation 'com.github.lalyos:jfiglet:0.0.3'
    compileOnly 'org.projectlombok:lombok'
    compile 'org.springdoc:springdoc-openapi-ui:1.2.32'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
}

Step 2 – Create quartz properties file with MongoDB configurations.

Create new property quartz.properties file inside the resources folder. And add the below configuration. Please make sure to change the mongo database URL and the database name.

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Quartz Job Scheduling
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#org.quartz.scheduler.instanceName=springboot-quartz-mongodb
#org.quartz.scheduler.instanceId=AUTO
# 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://user:password@database_IP:port/DATABASE_NAME?authSource=admin
# MongoDB Database name
org.quartz.jobStore.dbName=DATABASE_NAME
# Will be used to create collections like quartz_jobs, quartz_triggers, quartz_calendars, quartz_locks
org.quartz.jobStore.collectionPrefix=onload_quartz_
# Thread count setting is ignored by the MongoDB store but Quartz requires it
org.quartz.threadPool.threadCount=1
# Skip running a web request to determine if there is an updated version of Quartz available for download
org.quartz.scheduler.skipUpdateCheck=true
# Register Quartz plugins to be executed
# turn clustering on:
#org.quartz.jobStore.isClustered=true
# Must be unique for each node or AUTO to use autogenerated:
org.quartz.scheduler.instanceId=onloadcode
# org.quartz.scheduler.instanceId=node1
# The same cluster name on each node:
org.quartz.scheduler.instanceName=onloadcode
# To setup other clusters use different collection prefix
org.quartz.scheduler.collectionPrefix=onloadcode_quartz_
# Frequency (in milliseconds) at which this instance checks-in to cluster.
# Affects the rate of detecting failed instances.
# Defaults to 7500 ms.
org.quartz.scheduler.clusterCheckinInterval=10000
# Time in millis after which a trigger can be considered as expired.
# Defaults to 10 minutes:
org.quartz.scheduler.triggerTimeoutMillis=1200000
# Time in millis after which a job can be considered as expired.
# Defaults to 10 minutes:
org.quartz.scheduler.jobTimeoutMillis=1200000
# Time limit in millis after which a trigger should be treated as misfired.
# Defaults to 5000 ms.
org.quartz.scheduler.misfireThreshold=10000
# WriteConcern timeout in millis when writing in Replica Set.
# Defaults to 5000 ms.
org.quartz.scheduler.mongoOptionWriteConcernTimeoutMillis=10000

Step 3 – Create configuration classes to access MongoDB from Spring Boot.

First, we have to create a config file for SpringBeanJobFactory.

AutowiringSpringBeanJobFactory.java

package com.onloadcode.quartz.lesson.config;

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

@Component
public class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory
        implements ApplicationContextAware {
    private transient AutowireCapableBeanFactory beanFactory;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        beanFactory = applicationContext.getAutowireCapableBeanFactory();
    }

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

Next, we have to create JobConfigaration class for load properties and create SchedulerFactoryBean.

JobConfiguration.java

package com.onloadcode.quartz.lesson.config;

import lombok.extern.slf4j.Slf4j;
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;

import java.io.IOException;
import java.util.Properties;

@Slf4j
@Configuration
public class JobConfiguration {
    @Autowired
    AutowiringSpringBeanJobFactory jobFactory;

    @Bean
    public SchedulerFactoryBean schedulerFactory(ApplicationContext applicationContext) {
        try {
            SchedulerFactoryBean factoryBean = new SchedulerFactoryBean();
            factoryBean.setQuartzProperties(quartzProperties());
            jobFactory.setApplicationContext(applicationContext);
            factoryBean.setJobFactory(jobFactory);
            factoryBean.setOverwriteExistingJobs(true);
            factoryBean.setSchedulerName("onloadcode-job-scheduler");
            log.info("onloadcode Quartz Scheduler initialized");
            return factoryBean;
        } catch (Exception e) {
            log.error(
                    "onloadcode Scheduler can not be initialized, the error is "
                            + e.getMessage());
            return null;
        }
    }

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

Step 4 – Create a Sample Job class for testing.

Next, we have to create a sample Job class to execute a quartz job. Here we print the content of the context

SampleJob.java

package com.onloadcode.quartz.lesson.job;

import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

@Slf4j
public class SampleJob implements Job {
    /**
     * <p>
     * Called by the <code>{@link Scheduler}</code> when a <code>{@link Trigger}</code>
     * fires that is associated with the <code>Job</code>.
     * </p>
     *
     * <p>
     * The implementation may wish to set a
     * {@link JobExecutionContext#setResult(Object) result} object on the
     * {@link JobExecutionContext} before this method exits.  The result itself
     * is meaningless to Quartz, but may be informative to
     * <code>{@link JobListener}s</code> or
     * <code>{@link TriggerListener}s</code> that are watching the job's
     * execution.
     * </p>
     *
     * @param context
     * @throws JobExecutionException if there is an exception while executing the job.
     */
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        log.info("Job triggered - Sample Job");
        JobDataMap map = context.getMergedJobDataMap();
        printData(map);
        log.info("Job completed");
    }

    private void printData(JobDataMap map) {
        log.info(">>>>>>>>>>>>>>>>>>> START: ");
        map.entrySet().forEach(entry -> {
            log.info(entry.getKey() + " " + entry.getValue());
        });
        log.info(">>>>>>>>>>>>>>>>>>> END: ");
    }
}

Step 5 – Create Rest endpoints for operating Quartz scheduler via rest APIs.

Let’s create a rest controller class and supporting requests and response bean classes to access and operate quartz jobs.

Controller Classes

SchedulerManagementController.java

package com.onloadcode.quartz.lesson.controller;

import com.onloadcode.quartz.lesson.bean.request.JobDetailRequestBean;
import com.onloadcode.quartz.lesson.bean.response.SchedulerResponseBean;
import com.onloadcode.quartz.lesson.service.SchedulerService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.OK;

@RestController
@RequestMapping("/api/v1/scheduler")
@Tag(
        name = "SchedulerManagementController",
        description = "MongoDB Quartz | Scheduler Management API")
public class SchedulerManagementController {

    public static final String JOBS = "/job-group/{jobGroup}/jobs";
    public static final String JOBS_BY_NAME = "/job-group/{jobGroup}/jobs/{jobName}";
    public static final String JOBS_PAUSE = "/job-group/{jobGroup}/jobs/{jobName}/pause";
    public static final String JOBS_RESUME = "/job-group/{jobGroup}/jobs/{jobName}/resume";
    @Autowired
    private SchedulerService schedulerService;

    @Operation(
            summary = "MongoDB Quartz | Scheduler create a new Job",
            description = "",
            tags = {"SchedulerManagementController"})
    @ApiResponses(
            value = {
                    @ApiResponse(
                            responseCode = "200",
                            description = "successful operation",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "404",
                            description = "Scheduler Job Creation API not found",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "400",
                            description = "Bad Request,Scheduler Job Creation type not supported",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "500",
                            description = "Failure",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class)))
            })
    @PostMapping(path = JOBS)
    public ResponseEntity<SchedulerResponseBean> createJob(
            @PathVariable String jobGroup, @RequestBody JobDetailRequestBean jobDetailRequestBean) {
        return new ResponseEntity<SchedulerResponseBean>(
                schedulerService.createJob(jobGroup, jobDetailRequestBean), CREATED);
    }

    @Operation(
            summary = "MongoDB Quartz | Scheduler find a Job",
            description = "",
            tags = {"SchedulerManagementController"})
    @ApiResponses(
            value = {
                    @ApiResponse(
                            responseCode = "200",
                            description = "successful operation",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "404",
                            description = "Scheduler Job find API not found",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "400",
                            description = "Bad Request,Scheduler Job find type not supported",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "500",
                            description = "Failure",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class)))
            })
    @GetMapping(path = JOBS_BY_NAME)
    public ResponseEntity<SchedulerResponseBean> findJob(
            @PathVariable String jobGroup, @PathVariable String jobName) {
        return new ResponseEntity<>(schedulerService.findJob(jobGroup, jobName), OK);
    }

    @Operation(
            summary = "MongoDB Quartz | Scheduler update an existing Job",
            description = "",
            tags = {"SchedulerManagementController"})
    @ApiResponses(
            value = {
                    @ApiResponse(
                            responseCode = "200",
                            description = "successful operation",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "404",
                            description = "Scheduler Job update API not found",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "400",
                            description = "Bad Request,Scheduler Job update type not supported",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "500",
                            description = "Failure",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class)))
            })
    @PutMapping(path = JOBS_BY_NAME)
    public ResponseEntity<SchedulerResponseBean> updateJob(
            @PathVariable String jobGroup,
            @PathVariable String jobName,
            @RequestBody JobDetailRequestBean jobDetailRequestBean) {
        return new ResponseEntity<>(schedulerService.updateJob(jobGroup, jobName, jobDetailRequestBean), OK);
    }

    @Operation(
            summary = "MongoDB Quartz | Scheduler delete Job",
            description = "",
            tags = {"SchedulerManagementController"})
    @ApiResponses(
            value = {
                    @ApiResponse(
                            responseCode = "200",
                            description = "successful operation",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "404",
                            description = "Scheduler Job delete API not found",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "400",
                            description = "Bad Request,Scheduler Job delete type not supported",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "500",
                            description = "Failure",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class)))
            })
    @DeleteMapping(path = JOBS_BY_NAME)
    public ResponseEntity<SchedulerResponseBean> deleteJob(
            @PathVariable String jobGroup, @PathVariable String jobName) {
        return new ResponseEntity<>(schedulerService.deleteJob(jobGroup, jobName), OK);
    }

    @Operation(
            summary = "MongoDB Quartz | Scheduler pause Job",
            description = "",
            tags = {"SchedulerManagementController"})
    @ApiResponses(
            value = {
                    @ApiResponse(
                            responseCode = "200",
                            description = "successful operation",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "404",
                            description = "Scheduler Job pause API not found",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "400",
                            description = "Bad Request,Scheduler Job pause type not supported",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "500",
                            description = "Failure",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class)))
            })
    @PatchMapping(path = JOBS_PAUSE)
    public ResponseEntity<SchedulerResponseBean> pauseJob(
            @PathVariable String jobGroup, @PathVariable String jobName) {
        return new ResponseEntity<>(schedulerService.pauseJob(jobGroup, jobName), OK);
    }

    @Operation(
            summary = "MongoDB Quartz | Scheduler resume Job",
            description = "",
            tags = {"SchedulerManagementController"})
    @ApiResponses(
            value = {
                    @ApiResponse(
                            responseCode = "200",
                            description = "successful operation",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "404",
                            description = "Scheduler Job resume API not found",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "400",
                            description = "Bad Request,Scheduler Job resume type not supported",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class))),
                    @ApiResponse(
                            responseCode = "500",
                            description = "Failure",
                            content = @Content(schema = @Schema(implementation = SchedulerResponseBean.class)))
            })
    @PatchMapping(path = JOBS_RESUME)
    public ResponseEntity<SchedulerResponseBean> resumeJob(
            @PathVariable String jobGroup, @PathVariable String jobName) {
        return new ResponseEntity<>(schedulerService.resumeJob(jobGroup, jobName), OK);
    }
}

Request Bean Classes

JobDetailRequestBean.java

package com.onloadcode.quartz.lesson.bean.request;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.onloadcode.quartz.lesson.job.SampleJob;
import lombok.Data;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Trigger;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;

import static org.quartz.JobBuilder.newJob;

@Data
public class JobDetailRequestBean implements Serializable {
    @NotBlank
    private String name;
    private String group;
    @JsonProperty("triggers")
    private List<TriggerDetailsRequestBean> triggerDetails = new ArrayList<>();
    @NotEmpty
    private String orgCode;
    @NotEmpty
    private String jobType;
    @NotEmpty
    private String uniqueKey;
    private Map<String, Object> data = new LinkedHashMap<>();

    public static JobDetailRequestBean buildJobDetail(JobDetail jobDetail, List<? extends Trigger> triggersOfJob) {
        List<TriggerDetailsRequestBean> triggerDetailsRequestBeanList = triggersOfJob.stream()
                .map(TriggerDetailsRequestBean::buildTriggerDetails)
                .collect(Collectors.toList());

        return new JobDetailRequestBean()
                .setName(jobDetail.getKey().getName())
                .setGroup(jobDetail.getKey().getGroup())
                .setOrgCode(jobDetail.getJobDataMap().getString("orgCode"))
                .setJobType(jobDetail.getJobDataMap().getString("jobType"))
                .setUniqueKey(jobDetail.getJobDataMap().getString("uniqueKey"))
                .setData((Map<String, Object>) jobDetail.getJobDataMap().get("data"))
                .setTriggerDetails(triggerDetailsRequestBeanList);
    }

    public JobDetailRequestBean setTriggerDetails(final List<TriggerDetailsRequestBean> triggerDetails) {
        this.triggerDetails = triggerDetails;
        return this;
    }

    public JobDetailRequestBean setData(final Map<String, Object> data) {
        this.data = data;
        return this;
    }

    public JobDetailRequestBean setUniqueKey(String uniqueKey) {
        this.uniqueKey = uniqueKey;
        return this;
    }

    public JobDetailRequestBean setJobType(String jobType) {
        this.jobType = jobType;
        return this;
    }

    public JobDetailRequestBean setOrgCode(String orgCode) {
        this.orgCode = orgCode;
        return this;
    }

    public JobDetailRequestBean setGroup(final String group) {
        this.group = group;
        return this;
    }

    public JobDetailRequestBean setName(final String name) {
        this.name = name;
        return this;
    }

    public JobDetail buildJobDetail() {
        JobDataMap jobDataMap = new JobDataMap(getData());
        jobDataMap.put("orgCode", orgCode);
        jobDataMap.put("jobType", jobType);
        jobDataMap.put("uniqueKey", uniqueKey);
        jobDataMap.put("data", data);
        return newJob(SampleJob.class)
                .withIdentity(getName(), getGroup())
                .usingJobData(jobDataMap)
                .build();
    }

    @JsonIgnore
    public Set<Trigger> buildTriggers() {
        return triggerDetails.stream()
                .map(TriggerDetailsRequestBean::buildTrigger)
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }
}

TriggerDetailsRequestBean.java

package com.onloadcode.quartz.lesson.bean.request;

import lombok.Data;
import org.quartz.JobDataMap;
import org.quartz.Trigger;

import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.sql.Date;
import java.time.LocalDateTime;
import java.util.TimeZone;

import static java.time.ZoneId.systemDefault;
import static java.util.UUID.randomUUID;
import static org.quartz.CronExpression.isValidExpression;
import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.springframework.util.StringUtils.isEmpty;

@Data
public class TriggerDetailsRequestBean implements Serializable {
    @NotBlank
    private String name;
    private String group;
    private LocalDateTime fireTime;
    private String cron;

    /**
     * Build trigger details trigger details request bean.
     *
     * @param trigger the trigger
     * @return the trigger details request bean
     */
    public static TriggerDetailsRequestBean buildTriggerDetails(Trigger trigger) {
        return new TriggerDetailsRequestBean()
                .setName(trigger.getKey().getName())
                .setGroup(trigger.getKey().getGroup())
                .setFireTime((LocalDateTime) trigger.getJobDataMap().get("fireTime"))
                .setCron(trigger.getJobDataMap().getString("cron"));
    }

    /**
     * Sets cron.
     *
     * @param cron the cron
     * @return the cron
     */
    public TriggerDetailsRequestBean setCron(final String cron) {
        this.cron = cron;
        return this;
    }

    /**
     * Sets fire time.
     *
     * @param fireTime the fire time
     * @return the fire time
     */
    public TriggerDetailsRequestBean setFireTime(final LocalDateTime fireTime) {
        this.fireTime = fireTime;
        return this;
    }

    /**
     * Sets group.
     *
     * @param group the group
     * @return the group
     */
    public TriggerDetailsRequestBean setGroup(final String group) {
        this.group = group;
        return this;
    }

    /**
     * Sets name.
     *
     * @param name the name
     * @return the name
     */
    public TriggerDetailsRequestBean setName(final String name) {
        this.name = name;
        return this;
    }

    /**
     * Build trigger trigger.
     *
     * @return the trigger
     */
    public Trigger buildTrigger() {
        if (!isEmpty(cron)) {
            if (!isValidExpression(cron))
                throw new IllegalArgumentException(
                        "Provided expression " + cron + " is not a valid cron expression");
            return newTrigger()
                    .withIdentity(buildName(), group)
                    .withSchedule(
                            cronSchedule(cron)
                                    .withMisfireHandlingInstructionFireAndProceed()
                                    .inTimeZone(TimeZone.getTimeZone(systemDefault())))
                    .usingJobData("cron", cron)
                    .build();
        } else if (!isEmpty(fireTime)) {
            JobDataMap jobDataMap = new JobDataMap();
            jobDataMap.put("fireTime", fireTime);
            return newTrigger()
                    .withIdentity(buildName(), group)
                    .withSchedule(simpleSchedule().withMisfireHandlingInstructionNextWithExistingCount())
                    .startAt(Date.from(fireTime.atZone(systemDefault()).toInstant()))
                    .usingJobData(jobDataMap)
                    .build();
        }
        throw new IllegalStateException("unsupported trigger details " + this);
    }

    private String buildName() {
        return isEmpty(name) ? randomUUID().toString() : name;
    }
}

Response Bean Classes

SchedulerResponseBean.java

package com.onloadcode.quartz.lesson.bean.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.http.HttpStatus;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SchedulerResponseBean {
    private Object result;
    private HttpStatus resultCode;
}

Step 6 – Create a Services class for change quartz scheduler operations.

Let’s create a service class to operate quartz scheduler basic operations.

SchedulerService.java

package com.onloadcode.quartz.lesson.service;

import com.onloadcode.quartz.lesson.bean.request.JobDetailRequestBean;
import com.onloadcode.quartz.lesson.bean.response.SchedulerResponseBean;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import java.util.Objects;
import java.util.Optional;
import java.util.Set;

import static org.quartz.JobKey.jobKey;

@Slf4j
@Service
@RequiredArgsConstructor
public class SchedulerService {

    private final Scheduler scheduler;

    /**
     * Create job scheduler response bean.
     *
     * @param jobGroup             the job group
     * @param jobDetailRequestBean the job detail request bean
     * @return the scheduler response bean
     */
    public SchedulerResponseBean createJob(
            String jobGroup, JobDetailRequestBean jobDetailRequestBean) {
        SchedulerResponseBean responseBean = new SchedulerResponseBean();
        jobDetailRequestBean.setGroup(jobGroup);
        JobDetail jobDetail = jobDetailRequestBean.buildJobDetail();
        Set<Trigger> triggersForJob = jobDetailRequestBean.buildTriggers();
        log.info("About to save job with key - {}", jobDetail.getKey());
        try {
            scheduler.scheduleJob(jobDetail, triggersForJob, false);
            log.info("Job with key - {} saved sucessfully", jobDetail.getKey());
            responseBean.setResult(jobDetailRequestBean);
            responseBean.setResultCode(HttpStatus.CREATED);
        } catch (SchedulerException e) {
            log.error(
                    "Could not save job with key - {} due to error - {}",
                    jobDetail.getKey(),
                    e.getLocalizedMessage());
            throw new IllegalArgumentException(e.getLocalizedMessage());
        }
        return responseBean;
    }

    /**
     * Find job scheduler response bean.
     *
     * @param jobGroup the job group
     * @param jobName  the job name
     * @return the scheduler response bean
     */
    public SchedulerResponseBean findJob(String jobGroup, String jobName) {
        SchedulerResponseBean responseBean = new SchedulerResponseBean();
        try {
            JobDetail jobDetail = scheduler.getJobDetail(jobKey(jobName, jobGroup));
            if (Objects.nonNull(jobDetail))
                responseBean.setResult(
                        Optional.of(
                                JobDetailRequestBean.buildJobDetail(
                                        jobDetail, scheduler.getTriggersOfJob(jobKey(jobName, jobGroup)))));
            responseBean.setResultCode(HttpStatus.OK);

        } catch (SchedulerException e) {
            String errorMsg =
                    String.format(
                            "Could not find job with key - %s.%s  due to error -  %s",
                            jobGroup, jobName, e.getLocalizedMessage());
            log.error(errorMsg);
            responseBean.setResultCode(HttpStatus.INTERNAL_SERVER_ERROR);
            responseBean.setResult(errorMsg);
        }
        log.warn("Could not find job with key - {}.{}", jobGroup, jobName);
        return responseBean;
    }

    /**
     * Update job scheduler response bean.
     *
     * @param jobGroup             the job group
     * @param jobName              the job name
     * @param jobDetailRequestBean the job detail request bean
     * @return the scheduler response bean
     */
    public SchedulerResponseBean updateJob(
            String jobGroup, String jobName, JobDetailRequestBean jobDetailRequestBean) {
        SchedulerResponseBean responseBean = new SchedulerResponseBean();
        try {
            JobDetail oldJobDetail = scheduler.getJobDetail(jobKey(jobName, jobGroup));
            if (Objects.nonNull(oldJobDetail)) {
                JobDataMap jobDataMap = oldJobDetail.getJobDataMap();
                jobDataMap.put("orgCode", jobDetailRequestBean.getOrgCode());
                jobDataMap.put("jobType", jobDetailRequestBean.getJobType());
                jobDataMap.put("uniqueKey", jobDetailRequestBean.getUniqueKey());
                jobDataMap.put("data", jobDetailRequestBean.getData());
                JobBuilder jb = oldJobDetail.getJobBuilder();
                JobDetail newJobDetail = jb.usingJobData(jobDataMap).storeDurably().build();
                scheduler.addJob(newJobDetail, true);
                log.info("Updated job with key - {}", newJobDetail.getKey());
                responseBean.setResult(jobDetailRequestBean);
                responseBean.setResultCode(HttpStatus.CREATED);
            }
            log.warn("Could not find job with key - {}.{} to update", jobGroup, jobName);
        } catch (SchedulerException e) {
            String errorMsg =
                    String.format(
                            "Could not find job with key - %s.%s to update due to error -  %s",
                            jobGroup, jobName, e.getLocalizedMessage());
            log.error(errorMsg);
            responseBean.setResultCode(HttpStatus.INTERNAL_SERVER_ERROR);
            responseBean.setResult(errorMsg);
        }
        return responseBean;
    }

    /**
     * Delete job scheduler response bean.
     *
     * @param jobGroup the job group
     * @param jobName  the job name
     * @return the scheduler response bean
     */
    public SchedulerResponseBean deleteJob(String jobGroup, String jobName) {
        SchedulerResponseBean responseBean = new SchedulerResponseBean();
        try {
            scheduler.deleteJob(jobKey(jobName, jobGroup));
            String msg = "Deleted job with key - " + jobGroup + "." + jobName;
            responseBean.setResult(msg);
            responseBean.setResultCode(HttpStatus.OK);
            log.info(msg);
        } catch (SchedulerException e) {
            String errorMsg =
                    String.format(
                            "Could not find job with key - %s.%s to Delete due to error -  %s",
                            jobGroup, jobName, e.getLocalizedMessage());
            log.error(errorMsg);
            responseBean.setResultCode(HttpStatus.INTERNAL_SERVER_ERROR);
            responseBean.setResult(errorMsg);
        }
        return responseBean;
    }

    /**
     * Pause job scheduler response bean.
     *
     * @param jobGroup the job group
     * @param jobName  the job name
     * @return the scheduler response bean
     */
    public SchedulerResponseBean pauseJob(String jobGroup, String jobName) {
        SchedulerResponseBean responseBean = new SchedulerResponseBean();
        try {
            scheduler.pauseJob(jobKey(jobName, jobGroup));
            String msg = "Paused job with key - " + jobGroup + "." + jobName;
            responseBean.setResult(msg);
            responseBean.setResultCode(HttpStatus.OK);
        } catch (SchedulerException e) {
            String errorMsg =
                    String.format(
                            "Could not find job with key - %s.%s  due to error -  %s",
                            jobGroup, jobName, e.getLocalizedMessage());
            log.error(errorMsg);
            responseBean.setResultCode(HttpStatus.INTERNAL_SERVER_ERROR);
            responseBean.setResult(errorMsg);
        }
        return responseBean;
    }

    /**
     * Resume job scheduler response bean.
     *
     * @param jobGroup the job group
     * @param jobName  the job name
     * @return the scheduler response bean
     */
    public SchedulerResponseBean resumeJob(String jobGroup, String jobName) {
        SchedulerResponseBean responseBean = new SchedulerResponseBean();
        try {
            scheduler.resumeJob(jobKey(jobName, jobGroup));
            String msg = "Resumed job with key - " + jobGroup + "." + jobName;
            responseBean.setResult(msg);
            responseBean.setResultCode(HttpStatus.OK);
        } catch (SchedulerException e) {
            String errorMsg =
                    String.format(
                            "Could not find job with key - %s.%s  due to error -  %s",
                            jobGroup, jobName, e.getLocalizedMessage());
            log.error(errorMsg);
            responseBean.setResultCode(HttpStatus.INTERNAL_SERVER_ERROR);
            responseBean.setResult(errorMsg);
        }
        return responseBean;
    }
}

Step 7 – Create Open API configurations.

Let’s Configure open API configurations for swagger documentations

Create OpenApiConfig class file

OpenApiConfig.java

package com.onloadcode.quartz.lesson.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

@Configuration
public class OpenApiConfig {
    /**
     * The constant DEFAULT_CONTACT.
     */
    public static final Contact DEFAULT_CONTACT =
            new Contact()
                    .name("Onload Code")
                    .email("author@onloadcode.com")
                    .url("www.onloadcode.com");

    /**
     * The constant LICENSE.
     */
    public static final License LICENSE =
            new License().name("Apache 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html");

    private static final Set<String> DEFAULT_PRODUCES_AND_CONSUMES =
            new HashSet<>(Arrays.asList("application/json", "application/xml"));
    /**
     * The Api controller package.
     */
    @Value("${api-package}")
    public String API_CONTROLLER_PACKAGE;
    /**
     * The Application name.
     */
    @Value("${application.name}")
    public String APPLICATION_NAME;
    /**
     * The Application description.
     */
    @Value("${application.description}")
    public String APPLICATION_DESCRIPTION;

    /**
     * Custom open api open api.
     *
     * @return the open api
     */
    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI().components(new Components()).info(apiEndPointsInfo());
    }

    private Info apiEndPointsInfo() {
        return new Info()
                .title(APPLICATION_NAME)
                .description(APPLICATION_DESCRIPTION)
                .contact(DEFAULT_CONTACT)
                .license(LICENSE)
                .version("1.0.0");
    }
}

Add properties to the application.properties file.

api-package=com.onloadcode.quartz.lesson.controller
application.name=quarts-scheduling-spring-boot-mongodb
application.description=Quartz Scheduler using Rest API with Spring Boot and MongoDB

Step 8 –Test the application.

Let’s run the application and verify its run without any issues

After successfully application up we can check the mongo database and see 4 quartz collection on there with our given prefix.

quartz collections

We can see all rest endpoints from open api documentation

http://localhost:8080/swagger-ui/index.html?configUrl=/v3/api-docs/

Open API documentations

Demo – Create a Job

Let’s try to create a new job using the rest of API.

request cURL

curl --location --request POST 'http://localhost:8080/api/v1/scheduler/job-group/TEST_TEMPLATE/jobs' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "testJ1",
    "orgCode": "SIN",
    "uniqueKey": "uniqueKey",
    "jobType": "QUERY_TEMPLATE",
    "jobProperties": [
        {
            "propertyKey": "template-name",
            "propertyValue": "temp-1"
        }
    ],
    "triggers": [
        {
            "name": "testJ1",
            "group": "QUERY_TEMPLATE",
            "cron": "0/30 0/1 * 1/1 * ? *"
        }
    ]
}'

the result will be 201 Created

{
    "result": {
        "name": "testJ1",
        "group": "TEST_TEMPLATE",
        "orgCode": "SIN",
        "jobType": "QUERY_TEMPLATE",
        "uniqueKey": "uniqueKey",
        "data": {},
        "triggers": [
            {
                "name": "testJ1",
                "group": "QUERY_TEMPLATE",
                "fireTime": null,
                "cron": "0/30 0/1 * 1/1 * ? *"
            }
        ]
    },
    "resultCode": "CREATED"
}

According to this given parameters every 30 second this job will trigger and we can see on the logs

2020-12-06 20:23:00.034  INFO 33552 --- [eduler_Worker-1] c.o.quartz.lesson.job.SampleJob          : Job triggered - Sample Job
2020-12-06 20:23:00.034  INFO 33552 --- [eduler_Worker-1] c.o.quartz.lesson.job.SampleJob          : >>>>>>>>>>>>>>>>>>> START: 
2020-12-06 20:23:00.034  INFO 33552 --- [eduler_Worker-1] c.o.quartz.lesson.job.SampleJob          : cron 0/30 0/1 * 1/1 * ? *
2020-12-06 20:23:00.034  INFO 33552 --- [eduler_Worker-1] c.o.quartz.lesson.job.SampleJob          : data {}
2020-12-06 20:23:00.034  INFO 33552 --- [eduler_Worker-1] c.o.quartz.lesson.job.SampleJob          : orgCode SIN
2020-12-06 20:23:00.034  INFO 33552 --- [eduler_Worker-1] c.o.quartz.lesson.job.SampleJob          : uniqueKey uniqueKey
2020-12-06 20:23:00.034  INFO 33552 --- [eduler_Worker-1] c.o.quartz.lesson.job.SampleJob          : jobType QUERY_TEMPLATE
2020-12-06 20:23:00.034  INFO 33552 --- [eduler_Worker-1] c.o.quartz.lesson.job.SampleJob          : >>>>>>>>>>>>>>>>>>> END: 
2020-12-06 20:23:00.034  INFO 33552 --- [eduler_Worker-1] c.o.quartz.lesson.job.SampleJob          : Job completed
2020-12-06 20:23:00.043  INFO 33552 --- [eduler_Worker-1] c.n.quartz.mongodb.dao.LocksDao          : Removing trigger lock QUERY_TEMPLATE.testJ1.onloadcode
2020-12-06 20:23:00.050  INFO 33552 --- [eduler_Worker-1] c.n.quartz.mongodb.dao.LocksDao          : Trigger lock QUERY_TEMPLATE.testJ1.onloadcode removed.
2020-12-06 20:23:00.067  INFO 33552 --- [SchedulerThread] c.n.quartz.mongodb.dao.LocksDao          : Inserting lock for trigger QUERY_TEMPLATE.testJ1
2020-12-06 20:23:00.079  INFO 33552 --- [SchedulerThread] c.n.quartz.mongodb.TriggerRunner         : Acquired trigger: QUERY_TEMPLATE.testJ1

Demo – Find a Job

Let us use the rest API to search for a job we created.

request cURL

curl --location --request GET 'http://localhost:8080/api/v1/scheduler/job-group/TEST_TEMPLATE/jobs/testJ1' \
--data-raw ''

the result will be 200 OK

{
    "result": {
        "name": "testJ1",
        "group": "TEST_TEMPLATE",
        "orgCode": "SIN",
        "jobType": "QUERY_TEMPLATE",
        "uniqueKey": "uniqueKey",
        "data": {},
        "triggers": [
            {
                "name": "testJ1",
                "group": "QUERY_TEMPLATE",
                "fireTime": null,
                "cron": "0/30 0/1 * 1/1 * ? *"
            }
        ]
    },
    "resultCode": "OK"
}

Demo – Update a Job

Let’s update the existing job we created.

request cURL

curl --location --request PUT 'http://localhost:8080/api/v1/scheduler/job-group/TEST_TEMPLATE/jobs/testJ1' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "testJ1",
    "orgCode": "SIN",
    "uniqueKey": "uniqueKey",
    "jobType": "QUERY_TEMPLATE",
    "data": 
        {
            "propertyKey": "template-name",
            "propertyValue": "temp-1"
        }
    ,
    "triggers": [
        {
            "name": "testJ1",
            "group": "QUERY_TEMPLATE",
            "cron": "0/30 0/1 * 1/1 * ? *"
        }
    ]
}'

the result will be 200 OK

{
    "result": {
        "name": "testJ1",
        "group": null,
        "orgCode": "SIN",
        "jobType": "QUERY_TEMPLATE",
        "uniqueKey": "uniqueKey",
        "data": {
            "propertyKey": "template-name",
            "propertyValue": "temp-1"
        },
        "triggers": [
            {
                "name": "testJ1",
                "group": "QUERY_TEMPLATE",
                "fireTime": null,
                "cron": "0/30 0/1 * 1/1 * ? *"
            }
        ]
    },
    "resultCode": "CREATED"
}

You can try to do pause,  resume, and delete operations as well.

Conclusion

Thanks for reading the article how to implement and manage Quartz Scheduler using Rest API with Spring Boot and MongoDB

You can find source codes for this tutorial from our Github.