Saturday, July 25, 2015

Spring 3.2.3 + Quartz 2.2.1 integration example (1) - configuration in Spring context XML

Recently, I am assigned a task of integrating Quartz into a Spring project. I find it's quite an interesting job and would like to share some of my findings.

Quartz is a very popular scheduling library and is used in many Java applications to run jobs. It is open source, simple to use and most importantly, it is free. Here is the Quartz website Quartz Scheduler.

In this post, I'd share how to integrate Quartz into Spring. And how to schedule some simple jobs using Spring's JobDetailFactory and MethodInvokingJobDetailFactory in a simple trigger and CRON trigger respectively.

Environment

Java 8
Maven 3
Spring 3.2.3
Quartz 2.2.2
SFL4J 1.7.9 (to show Quartz start up information)

Project configuration

Create a new Maven project and add the following dependencies to the pom file (Assume Java 8 and Maven 3 have already been installed).
   <dependencies>  
     <dependency>  
       <groupId>org.springframework</groupId>  
       <artifactId>spring-core</artifactId>  
       <version>${spring.version}</version>  
     </dependency>  
     <dependency>  
       <groupId>org.springframework</groupId>  
       <artifactId>spring-context-support</artifactId>  
       <version>${spring.version}</version>  
     </dependency>  
     <!-- Transaction dependency is required with Quartz integration -->  
     <dependency>  
       <groupId>org.springframework</groupId>  
       <artifactId>spring-tx</artifactId>  
       <version>${spring.version}</version>  
     </dependency>  
     <!-- Quartz framework -->  
     <dependency>  
       <groupId>org.quartz-scheduler</groupId>  
       <artifactId>quartz</artifactId>  
       <version>${quartz.version}</version>  
     </dependency>  
     <dependency>  
       <groupId>org.slf4j</groupId>  
       <artifactId>slf4j-api</artifactId>  
       <version>${slf4j.version}</version>  
     </dependency>  
     <dependency>  
       <groupId>org.slf4j</groupId>  
       <artifactId>slf4j-simple</artifactId>  
       <version>${slf4j.version}</version>  
     </dependency>  
   </dependencies>  
   <properties>  
     <spring.version>3.2.3.RELEASE</spring.version>  
     <quartz.version>2.2.1</quartz.version>  
     <slf4j.version>1.7.9</slf4j.version>  
   </properties>

Quartz job and trigger configuration

The first job class simply print a greeting message. I print the date time at the beginning of the greeting so we can know when the job is triggered by Quartz.

public class HelloWorldTask {

    public void sayHello() {
        System.out.println("[" + new Date() + "] Hello from Quartz! World");
    }
}
Then let's schedule the job into Quartz. Firstly, we need configure the JobDetail bean in Spring context file. We'll use the MethodInvokingJobDetailFacrotyBean, it allows to specify which class and which method in the class to run when the job is triggered. For details about MethodInvokingJobDetailFacrotyBean, please read the Spring docs.

   <bean id="helloWorldTask" class="com.geekspearls.quartz.task.HelloWorldTask"/>
   
   <bean id="helloWorldJob"  
      class = "org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">  
     <property name="targetObject" ref="helloWorldTask"/>  
     <property name="targetMethod" value="sayHello"/>  
   </bean>

Secondly, we need to add our job to a trigger. We will use the SimpleTriggerFactoryBean to schedule our first job and set the job to be run every 5 seconds and delay 1 second to start. For details about SimpleTriggerFactoryBean, please read the Spring docs.

  <bean id="simpleTrigger"  
      class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">  
     <property name="jobDetail" ref="helloWorldJob" />  
     <property name="repeatInterval" value="5000" />  
     <property name="startDelay" value="1000" />  
   </bean>  

Then let's create our second job. The second job class extends QuartzJobBean interface. When it is triggered, executeInternal method will be invoked. Inside the method, it takes the value of 'name' from JobDataMap which we set in the jobDetails configuration and print a greeting message. The date time will also be printed out so we can know when the job is triggered.

public class SayHelloTask extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        String name = jobExecutionContext.getMergedJobDataMap().getString("name");
        System.out.println("[" + new Date() + "] Hello from Quartz! " + name);
    }
}

For our second job, we will use the JobDetailFactoryBean to configure it in Spring context file and pass in a value of 'name' into jobDataMap.
  <bean id="sayHelloJob"  
      class="org.springframework.scheduling.quartz.JobDetailFactoryBean">  
     <property name="jobClass" value="com.geekspearls.quartz.task.SayHelloTask"/>  
     <property name="jobDataAsMap">  
       <map>  
         <entry key="name" value="Geek's pearls reader"/>  
       </map>  
     </property>  
     <property name="durability" value="true"/>  
   </bean> 

Then we schedule this job with CronTriggerFactoryBean. It is scheduled to run every 10 seconds start from every 0 second. For details about CRON, please read Quartz CronTrigger Tutorial
  <bean id="cronTrigger"  
      class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">  
     <property name="jobDetail" ref="sayHelloJob" />  
     <property name="cronExpression" value="0/10 * * * * ?"/>  
   </bean> 

Now, we have our jobs and triggers ready. Let's add them into Quartz scheduler.
   <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">  
     <property name="jobDetails">  
       <list>  
         <ref bean="helloWorldJob" />  
         <ref bean="sayHelloJob" />  
       </list>  
     </property>  
     <property name="triggers">  
       <list>  
         <ref bean="simpleTrigger" />  
         <ref bean="cronTrigger" />  
       </list>  
     </property>  
   </bean>

Finally, everything is ready. The completed Spring context XML file is below.
   <beans xmlns="http://www.springframework.org/schema/beans"  
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">  
   <bean id="sayHelloJob"  
      class="org.springframework.scheduling.quartz.JobDetailFactoryBean">  
     <property name="jobClass" value="com.geekspearls.quartz.task.SayHelloTask"/>  
     <property name="jobDataAsMap">  
       <map>  
         <entry key="name" value="Geek's pearls reader"/>  
       </map>  
     </property>  
     <property name="durability" value="true"/>  
   </bean>  
   <bean id="helloWorldTask" class="com.geekspearls.quartz.task.HelloWorldTask"/>  
   <bean id="helloWorldJob"  
      class = "org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">  
     <property name="targetObject" ref="helloWorldTask"/>  
     <property name="targetMethod" value="sayHello"/>  
   </bean>  
   <bean id="simpleTrigger"  
      class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">  
     <property name="jobDetail" ref="helloWorldJob" />  
     <property name="repeatInterval" value="5000" />  
     <property name="startDelay" value="1000" />  
   </bean>  
   <bean id="cronTrigger"  
      class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">  
     <property name="jobDetail" ref="sayHelloJob" />  
     <property name="cronExpression" value="0/10 * * * * ?"/>  
   </bean>  
   <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">  
     <property name="jobDetails">  
       <list>  
         <ref bean="helloWorldJob" />  
         <ref bean="sayHelloJob" />  
       </list>  
     </property>  
     <property name="triggers">  
       <list>  
         <ref bean="simpleTrigger" />  
         <ref bean="cronTrigger" />  
       </list>  
     </property>  
   </bean>  
 </beans>

Now, let's test it! Run the following class as Java application.
public class HelloWorld {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        System.out.println("Application started [" + new Date() + "]");
    }
}

In the log and console output, we can see the information below. Firstly, it tells us a Quartz scheduler with instanceId 'NON_CLUSTER' running locally with 10 threads. And it doesn't support persistence and cluster. I will share how to use clustered Quartz scheduler in my next post.
[main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.2.1) 'org.springframework.scheduling.quartz.SchedulerFactoryBean#0' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
  Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.

Then we can see the application is started at 17:14:23. 1 second later, our first job is triggered and repeated every 5 seconds. At 17:14:30, our second job is triggered and repeated every 10 seconds. That's what we expected! The completed example can be found on my GitHub here. I hope you will enjoy it!
Application started [Sat Jul 25 17:14:23 AEST 2015]
[Sat Jul 25 17:14:24 AEST 2015] Hello from Quartz! World
[Sat Jul 25 17:14:29 AEST 2015] Hello from Quartz! World
[Sat Jul 25 17:14:30 AEST 2015] Hello from Quartz! Geek's pearls reader
[Sat Jul 25 17:14:34 AEST 2015] Hello from Quartz! World
[Sat Jul 25 17:14:39 AEST 2015] Hello from Quartz! World
[Sat Jul 25 17:14:40 AEST 2015] Hello from Quartz! Geek's pearls reader
[Sat Jul 25 17:14:44 AEST 2015] Hello from Quartz! World
[Sat Jul 25 17:14:49 AEST 2015] Hello from Quartz! World
[Sat Jul 25 17:14:50 AEST 2015] Hello from Quartz! Geek's pearls reader
[Sat Jul 25 17:14:54 AEST 2015] Hello from Quartz! World
[Sat Jul 25 17:14:59 AEST 2015] Hello from Quartz! World
[Sat Jul 25 17:15:00 AEST 2015] Hello from Quartz! Geek's pearls reader
[Sat Jul 25 17:15:04 AEST 2015] Hello from Quartz! World
[Sat Jul 25 17:15:09 AEST 2015] Hello from Quartz! World
[Sat Jul 25 17:15:10 AEST 2015] Hello from Quartz! Geek's pearls reader
[Sat Jul 25 17:15:14 AEST 2015] Hello from Quartz! World
[Sat Jul 25 17:15:19 AEST 2015] Hello from Quartz! World
[Sat Jul 25 17:15:20 AEST 2015] Hello from Quartz! Geek's pearls reader
[Sat Jul 25 17:15:24 AEST 2015] Hello from Quartz! World
[Sat Jul 25 17:15:29 AEST 2015] Hello from Quartz! World
[Sat Jul 25 17:15:30 AEST 2015] Hello from Quartz! Geek's pearls reader
[Sat Jul 25 17:15:34 AEST 2015] Hello from Quartz! World
[Sat Jul 25 17:15:39 AEST 2015] Hello from Quartz! World
[Sat Jul 25 17:15:40 AEST 2015] Hello from Quartz! Geek's pearls reader
...