Monday, April 30, 2012

Scheduling jobs with Quartz + Spring 3

You are asked to schedule a job to run under a certain frequency. How would you solve this requirement?

You can easily add Quartz to your project to meet this requirement. But using Quartz + Spring configuration in your project is a cleaner solution and more powerful.

In this example we will create a simple service that says "Hellooo World!!!!" every one minute using a cron expression. Then we will see how to start the schedule when you deploy your application in your server.

On the first place, let's see the pom file for the scheduler project:

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>ar.com.pabloExample</groupId>
 <artifactId>spring-quartz-example</artifactId>
 <version>1.0-SNAPSHOT</version>
 <packaging>jar</packaging>

 <name>spring-quartz-example</name>
 <url>http://maven.apache.org</url>

 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 </properties>

 <repositories>
  <repository>
   <id>springsource-repo</id>
   <name>SpringSource Repository</name>
   <url>http://repo.springsource.org/release</url>
  </repository>
 </repositories>

 <dependencies>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context</artifactId>
   <version>3.1.1.RELEASE</version>
   <scope>provided</scope>
  </dependency>

  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context-support</artifactId>
   <version>3.1.1.RELEASE</version>
   <scope>provided</scope>
  </dependency>

  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-tx</artifactId>
   <version>3.1.1.RELEASE</version>
   <scope>provided</scope>
  </dependency>

  <dependency>
   <groupId>org.springframework.batch</groupId>
   <artifactId>spring-batch-core</artifactId>
   <version>2.1.8.RELEASE</version>
   <scope>provided</scope>
  </dependency>

  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.8.2</version>
   <scope>test</scope>
  </dependency>
 </dependencies>
</project>

As this scheduler service project will be packaged as a JAR file, every dependency is in provided or test scope. This is because we will package it inside a WAR file that will have the same dependencies but in compile scope.

Now, this is the service to be scheduled:

HelloWorldService

package ar.com.pabloExample.service;

public class HelloWorldService {
 
 public void sayHelloWorld() {
  System.out.println("Hellooo World!!!!");
 }
}

Now, let's create a XML file where we will define the HelloWorldService as a Spring bean and the rest of scheduling configuration beans.

beans-spring.xml

<bean id="helloWorldService" class="ar.com.pabloExample.service.HelloWorldService" />

<bean id="myJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
 <property name="targetObject" ref="helloWorldService" />
 <property name="targetMethod" value="sayHelloWorld" />
</bean>

<bean id="everyMinuteTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
 <property name="jobDetail" ref="myJobDetail" />
 <property name="cronExpression" value="0 0/1 * 1/1 * ? *" />
</bean>

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
 <property name="jobDetails">
  <list>
   <ref bean="myJobDetail" />
  </list>
 </property>
</bean>

Let's see this XML in more detail:

<bean id="myJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
 <property name="targetObject" ref="helloWorldService" />
 <property name="targetMethod" value="sayHelloWorld" />
</bean>
This bean defines the class and method of that class that is going to be scheduled.


<bean id="everyMinuteTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
 <property name="jobDetail" ref="myJobDetail" />
 <property name="cronExpression" value="0 0/1 * 1/1 * ? *" />
</bean>
This bean defines a trigger associated to the previous bean. Here a cron expression is used, this one in particular defines a frequency of one minute. You can easily create cron expressions using online tools like this one.

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
 <property name="triggers">
  <list>
   <ref bean="everyMinuteTrigger" />
  </list>
 </property>
</bean>
This is the bean that can holds the list of jobs chosen to run. When this bean is instantiated, the scheduler starts. An explanation on how to start the scheduler is the next step to be explained.

The result up to now should be a jar file with the class to be scheduled and the bean configuration file.


I already have the schedule configuration, how should I start the schedule?


Ways to initiate the scheduling:
1- Only if using JBoss AS, you can use Snowdrop; it will automatically scan deployed beans and instantiate every bean under *spring.xml name pattern. This is pure server configuration, it is useful for deploying either a WAR or a simple JAR file. Take a look at this post to see the configuration needed, except for the EJB bean injection; you don't need that part.
2- Add a Spring listener in your web.xml that automatically deploys chosen XML files (you define the listener and the chosen bean files in the same web.xml file). Basically this is the same as the above way but within the context of a webapp.


Creating a webapp to deploy the scheduling service


For the most usual case, this is the configuration you should follow to schedule within the context of a webapp (a WAR file):

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/maven-v4_0_0.xsd">
 <modelVersion>4.0.0</modelVersion>
 
 <groupId>ar.com.pabloExample</groupId>
 <artifactId>spring-quartz-web-example</artifactId>
 <packaging>war</packaging>
 <version>1.0-SNAPSHOT</version>
 
 <name>spring-quartz-web-example Maven Webapp</name>
 <url>http://maven.apache.org</url>

 <dependencies>

  <!-- This dependency has the ContextLoaderListener defined in web.xml -->
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-web</artifactId>
   <version>3.1.1.RELEASE</version>
   <scope>compile</scope>
  </dependency>

  <!-- This dependency has the beans-spring.xml with all the quartz config included -->

  <dependency>
   <groupId>ar.com.pabloExample</groupId>
   <artifactId>spring-quartz-example</artifactId>
   <version>1.0-SNAPSHOT</version>
   <scope>compile</scope>
  </dependency>

  <!-- In the war all the jars needed by spring-quartz-example to work the are included -->

  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context</artifactId>
   <version>3.1.1.RELEASE</version>
   <scope>compile</scope>
  </dependency>

  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context-support</artifactId>
   <version>3.1.1.RELEASE</version>
   <scope>compile</scope>
  </dependency>

  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-tx</artifactId>
   <version>3.1.1.RELEASE</version>
   <scope>compile</scope>
  </dependency>

  <dependency>
   <groupId>org.springframework.batch</groupId>
   <artifactId>spring-batch-core</artifactId>
   <version>2.1.8.RELEASE</version>
   <scope>compile</scope>
  </dependency>
 </dependencies>

 <build>
  <finalName>spring-quartz-web-example</finalName>
 </build>

</project>

For the WAR file, the pom has the dependency for the artifact spring-quartz-example which is the jar file that includes the HelloWorldService class and the beans configuration XML that was created in the previous step.

web.xml

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
 <display-name>Archetype Created Web Application</display-name>
 <context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>classpath:beans-spring.xml</param-value>
 </context-param>

 <listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 </listener>
</web-app>

This way, when you deploy your WAR file in the server, the Spring Context listener will scan your beans-spring.xml file and deploy the beans. Nothing else is needed at this point; the schedule has already been started.

3 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Kiến thức của tác giả rất hay, thank bạn đã share.
    Xem tại website : Gia công thạch anh

    ReplyDelete
  3. Bài viết của Chủ thớt rất hay, thank anh đã share.
    Thông tin thêm : Thiềm thừ

    ReplyDelete