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.
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.
We can see all rest endpoints from open api documentation
http://localhost:8080/swagger-ui/index.html?configUrl=/v3/api-docs/
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.