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-init
to 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.