Spring Batch Quartz Example
In this article we present an example of scheduling a Spring Batch job to run with a Quartz scheduler. This will be a simple job that executes a Tasklet. We will use an HSQL(which is an in-memory) database table.
The Tasklet will be scheduled to read some data from the table periodically. The sections have been organized as shown below. The example code is available for download at the end of the article.
Table Of Contents
1. Introduction
Before we delve into the example code, here’s a quick reference of the core concepts involved in this example. More content on Spring Batch have been detailed in another JCG article here. Those already familiar with these concepts may skip to the example directly.
Job
: A Job encapsulates the entire batch process. It consists of one or more Steps. A Job is a simple interface andSimpleJob
is its simplest implementation provided by the Spring Batch framework.Step
: A Step is a sequential phase of a batch job. All the processing in a Job actually happens in a Step.Tasklet
: It is one of the ways in which Step processing could be done. It is used when a Step involves just one task like running a Stored Procedure, making a remote call or executing some method etc.Quartz Scheduler
: It is an open-source scheduling library. It is simple and easy-to-use to schedule the running of a job periodically.
2. Example Code
In this example, we will set-up a Tasklet that reads data from an HSQL database table and prints it to the console. The Tasklet will be scheduled to run periodically using a Quartz Scheduler.
2.1 Tools Used
Following are the tools used:
- Eclipse Helios
- Spring Batch 3.0.4.RELEASE
- Spring 4.0.5.RELEASE
- Quartz 1.8.5
- HSQL 1.8.0.7
2.2 Project Set-Up
- Fire up Eclipse from a suitable location
- Go to File->New->Project… as shown in the screenshot below
- We will create a Maven project. So click on Project and in the wizard window choose the creation of a Maven project
- This will be a simple Maven project and we will skip the ‘archeType’ selection as shown below
- In the next window that comes up, provide the project details. We will use the following.
- Next, we will add some folders to the project so that we have the final project structure as shown in the following screenshot.
2.3 Maven Dependency
Open the pom.xml
file and add the following dependencies in it.
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.javacodegeeks.exampl</groupId> <artifactId>SpringBatchQuartz</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <spring.version>4.0.5.RELEASE</spring.version> <spring.batch.version>3.0.4.RELEASE</spring.batch.version> <quartz.scheduler.version>1.8.5</quartz.scheduler.version> <spring.jdbc.version>4.0.5.RELEASE</spring.jdbc.version> <hsql.version>1.8.0.7</hsql.version> <commons.version>1.4</commons.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework.batch</groupId> <artifactId>spring-batch-core</artifactId> <version>${spring.batch.version}</version> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>${quartz.scheduler.version}</version> </dependency> <dependency> <groupId>hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>${hsql.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.jdbc.version}</version> </dependency> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>${commons.version}</version> </dependency> </dependencies> </project>
Note: Spring Quartz 2.x is not compatible with Spring Batch 3.x. Hence, we are using Quartz version 1.x. Using uncompatible versions could trigger exceptions of the following sort.
Caused by: java.lang.IncompatibleClassChangeError: class org.springframework.scheduling.quartz.JobDetailBean has interface org.quartz.JobDetail as super class at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631) at java.lang.ClassLoader.defineClass(ClassLoader.java:615)
2.4 Create HSQL Table
Since our Tasklet will be reading from an HSQL database table, we will write a short script to create a table and insert just one record into it. It will be placed under the ‘hsqldb’ folder as shown in the project structure snapshot above (refer Fig.5). Note that one might achieve much more meaningful tasks in a Tasklet but for our example we will keep it simple. This script will be executed from our context.xml
file as shown in the following sections.
initial-query.sql
DROP TABLE PERSON IF EXISTS; CREATE TABLE PERSON( firstName VARCHAR(20), lastName VARCHAR(20), school VARCHAR(20) ); INSERT INTO PERSON VALUES('New','User','JavaCodeGeeks');
2.5 Set-Up POJOs
Now to map the data read from the HSQL database table, we will need a POJO and a RowMapper for it. These are two simple java classes.
Person.java
package com.javacodegeeks.example.util; public class Person { private String firstName; private String lastName; private String school; public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getSchool() { return school; } public void setSchool(String school) { this.school = school; } @Override public String toString(){ return "Hello! "+ firstName+" "+lastName+", welcome to "+ school+"."; } }
PersonMapper.java
package com.javacodegeeks.example.util; import java.sql.ResultSet; import java.sql.SQLException; import org.springframework.jdbc.core.RowMapper; public class PersonMapper implements RowMapper{ public Person mapRow(ResultSet rs, int rowNum) throws SQLException { Person person = new Person(); person.setFirstName(rs.getString("firstName")); person.setLastName(rs.getString("lastName")); person.setSchool(rs.getString("school")); return person; } }
2.6 Define Tasklet
Next we are going to define our Tasklet. It is again a simple class that implements the Tasklet
interface.
package com.javacodegeeks.example.util; import java.util.ArrayList; import java.util.List; import javax.sql.DataSource; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.jdbc.core.JdbcTemplate; public class MyTasklet implements Tasklet{ private DataSource dataSource; private String sql = "select firstName,lastName,school from PERSON;"; public RepeatStatus execute(StepContribution step, ChunkContext chunk) throws Exception { List person = new ArrayList(); JdbcTemplate myTemplate = new JdbcTemplate(getDataSource()); person = myTemplate.query(sql, new PersonMapper()); for(Person p: person){ System.out.println(p); } return RepeatStatus.FINISHED; } public DataSource getDataSource() { return dataSource; } public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } }
2.7 Configure Scheduler
Almost there! Ok, so now we write our Scheduler. It extends the QuartzJobBean
class. Now this class has a property jobDataAsMap
which is a Map
through which properties can be supplied into this class. We will keep it minimal and just supply the jobName
, jobLauncher
and the jobLocator
to it as can be seen from the configuration in the job-config.xml
file in the following sections. The job will be launched from it based on the cron expression supplied.
MyTaskScheduler.java
package com.javacodegeeks.example.util; import java.util.Map; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.configuration.JobLocator; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.scheduling.quartz.QuartzJobBean; public class MyTaskScheduler extends QuartzJobBean{ private String jobName; private JobLauncher jobLauncher; private JobLocator jobLocator; public JobLauncher getJobLauncher() { return jobLauncher; } public void setJobLauncher(JobLauncher jobLauncher) { this.jobLauncher = jobLauncher; } public JobLocator getJobLocator() { return jobLocator; } public void setJobLocator(JobLocator jobLocator) { this.jobLocator = jobLocator; } @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { @SuppressWarnings("unchecked") Map mapData = context.getMergedJobDataMap(); jobName = (String) mapData.get("jobName"); try{ JobExecution execution = jobLauncher.run(jobLocator.getJob(jobName), new JobParameters()); System.out.println("Execution Status: "+ execution.getStatus()); }catch(Exception e){ System.out.println("Encountered job execution exception! "); e.printStackTrace(); } } }
2.8 Set-Up Context
Under src/main/resources/META-INF/spring
, we will add a context.xml
file with the following content. Here the generic beans required for setting up the context will be configured. Notice the creation of the meta-data tables and the execution of the initial-query.sql
context.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> <bean id="jobRepository" class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="transactionManager" ref="transactionManager" /> <property name="databaseType" value="hsql" /> </bean> <bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher"> <property name="jobRepository" ref="jobRepository" /> </bean> <bean id="transactionManager" class="org.springframework.batch.support.transaction.ResourcelessTransactionManager" /> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" lazy-init="true" destroy-method="close"> <property name="driverClassName" value="org.hsqldb.jdbcDriver" /> <property name="url" value="jdbc:hsqldb:file:src/main/resources/hsqldb/batchcore.db;shutdown=true;" /> <property name="username" value="sa" /> <property name="password" value="" /> </bean> <!-- Create meta-tables --> <jdbc:initialize-database data-source="dataSource"> <jdbc:script location="classpath:hsqldb/initial-query.sql" /> <jdbc:script location="org/springframework/batch/core/schema-drop-hsqldb.sql" /> <jdbc:script location="org/springframework/batch/core/schema-hsqldb.sql" /> </jdbc:initialize-database> </beans>
2.9 Set-up Job
Next, in job-config.xml
, we will configure a Job with a Tasklet in it that reads from the HSQL database table. Note the use of Spring’s SchedulerFactoryBean and Quartz’s JobDetailBean. Our TaskScheduler has been supplied to the latter. Also, the jobRegistry
needs to be set-up so that the jobLocator
could find the configured jobs.
<?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:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> <job id="myJob" xmlns="http://www.springframework.org/schema/batch" restartable="true"> <step id="step1" allow-start-if-complete="true"> <tasklet ref="myTasklet"> </tasklet> </step> </job> <bean id="myTasklet" class="com.javacodegeeks.example.util.MyTasklet"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- run every 10 seconds --> <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean"> <property name="jobDetail" ref="jobDetail" /> <property name="cronExpression" value="*/10 * * * * ?" /> </bean> </property> </bean> <bean id="jobDetail" class="org.springframework.scheduling.quartz.JobDetailBean"> <property name="jobClass" value="com.javacodegeeks.example.util.MyTaskScheduler"></property> <property name="jobDataAsMap"> <map> <entry key="jobName" value="myJob"></entry> <entry key="jobLauncher" value-ref="jobLauncher"></entry> <entry key="jobLocator" value-ref="jobRegistry"></entry> </map> </property> </bean> <bean class="org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor"> <property name="jobRegistry" ref="jobRegistry" /> </bean> <bean id="jobRegistry" class="org.springframework.batch.core.configuration.support.MapJobRegistry" /> </beans>
2.10 Run the Job
Now, in the Main.java
, we will just load the context and run it as a Java Application. The Scheduler will take care of running the tasklet.
Main.java
package com.javacodegeeks.example.app; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Main { public static void main(String[] args) { String[] str = {"classpath:META-INF/spring/context.xml","classpath:META-INF/spring/job-config.xml"}; ApplicationContext ctx = new ClassPathXmlApplicationContext(str); } }
2.11 Output
On running the application, it will print an output as follows every 10 seconds. Since this is the time set in our cron expression above. Note: the output printed from our Tasklet is “Hello! New User, welcome to JavaCodeGeeks.”
Jul 2, 2015 12:10:10 AM org.springframework.batch.core.job.SimpleStepHandler handleStep INFO: Executing step: [step1] Hello! New User, welcome to JavaCodeGeeks. Jul 2, 2015 12:10:10 AM org.springframework.batch.core.launch.support.SimpleJobLauncher run INFO: Job: [FlowJob: [name=myJob]] completed with the following parameters: [{}] and the following status: [COMPLETED] Execution Status: COMPLETED Jul 2, 2015 12:10:20 AM org.springframework.batch.core.launch.support.SimpleJobLauncher run INFO: Job: [FlowJob: [name=myJob]] launched with the following parameters: [{}] Jul 2, 2015 12:10:20 AM org.springframework.batch.core.job.SimpleStepHandler handleStep INFO: Executing step: [step1] Hello! New User, welcome to JavaCodeGeeks. Execution Status: COMPLETED
3. Conclusion
This brings us to the end of the example. It was a pretty simple example with a pretty simple Tasklet that just read a table record and printed it out. Of course, much more meaningful tasks can be accomplished. The idea was just to demonstrate how to go about scheduling a Spring Batch Job using a Quartz Scheduler. The full example code is available for download below.
You can download the full source code of this example here : SpringBatchQuartz