Java Quartz vs Spring Batch Example
1. Introduction
This example shows how to build a Quartz scheduler and Spring Batch application. Both applications execute a business task repeatedly in a different way.
Quartz is an open source library designed to schedule a job for enterprises. Quartz application repeatedly executes the job based on the scheduled time.
Spring Batch is an open source framework designed to enable the development of robust batch applications vital for the daily operations of enterprise systems. Spring Batch application repeatedly executes the job for the batch data.
2. Business Problem
A telecommunication company receives Call Detail Records (CDRs) from the network switch periodically. An application reads the CDR, enriches it by adding the billing account, and then saves the enriched CDR.
2.1 Business domain models and services
There are two simplified domain models to represent the CDR with simple and complex format.
2.1.1 CallDetailRecord_Simple
Simple CDR model.
CallDetailRecord_Simple
package jcg.demo.model; public class CallDetailRecord_Simple { private String callerId; private String calling; public void setCallerId(String callerId) { this.callerId = callerId; } public String getCalling() { return calling; } public void setCalling(String calling) { this.calling = calling; } public String getCallerId() { return callerId; } @Override public String toString() { return "CallDetailRecord_Simple [callerId=" + callerId + ", calling=" + calling + "]"; } }
2.1.2 CallDetailRecord_Rich
Rich CDR model.
CallDetailRecord_Rich
package jcg.demo.model; public class CallDetailRecord_Rich { private String callerId; private String calling; private String billingAccount; public void setCallerId(String callerId) { this.callerId = callerId; } public String getCalling() { return calling; } public void setCalling(String calling) { this.calling = calling; } public String getCallerId() { return callerId; } public String getBillingAccount() { return billingAccount; } public void setBillingAccount(String billingAccount) { this.billingAccount = billingAccount; } @Override public String toString() { return "CallDetailRecord_Rich [ billingAccount=" + billingAccount + ", callerId=" + callerId + ", calling=" + calling + "]"; } }
2.2 Call Detail Record service
A service to enrich and save the Call Detail Record.
CallDetailRecordService
package jcg.demo.service; import org.springframework.stereotype.Service; import jcg.demo.model.CallDetailRecord_Rich; import jcg.demo.model.CallDetailRecord_Simple; /** * This service enriches the CallDetailRecord with the billing account. * * @author Mary.Zheng */ @Service public class CallDetailRecordService { public CallDetailRecord_Rich enrich(CallDetailRecord_Simple simpleCDR) { System.out.println("\tCallDetailRecordService enrich()"); CallDetailRecord_Rich richCdr = new CallDetailRecord_Rich(); richCdr.setCallerId(simpleCDR.getCallerId()); richCdr.setCalling(simpleCDR.getCalling()); richCdr.setBillingAccount("BTN" + simpleCDR.getCallerId()); return richCdr; } public void save(CallDetailRecord_Rich richCdr) { System.out.println("\tCallDetailRecordService save()"); } }
3. Technologies used
The example code in this article was built and run using:
- Java 1.8.101 (1.8.x will do fine)
- Maven 3.3.9 (3.3.x will do fine)
- Quartz 2.2.1 (2.x will do fine)
- Spring Batch 3.0.5.RELEASE (4.0.0.M1 will do fine)
- Eclipse Neon (Any Java IDE would work)
4. Quartz Application
4.1 Overview
This example demonstrates how to build an application to process the CDR via Quartz scheduler library.
4.2 Build a Quartz scheduler application
4.2.1 Dependency
Add Quartz dependency to the pom.xml.
Dependency in pom.xml
<dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.2.1</version> </dependency>
4.2.2 Quartz Job
Create a Quartz job by implementing org.quartz.Job
.
QuartzJob
package jcg.demo.scheduler.quartz2; import java.time.LocalDateTime; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import jcg.demo.model.CallDetailRecord_Rich; import jcg.demo.model.CallDetailRecord_Simple; import jcg.demo.service.CallDetailRecordService; /** * This class implements Quartz Job interface, anything you wish to be executed * by the Quartz Scheduler should be here it should invokes business class to * perform task. * * @author Mary.Zheng * */ public class QuartzJob implements Job { private CallDetailRecordService cdrService = new CallDetailRecordService(); @Override public void execute(JobExecutionContext context) throws JobExecutionException { LocalDateTime localTime = LocalDateTime.now(); System.out.println(Thread.currentThread().getName() + ": Run QuartzJob at " + localTime.toString()); CallDetailRecord_Simple simpleCDR = dummySimpleCDR(); CallDetailRecord_Rich richCdr = cdrService.enrich(simpleCDR); cdrService.save(richCdr); } private CallDetailRecord_Simple dummySimpleCDR() { CallDetailRecord_Simple cdr = new CallDetailRecord_Simple(); cdr.setCalling("3145791111"); cdr.setCallerId("6365272222"); return cdr; } }
- line 21: Create
QuartzJob
which implementsorg.quartz.Job
- line 23: Initialize
cdrService
fromCallDetailRecordService
- line 31: Invoke
cdrService
save
. This indicates that the data transaction happens at the record level
4.2.3 Quartz application
Create a Quartz scheduler to execute the QuartzJob
every minute.
QuartzSchedulerApp
package jcg.demo.scheduler.quartz2; import org.quartz.CronScheduleBuilder; import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.impl.StdSchedulerFactory; /** * This application schedule a job to run every minute * * @author Mary.Zheng * */ public class QuartzSchedulerApp { private static final String TRIGGER_NAME = "MyTriggerName"; private static final String GROUP = "simple_Group"; private static final String JOB_NAME = "someJob"; private static Scheduler scheduler; public static void main(String[] args) throws Exception { System.out.println("QuartzSchedulerApp main thread: " + Thread.currentThread().getName()); scheduler = new StdSchedulerFactory().getScheduler(); scheduler.start(); Trigger trigger = buildCronSchedulerTrigger(); scheduleJob(trigger); } private static void scheduleJob(Trigger trigger) throws Exception { JobDetail someJobDetail = JobBuilder.newJob(QuartzJob.class).withIdentity(JOB_NAME, GROUP).build(); scheduler.scheduleJob(someJobDetail, trigger); } private static Trigger buildCronSchedulerTrigger() { String CRON_EXPRESSION = "0 * * * * ?"; Trigger trigger = TriggerBuilder.newTrigger().withIdentity(TRIGGER_NAME, GROUP) .withSchedule(CronScheduleBuilder.cronSchedule(CRON_EXPRESSION)).build(); return trigger; } }
4.3 Quartz scheduler execution
Execute the Quartz scheduler application.
Output
QuartzSchedulerApp main thread: main DefaultQuartzScheduler_Worker-1: Run QuartzJob at 2017-11-26T11:24:00.049 CallDetailRecordService enrich() CallDetailRecordService save() DefaultQuartzScheduler_Worker-2: Run QuartzJob at 2017-11-26T11:25:00.002 CallDetailRecordService enrich() CallDetailRecordService save() DefaultQuartzScheduler_Worker-3: Run QuartzJob at 2017-11-26T11:26:00.003 CallDetailRecordService enrich() CallDetailRecordService save() ...
As you see here, the job ran infinitely every minute. The CallDetailRecordService
save()
was invoked for each CDR.
5. Spring Batch Application
5.1 Overview
This example demonstrates how to build a batch application to process a CDR via Spring Batch framework.
5.2 Build a Spring batch application
5.2.1 Dependency
Add Spring Batch dependency to the pom.xml.
Dependency in pom.xml
<dependency> <groupId>org.springframework.batch</groupId> <artifactId>spring-batch-core</artifactId> <version>3.0.5.RELEASE</version> </dependency>
5.2.2 Spring Batch ItemReader
Create a BatchItemReader
by implementing the org.springframework.batch.item.ItemReader
.
BatchItemReader
package jcg.demo.batch.spring; import java.util.Random; import org.springframework.batch.item.ItemReader; import org.springframework.batch.item.NonTransientResourceException; import org.springframework.batch.item.ParseException; import org.springframework.batch.item.UnexpectedInputException; import org.springframework.stereotype.Component; import jcg.demo.model.CallDetailRecord_Simple; /** * This is implementation of the batch item reader according to your business. * This example builds a dummy data. Spring-batch provides several common * reader. such as FlatFileItemReader, JdbcCursorItemReader etc. The batch * job completes when the ItemReader has no data to read. * * @author Mary.Zheng * */ @Component("batchItemReader") public class BatchItemReader implements ItemReader <CallDetailRecord_Simple > { @Override public CallDetailRecord_Simple read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException { CallDetailRecord_Simple cdr = buildDummyCDR(); System.out.println(Thread.currentThread().getName() + "- Inside BatchItemReader..." + cdr); return cdr; } private CallDetailRecord_Simple buildDummyCDR() { Random rand = new Random(); int value = rand.nextInt(10); if (value == 0) { return null; } CallDetailRecord_Simple cdr = new CallDetailRecord_Simple(); cdr.setCalling("314579111" + value); cdr.setCallerId("636527222" + value); return cdr; }
- line 23: Create
BatchItemReader
which implementsorg.springframework.batch.item.ItemReader
withCallDetailRecord_Simple
- line 39-41: Batch job is completed when its step’s reader reads a null object
5.2.3 Spring Batch ItemWriter
Create BatchItemWriter
by implementing the org.springframework.batch.item.ItemWriter
.
BatchItemWriter
package jcg.demo.batch.spring; import java.util.List; import org.springframework.batch.item.ItemWriter; import org.springframework.stereotype.Component; import jcg.demo.model.CallDetailRecord_Rich; /** * This is implementation of the batch item writer according to your business. * * @author Mary.Zheng * */ @Component("batchItemWriter") public class BatchItemWriter implements ItemWriter <CallDetailRecord_Rich >{ @Override public void write(List<? extends CallDetailRecord_Rich> arg0) throws Exception { System.out.println(Thread.currentThread().getName() + "- Inside BatchItemWriter..." + arg0); } }
- line 17: Create
BatchItemWriter
which implementsorg.springframework.batch.item.ItemWriter
withCallDetailRecord_Rich
5.2.4 Spring Batch ItemProcessor
Create BatchItemProcessor
by implementing the org.springframework.batch.item.ItemProcessor
.
BatchItemProcessor
package jcg.demo.batch.spring; import org.springframework.batch.item.ItemProcessor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import jcg.demo.model.CallDetailRecord_Rich; import jcg.demo.model.CallDetailRecord_Simple; import jcg.demo.service.CallDetailRecordService; /** * This is implementation of the batch item processor according to your * business. It transforms the batch item from the reader format to the writer * format. * * @author Mary.Zheng * */ @Component("batchItemProcesser") public class BatchItemProcessor implements ItemProcessor <CallDetailRecord_Simple , CallDetailRecord_Rich> { @Autowired private CallDetailRecordService cdrService; @Override public CallDetailRecord_Rich process(CallDetailRecord_Simple cdr) throws Exception { System.out.println(Thread.currentThread().getName() + "- Inside BatchItemProcessor..." + cdr); return cdrService.enrich(cdr); } }
- line 20: Create
BatchItemProcessor
which implementsorg.springframework.batch.item.ItemProcessor
to transform the data fromCallDetailRecord_Simple
toCallDetailRecord_Rich
5.2.5 Configure Spring Batch beans
Configure Spring Batch beans provided by Spring Batch framework.
batchContext.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:batch="http://www.springframework.org/schema/batch" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:task="http://www.springframework.org/schema/task" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xmlns:util="http://www.springframework.org/schema/util" xmlns:crypt="http://springcryptoutils.com/schema/crypt" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd http://springcryptoutils.com/schema/crypt http://springcryptoutils.com/schema/crypt.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.2.xsd"> <context:component-scan base-package="jcg.demo.batch.spring,jcg.demo.service" /> <batch:job id="processCDRJob" xmlns="http://www.springframework.org/schema/batch"> <batch:step id="step1"> <batch:tasklet> <batch:chunk reader="batchItemReader" writer="batchItemWriter" processor="batchItemProcesser" commit-interval="1" /> </batch:tasklet> </batch:step> </batch:job> <bean id="transactionManager" class="org.springframework.batch.support.transaction.ResourcelessTransactionManager" /> <bean id="jobRepository" class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean"> <property name="transactionManager" ref="transactionManager" /> </bean> <bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher"> <property name="jobRepository" ref="jobRepository" /> </bean> </beans>
- line 16: Enable Spring
ComponentScan
at bothjcg.demo.batch.spring
andjcg.demo.service
packages - line 18-25: Configure the
processCDRJob
batch job based onbatchItemReader
created at step 5.2.2,batchItemWriter
created at step 5.2.3, andbatchItemProcessor
created at step 5.2.4 - line 27-28: Configure Spring Batch Bean
transactionManager
- line 30-33: Configure Spring Batch Bean
jobRepository
- line 35-38: Configure Spring Batch Bean
jobLauncher
5.2.6 Spring Batch application
Create a Spring Batch application.
SpringBatchApp
package jcg.demo.batch.spring; import org.springframework.batch.core.Job; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersInvalidException; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; import org.springframework.batch.core.repository.JobRestartException; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportResource; @Configuration @ImportResource({ "classpath:/spring/batchcontext.xml" }) public class SpringBatchApp { public static void main(String[] args) throws JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException, JobParametersInvalidException { @SuppressWarnings("resource") AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(SpringBatchApp.class); context.refresh(); JobLauncher jobLauncher = (JobLauncher) context.getBean("jobLauncher"); Job processCDRJob = (Job) context.getBean("processCDRJob"); JobExecution execution = jobLauncher.run(processCDRJob, new JobParameters()); System.out.println(Thread.currentThread().getName() + "- Exit Status : " + execution.getStatus()); } }
- line 16: Import Spring
batchcontext.xml
created at step 5.2.5 - line 27: Find the
JobLauncher
from Spring context - line 29: Find the
processCDRJob
from Spring context - line 31: Invoke
jobLauncher
run
method toprocessCDRJob
- line 32: Print out the job execution status
5.3 Spring batch application execution
Execute the Spring batch application.
Output
Nov 26, 2017 10:29:56 AM org.springframework.context.annotation.AnnotationConfigApplicationContext prepareRefresh INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@2e5d6d97: startup date [Sun Nov 26 10:29:56 CST 2017]; root of context hierarchy Nov 26, 2017 10:29:56 AM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions INFO: Loading XML bean definitions from class path resource [spring/batchcontext.xml] Nov 26, 2017 10:29:57 AM org.springframework.batch.core.launch.support.SimpleJobLauncher afterPropertiesSet INFO: No TaskExecutor has been set, defaulting to synchronous executor. Nov 26, 2017 10:29:57 AM org.springframework.batch.core.launch.support.SimpleJobLauncher run INFO: Job: [FlowJob: [name=processCDRJob]] launched with the following parameters: [{}] Nov 26, 2017 10:29:57 AM org.springframework.batch.core.job.SimpleStepHandler handleStep INFO: Executing step: [step1] main- Inside BatchItemReader...CallDetailRecord_Simple [callerId=6365272224, calling=3145791114] main- Inside BatchItemProcessor...CallDetailRecord_Simple [callerId=6365272224, calling=3145791114] CallDetailRecordService enrich() main- Inside BatchItemWriter...[CallDetailRecord_Rich [ billingAccount=BTN6365272224, callerId=6365272224, calling=3145791114]] main- Inside BatchItemReader...CallDetailRecord_Simple [callerId=6365272225, calling=3145791115] main- Inside BatchItemProcessor...CallDetailRecord_Simple [callerId=6365272225, calling=3145791115] CallDetailRecordService enrich() main- Inside BatchItemWriter...[CallDetailRecord_Rich [ billingAccount=BTN6365272225, callerId=6365272225, calling=3145791115]] main- Inside BatchItemReader...CallDetailRecord_Simple [callerId=6365272221, calling=3145791111] main- Inside BatchItemProcessor...CallDetailRecord_Simple [callerId=6365272221, calling=3145791111] CallDetailRecordService enrich() main- Inside BatchItemWriter...[CallDetailRecord_Rich [ billingAccount=BTN6365272221, callerId=6365272221, calling=3145791111]] main- Inside BatchItemReader...null Nov 26, 2017 10:29:57 AM org.springframework.batch.core.launch.support.SimpleJobLauncher run INFO: Job: [FlowJob: [name=processCDRJob]] completed with the following parameters: [{}] and the following status: [COMPLETED] main- Exit Status : COMPLETED
As you see here, the Spring Batch application was completed with exist status of “COMPLETED” when the Spring Batch ItemReader
no longer read any item.
- line 8: The log provided from Spring batch framework to launch the
processCDRJob
- line 9-10: The log provided from Spring batch framework to execute batch job
Step 1
- line 25: The log provided from Spring batch framework to display the Job status log
6. Comparison
Quartz is a open source solution for scheduling problem. The data transaction is not part of the Quartz library. Application handles the transaction at the individual record level.
Spring Batch is an open source framework solution for a batch/bulk processing paradigm. The data transaction is part of Spring Batch framework. It helps business focus on the batch data’s operations – reading, processing and writing based on the business logic for each batch job. Spring Batch framework covers logging, transaction, starting, stopping, execution the job.
7. Summary
In this example, we demonstrate how to build Quartz scheduler and Spring Batch application. Both applications execute a specific business task repeatedly in a different way. In a nutshell, Quartz helps businesses decide when to execute the job. Spring Batch helps businesses decide what to include in a batch job’s step.
8. Download the Source Code
This example consists of both Quartz scheduler and Spring Batch application.
You can download the full source code of this example here:Java Quartz vs Spring Batch Example
Hi Mary nice article to introduce the problem of tasks schedule and run. But I disagree with the “vs” in the title because Quartz and Spring Batch are complementary about the problem. One is a scheduler (maybe the de facto standard) the other is a batch framework to build up your job engine. Quartz tells when execute something and if that thing is a job maybe it could be a Spring Batch job. See also https://projects.spring.io/spring-batch/faq.html#quartz
Yes, I also agree. I could be Quartz vs @Scheduled from Spring.