The purpose of this entry is to provide a short tutorial for setting up a Maven2 project that uses Spring 3.0 to schedule a Quartz batch job, to inject Spring-managed beans into this Quartz-managed job, to use Spring to test the job, and to setup a distributable, runnable assembly for the project.
Setup a New Maven Project
Setting up a new Maven project in Eclipse with the m2eclipse plug-in is straightforward, even without the fancy pom editors. For this example, we can create a simple project (skip archetype selection) and package the target as a jar.
Add Repositories and Register Dependencies
Because Spring 3.0.0.M1 is currently not available in the main Maven repositories, we will need to add custom repositories to our pom.xml (Thanks to Chris Beams for his instructions).
We are particularly interested in org.springframework.context.support, which contains the Spring-Quartz bridge that has been separated out from the core spring.jar between Spring 2.5 and 3.0; org.springframework.transaction, which the Spring-Quartz bridge depends on; commons-collections, which the Spring-Quartz bridge also depends on; and quartz from OpenSymphony.
Our new pom.xml should look like this:
NB: As Spring 3.0.0 becomes generally available, registering custom repositories will no longer be necessary, and the necessary versions of the Spring dependencies may change.
Create the Spring Application
We are now ready to register a batch job with Spring in a /src/main/resources/applicationContext.xml file. This registration requires three components: a job declaration, a trigger that depends on the registered job, and a scheduler that depends on the trigger. In our case, we would also like to declare a separate Spring-managed bean and inject that as a dependency into the job. A sample applicationContext.xml would look like this, where our (unimplemented) job will run every five seconds:
NB: Spring injects the myBean dependency through the jobDataAsMap property on the JobDetailBean.
The MyBean.java implementation can be as simple as
MyJob.java contains the logic for our batch job.
NB: For this example, we have created a mutator so myBean can be injected, we print whenever a new Job is constructed, and when the job runs, we print the name of myBean and its instance.
Create a Main Runner
We can easily create a runner for the batch job just to get a feel for what is happening as the Spring container is initialized, as Spring starts the Quartz scheduler and as the Quartz scheduler runs. We simply need to create an ApplicationContext with our applicationContext.xml.
If we run this file as a java application, we see that every five seconds, a new instance of our job is created (which is not necessarily surprising) and the job is executed with the same instance of myBean (which should be anticipated).
Scope MyBean as Prototype
Suppose we want a new instance of myBean for each execution of the job. If Spring were managing the lifecycle of the job instances, this might be as straightforward as declaring the myBean's scope as prototype in the applicationContext.xml.
What happens? The myBean instance that is printed for each run of the job is the same. Quartz, not Spring, is managing the construction of these jobs.
What happens if we specify that the JobDetailBean is a prototype? The instance of myBean in the job is still the same.
Fortunately, it is possible to get a handle on the Spring ApplicationContext from the job by changing only a few lines of code in our applicationContext.xml and in MyJob.java.
Add the Application Context to the Job Data Map
To inject the ApplicationContext into a Job, we need to specify a key for it, so we can retrieve it from the JobExecutionContext's data map. We also will no longer inject the dependency into the job since we will retrieve it directly from the ApplicationContext.
The updated applicationContext.xml should look like this:
NB: we can set the ApplicationContext's key by specifying the applicationContextJobDataKey property for the JobDetailBean.
The updated MyJob.java now retrieves myBean directly from the ApplicationContext.
Test The Batch Job Instance
As we have demonstrated, Spring is not managing the lifecycle of our quartz job directly, only of the JobDetailBean. Whenever I test a Spring-managed bean, I personally prefer that Spring injects an instance of the bean into the test, rather than to call the constructor for that bean directly. After all, the bean under test is a dependency of the test case itself.
While it may not be obvious how we can get an instance of our job inside the test case for that job, it is not difficult, only slightly roundabout. We must specify a JobFactory for the SchedulerFactoryBean explicitly in our applicationContext.xml.
We will also declare a couple of new test-scoped dependencies in our pom.xml.
Finally, we can create a JUnit test case, MyJobTest.java, that simulates the triggering of the job. Whether or not wiring together the scheduler, trigger and factory is better than instantiating the job directly depends on your own preference. This technique is simply one approach.
Create an Assembly
Now that we have a batch process, a test suite and a runner, we can create a distribution. Thanks to a comment by Valerio Schiavoni, creating an executable jar with all its dependencies is easy in Maven2. We can add an incantation to our pom.xml that configures the jar to execute our Main.java:
We will assemble this distribution by invoking Run As -> Maven assembly:assembly from inside Eclipse.
We can run the jar from the command-line by invoking java -jar timezra.blog.spring_batch-0.0.1-SNAPSHOT-jar-with-dependencies.jar.
We now have a robust infrastructure for easily adding new jobs to our runner, for testing those jobs, and for creating an executable distribution.