Wednesday, August 10, 2011

Dynamically Creates Quartz Scheduler using Spring

 Here are  my requirements: I need to dynamically create quartz cron triggers and add them to my scheduler. All my triggers schedule the same job but with trigger specific job mapping data.

Before I show how to dynamically create quartz schedulers using Spring, let's quickly review Sping's static configuration. The following excerpts come from "Chapter 23 Scheduling and Tread Pooling" in Spring 2.5.6 reference document and shows how to statically create a quartz scheduler:

<bean name="exampleJob" class="org.springframework.scheduling.quartz.JobDetailBean">
   <property name="jobClass" value="example.ExampleJob"/>
</bean>

<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
   <property name="jobDetail" ref="exampleJob" />
   <!-- run every morning at 6 AM -->
   <property name="cronExpression" value="0 0 6 * * ?" />
</bean>

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

Spring's JobDetailBean extends quartz's JobDetail and provides the following added functions:
  1. Setups sensible defaults in its life-cycle init method afterPropertiesSet() e.g. set the job's name to the bean name and set the job's group name to "DEFAULT" if they are empty (Remember that quartz scheduler uniquely identifies a job by its name and group);
  2. Supports any job class that implements the Runnable interface besides quartz'a regular Job interface;
  3. If your actual Job class extends Spring's QuartzJobBean, Spring also automatically injects the job map data to the job's properties;
Spring's CronTriggerBean extends quartz CronTrigger and provides the following added functions:
  1. Setups sensible defaults in its life-cycle init method afterPropertiesSet() e.g. set the trigger's name to the bean name and set the trigger's group name to "DEFAULT" if they are empty (Remember that quartz scheduler uniquely identifies a trigger by its name and group).
  2. Associates the trigger to the provided's JobDetail in afterPropertiesSet();
  3. Set the trigger's timezone to the default one and start time to the current time afterPropertiesSet();
  4. Sets trigger specific job map data through the Java's Map interface;
Spring's SchedulerFactorBean is a quartz Scheduler factory bean and wires jobs and triggers together. It implements many functions behind the scene that you have to do by yourself  if you directly use quartz API:
  1. Overrides default quartz properties e.g. thread pool parameters and jmx export;
  2. Registers triggers;
  3. Registers jobs. If you already assigned a job in your Spring trigger bean, you don't need this;
  4. Sets the JobFactory to the default Spring's AdaptableJobFactory if it is not set by you. AdaptableJobFactory support Item 2 in the above JobDetailBean's added functions. AdaptableJobFactory's sublass SpringBeanJobFactory also supports the same function as mentioned in Item 3 in the above JobDetailBean's added function.
  5. Schedules your jobs with quartz's scheduler;
All the above functions are implemented in the Spring' init method afterPropertiesSet().

Here are the steps to dynamically configure quartz scheduler using Spring to meet my above requirements.
  1. Still configures my JobDetailBean, and my actual Job also extends Spring's QuartzJobBean in order to take advantage of the added function;
  2. Still configures my CronTriggerBean using Spring  in order to take advantage of the added function. But its scope should be "prototype" so that I can dynamically retrieve a new trigger bean instance and set its cronExpression and its job map data.
    Here is the new trigger configuration:

    <bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean" scope="prototype">
    <property name="jobDetail" ref="exampleJob"/>
    </bean>

    Here is the pseudo code:
    CronTrigger ct = (CronTrigger)appCtx.getBean("cronTrigger");
    ct.setName(ct.getName() + some-distinguisher);
    ct.getJobDataMap().put(some-key, some-value);
    ct.setCronExpression(some-exp); 

  3. Still configures my SchedulerFactorBean. But I need to add my above triggers to the scheduler. Because I create the above trigger after SchedulerFactorBean's afterPropertiesSet(), the trigger's associated JobDetail is not added to the scheduler. But I can explicitly register the JobDetail:

    <bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean" lazy-init="false">
       <property name="jobDetails">
          <list>
            <ref bean="exampleJob"/>
          </list>
       </property>
       <property name="triggers"> <!-- will dynamically add more triggers -->
          <list/>
       </property>
       <property name="autoStartup" value="true"/>
       <property name="quartzProperties">
          <props>
            <prop key="org.quartz.scheduler.jmx.export">true</prop>
            <prop key="org.quartz.threadPool.threadCount">2</prop>
          </props>
       </property>
    </bean>
    Here is the pseudo code:
    Scheduler sched = (Scheduler)appCtx.getBean("scheduler");
    sched.scheduleJob(ct);

5 comments:

  1. why throw:

    Invocation of init method failed; nested exception is java.lang.NoClassDefFoundError: org/apache/commons/modeler/Registry

    thanks!

    ReplyDelete
  2. this is easy to fix.
    You also enabled the jmx export by including "true" in the config", but your classpath doesn't have the following needed jar:
    commons-modeler-version.jar (i am using commons-modeler-2.0.1.jar)

    ReplyDelete
    Replies
    1. That's cool, but how to change the thread pool size dynamically?

      Delete
  3. Finally. I've looking for this for hours. A million thanks Yong!

    ReplyDelete
  4. Thank you! This helped me too.

    ReplyDelete