A funny thing happened while scheduling a job - No Fluff Just Stuff

A funny thing happened while scheduling a job

Posted by: Craig Walls on January 21, 2008

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.

Craig Walls

About Craig Walls

Craig Walls is a Principal Engineer, Java Champion, Alexa Champion, and the author of Spring AI in Action, Spring in Action, and Build Talking Apps. He's a zealous promoter of the Spring Framework, speaking frequently at local user groups and conferences and writing about Spring. When he's not slinging code, Craig is planning his next trip to Disney World or Disneyland and spending as much time as he can with his wife, two daughters, 1 bird and 2 dogs.

Why Attend the NFJS Tour?

  • » Cutting-Edge Technologies
  • » Agile Practices
  • » Peer Exchange

Current Topics:

  • Languages on the JVM: Scala, Groovy, Clojure
  • Enterprise Java
  • Core Java, Java 8
  • Agility
  • Testing: Geb, Spock, Easyb
  • REST
  • NoSQL: MongoDB, Cassandra
  • Hadoop
  • Spring 4
  • Cloud
  • Automation Tools: Gradle, Git, Jenkins, Sonar
  • HTML5, CSS3, AngularJS, jQuery, Usability
  • Mobile Apps - iPhone and Android
  • More...
Learn More »