Last week, I set out to create and schedule a job to run at 2 a.m. every morning. Not a problem. Spring's SchedulerFactoryBean, CronTriggerBean, and MethodInvokingJobDetailFactoryBean seemed up to task.
So, I configured something resembling the following in the application context:
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<ref bean="trigger" />
</property>
</bean>
<bean id="trigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail" ref="doStuffJob" />
<property name="cronExpression" value="0 0 2 * * ?" />
</bean>
<bean id="doStuffJob"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="stuffDoer" />
<property name="targetMethod" value="doStuff" />
</bean>
<bean id="stuffDoer" class="com.habuma.sched.StuffDoer" />
This is a fairly basic Spring scheduling configuration. The SchedulerFactoryBean is configured with one (or more) triggers that kick off the job(s). In this case, there's only one trigger, a CronTriggerBean configured to trigger the job defined by the "doStuffJob" bean at 2 a.m. As for "doStuffJob", it's a MethodInvokingJobDetailFactoryBean a factory bean that produces a Quartz job that invokes the doStuff() method on the "stuffDoer" bean. (The implementation of the "stuffDoer" bean is irrelevant to this discussion...I'll leave it to the imagination of the reader.)
This should've been a two minute task. Register the beans, then move on to some other more interesting work. And it would've been a two minute task except for one minor detail: It didn't work.
Before you say anything, no, I didn't wait until 2 a.m. to see the job triggered. Instead, I tweaked the cronExpression property to fire every 5 seconds ("0,5,10,15,20,25,30,35,40,45,50,55 * * * * ?") and expected to see the doStuff() method invoked every 5 seconds. Once I was satisfied that all the beans were wired up right, I'd change the cronExpression to trigger at 2am. But nope...it didn't work.
As you can imagine, this was quite frustrating and that frustration only increased as time went on. After 3 hours of hair-pulling, I finally figured it out. For the benefit of my readers and so that they don't experience the same exasperation as I, I now share the solution here:
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean"
lazy-init="false">
<property name="triggers">
<ref bean="trigger" />
</property>
</bean>
<bean id="trigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail" ref="doStuffJob" />
<property name="cronExpression" value="0 0 2 * * ?" />
</bean>
<bean id="doStuffJob"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="stuffDoer" />
<property name="targetMethod" value="doStuff" />
</bean>
<bean id="stuffDoer" class="com.habuma.sched.StuffDoer" />
Did you spot the difference? You see, my problem was that my <beans> element was configured with default-lazy-init="true" (for reasons I won't go into here). Consequently, Spring was waiting to instantiate all beans, including SchedulerFactoryBean, until someone needed them. As it turns out, nobody every asked the container for the SchedulerFactoryBean, so Spring never created it. Therefore, no scheduling every happened.
Lesson learned: Make sure that SchedulerFactoryBean is always eagerly initialized by the Spring container or else it won't work.
And now for the rest of the story
As I waged battle with SchedulerFactoryBean, two things struck me:
- I shouldn't have to remember to set
lazy-initto false. - That's a lot of XML to do something so simple as schedule a job.
That led me to create a new "sched" namespace. Using the new "sched" namespace, the 2 a.m. job above could be configured much more simply:
<?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:sched="http://www.springinaction.com/schema/sched"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springinaction.com/schema/sched
http://www.springinaction.com/schema/sched-1.0.xsd"
default-lazy-init="true">
<bean id="stuffDoer" class="com.habuma.sched.StuffDoer" />
<sched:cron-job
target-bean="stuffDoer"
target-method="doStuff"
cron-expression="0 0 2 * * ?" />
</beans>
The <sched:cron-job> element combines the configuration of SchedulerFactoryBean, CronTriggerBean, and MethodInvokingJobDetailFactoryBean into on simple 4-line snippet of XML. The target-bean and target-method attributes point at a bean and method to invoke at the times specified by the cron-expression attribute. What's more, the SchedulerFactoryBean that is created behind the scenes will always have its lazy-init set to false.
But that's not all that the "sched" namespace provides. There's also a <sched:timer-job> that uses a SimpleTriggerBean instead of CronTriggerBean to trigger a job at a specified interval. For example, to invoke the doStuff() method every 60 seconds:
<sched:timer-job
target-bean="stuffDoer"
target-method="doStuff"
interval="60000" />
Optionally, you can specify a start-delay and/or a repeat-count to specify how much time should elapse before the first invocation of doStuff() and the number of times (after the first time) that doStuff() should be invoked:
<sched:timer-job
target-bean="stuffDoer"
target-method="doStuff"
interval="60000"
start-delay="120000"
repeat-count="10" />
Here, I've specified that doStuff() should be invoked every 60 seconds, starting after the first 2 minutes, and be invoked 11 times (the first time plus ten more times).
There's plenty of room for the "sched" namespace to grow, but it's a good start and it solves many scheduling problems as-is.
In a few weeks, I'll blog on how I created this namespace. In the meantime, you can download the namespace's JAR, add it to your classpath, and start using it to configure your scheduled jobs. I've also submitted an issue along with a set of files to Spring's issue tracking to have this namespace added to Spring itself. If you'd like to see this namespace added to Spring, go to SPR-4359 and place your vote.