Announcement Announcement Module
Collapse
No announcement yet.
Scheduling persistant jobs with Quartz and Spring Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • Scheduling persistant jobs with Quartz and Spring

    Hi,

    Our application runs on an application server cluster, and we have jobs we need to schedule for periodic execution (eg email sending, database updates). We have chosen quartz to execute the jobs since it can guarantee that the jobs will run on only one node for each execution (ie we are not so interested in the absolute time of the jobs, but making sure that if we schedule the jobs to run every five minutes the job will execute on only one node).

    Initially I looked at the MethodInvokingJobDetailFactoryBean to configure Quartz via Spring, but soon realised that this does not work with a JDBC job store due to the fact that it can't serialize the references to Spring managed beans. This seems a major limitation as one of the main reasons we (and presumably other people) chose Quartz is because of its persistant job store.

    However it doesn't seem very difficult to write something similar to MethodInvokingJobDetailFactoryBean that simply holds the bean and method names and retrieves the bean each time the job is executed.

    eg (just main differences from MethodInvokingJobDetailFactoryBean)


    PersistableMethodInvokingJobDetailFactoryBean

    public void setTargetBeanName(String targetBeanName)
    {
    this.targetBeanName = targetBeanName;
    }

    public void setTargetBeanMethod(String targetBeanMethod)
    {
    this.targetBeanMethod = targetBeanMethod;
    }

    // Serializable / Not Spring Managed
    public void setArguments(Object[] arguments)
    {
    this.arguments = arguments;
    }

    public void afterPropertiesSet() throws ClassNotFoundException, NoSuchMethodException
    {
    // Use specific name if given, else fall back to bean name.
    String name = (this.name != null ? this.name : this.beanName);

    // Consider the concurrent flag to choose between stateful and stateless job.
    Class jobClass = (this.concurrent ? (Class) PersistableMethodInvokingJob.class : StatefulPersistableMethodInvokingJob.class);

    this.jobDetail = new JobDetail(name, this.group, jobClass);
    this.jobDetail.getJobDataMap().put("targetBean", targetBeanName);
    this.jobDetail.getJobDataMap().put("targetMethod", targetBeanMethod);
    this.jobDetail.getJobDataMap().put("arguments", arguments);
    }

    /**
    * Quartz Job implementation that invokes a specified method.
    */
    public static class PersistableMethodInvokingJob extends QuartzJobBean
    {
    private String mTargetMethod;
    private String mTargetBean;
    private Object[] mArguments;
    private ApplicationContext mApplicationContext;

    public void setTargetBean(String aTargetBean)
    {
    mTargetBean = aTargetBean;
    }

    public void setTargetMethod(String aTargetMethod)
    {
    mTargetMethod = aTargetMethod;
    }

    public void setArguments(Object[] aArguments)
    {
    mArguments = aArguments;
    }

    public void setApplicationContext(ApplicationContext aApplicationContext)
    {
    mApplicationContext = aApplicationContext;
    }

    // Dont want to build a proxy each time - too slow
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException
    {
    try
    {
    Object targetBean = mApplicationContext.getBean(mTargetBean);

    MethodInvoker methodInvoker = new MethodInvoker();
    methodInvoker.setTargetObject(targetBean);
    methodInvoker.setTargetMethod(mTargetMethod);
    methodInvoker.setArguments(mArguments);
    methodInvoker.prepare();

    methodInvoker.invoke();
    }
    catch (Exception e)
    {
    String errorMessage = "Could not invoke method '" + mTargetMethod + "' on target bean [" + mTargetBean + "]";

    throw new JobExecutionException(errorMessage, e, false);
    }
    }
    }

    /**
    * Extension of the PersistableMethodInvokingJob , implementing the StatefulJob interface.
    * Quartz checks whether or not jobs are stateful and if so,
    * won't let jobs interfere with each other.
    */
    public static class StatefulPersistableMethodInvokingJob extends PersistableMethodInvokingJob implements StatefulJob
    {
    // No implementation,
    }



    I have this working, but would it make sense to add something like this to core spring to support persistant jobs?

    Additionally we need to apply aspects to all jobs that are executed by the scheduler. One aspect sets up the security context for the job, and another puts the method name called and security context in the LOG4J MDC, so as the scheduled job executions can easily be picked out in the logs.

    Does this seem a sensible approach? What is the standard way to set up acegi security contexts for scheduled jobs?

    The question is how to apply the inteceptors to the jobs (as created above) .

    Ive extended the code above to supply a list of interceptor names to the factory bean, then the executeInternal method pulls these out of the application context also and uses a ReflectiveMethodInvocation to execute the method and apply the interceptors.

    The configuration then looks something like

    <bean id="scheduledJobTemplate" abstract="true"
    class="com.PersistableMethodInvokingJobDetailFacto ryBean">
    <property name="interceptorNames">
    <list>
    <value>scheduledAuthorisationSetupInterceptor</value>
    <value>scheduledLoggingInterceptor</value>
    </list>
    </property>
    </bean>

    <bean id="messageSendingJob" parent="scheduledJobTemplate">
    <property name="targetBeanName" value="messageService"/>
    <property name="targetBeanMethod" value="sendMessages"/>
    </bean>

    <bean id="messageSendingTrigger" class="org.springframework.scheduling.quartz.Simpl eTriggerBean">
    <property name="jobDetail" ref="messageSendingJob"/>
    <property name="repeatInterval" value="30000"/>
    </bean>


    However this may be to much work to be doing each time the job is executed.

    Is there a better way to do this? Or could the ability to add interceptors to jobs be added to spring core also?

    Thanks for any help / comments

    Dave

  • #2
    Hmmm, one thing that is missing though in this 'solution': where do you pass the applicationContext to the PersistableMethodInvokingJob instance?

    Comment


    • #3
      Ah, never mind, I found the solution in the meanwhile.
      The code for PersistableMethodInvokingJob would look like this:

      Code:
      	/**
      	 * Quartz Job implementation that invokes a specified method.
      	 */
      	public static class PersistableMethodInvokingJob extends QuartzJobBean {
      		private String mTargetMethod;
      		private String mTargetBean;
      		private Object[] mArguments;
      
      		public void setTargetBean(String aTargetBean) {
      			mTargetBean = aTargetBean;
      		}
      
      		public void setTargetMethod(String aTargetMethod) {
      			mTargetMethod = aTargetMethod;
      		}
      
      		public void setArguments(Object[] aArguments) {
      			mArguments = aArguments;
      		}
      
      		// Dont want to build a proxy each time - too slow
      		protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
      			try {
      				ApplicationContext appContext = (ApplicationContext)context.getScheduler().getContext().get("applicationContext");
      				Object targetBean = appContext.getBean(mTargetBean);
      
      				MethodInvoker methodInvoker = new MethodInvoker();
      				methodInvoker.setTargetObject(targetBean);
      				methodInvoker.setTargetMethod(mTargetMethod);
      				methodInvoker.setArguments(mArguments);
      				methodInvoker.prepare();
      
      				methodInvoker.invoke();
      			} catch (Exception ex) {
      				String errorMessage = "Could not invoke method '" + mTargetMethod + "' on target bean [" + mTargetBean + "]";
      				throw new JobExecutionException(errorMessage, ex, false);
      			}
      		}
      	}
      and the applicationContext.xml containing the scheduler would look like this:

      Code:
      	<bean id="schedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
      		<property name="triggers">
      			<list>
      				<ref local="triggerA" />
      				<ref local="triggerB" />
      				...
      			</list>
      		</property>
      		<property name="applicationContextSchedulerContextKey" value="applicationContext"/>
      		<property name="nonTransactionalDataSource" ref="nonXADataSource"/>
      		<property name="dataSource" ref="dataSource"/>
      		<property name="transactionManager" ref="transactionManager"/>
      		<property name="configLocation" value="classpath:quartz.properties"/>
      	</bean>
      where 'applicationContextSchedulerContextKey' contains the name of the key under which the applicationContext will get stored in the scheduler's context.

      Comment


      • #4
        The code I posted works also (providing you set the
        <property name="applicationContextSchedulerContextKey" value="applicationContext"/> as you have done)

        If you look at the java doc for this method on the SchedulerFactoryBean it describes how it automatically sets the application context into each job instance (see QuartzJobBean for the scheduler context properties being passed in as bean properties). I think this is cleaner than the lookup you have done as it uses dependency injection.

        Comment


        • #5
          Yes, you're right. Using the lookup as I did results in a dependency on the choosen name for storing the applicationContext. The way you describe it (and as it is described in the javadoc of setApplicationContextSchedulerContextKey(String) in SchedulerFactoryBean) is better, thanks :-)

          Comment


          • #6
            Hi,

            @david_geary

            your code works quite nice. See my implementation below. I managed to not
            re-implement but to extend from MethodInvokingJobDetailFactoryBean.

            Code:
            public class PersistableMethodInvokingJobDetailFactoryBean extends
            		MethodInvokingJobDetailFactoryBean {
            	private String targetBeanName;
            
            	private String targetMethod;
            
            	private Object[] arguments;
            
            	private String name;
            
            	private String group = Scheduler.DEFAULT_GROUP;
            
            	private boolean concurrent = true;
            
            	private String beanName;
            
            	private JobDetail jobDetail;
            
            	private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
            
            	/**
            	 * Set the name of the job. Default is the bean name of this FactoryBean.
            	 * 
            	 * @see org.quartz.JobDetail#setName
            	 */
            	public void setName(String name) {
            		this.name = name;
            	}
            
            	/**
            	 * Set the group of the job. Default is the default group of the Scheduler.
            	 * 
            	 * @see org.quartz.JobDetail#setGroup
            	 * @see org.quartz.Scheduler#DEFAULT_GROUP
            	 */
            	public void setGroup(String group) {
            		this.group = group;
            	}
            
            	/**
            	 * Specify whether or not multiple jobs should be run in a concurrent fashion. The behavior when
            	 * one does not want concurrent jobs to be executed is realized through adding the
            	 * {@link StatefulJob} interface. More information on stateful versus stateless jobs can be
            	 * found <a href="http://www.opensymphony.com/quartz/tutorial.html#jobsMore">here</a>.
            	 * <p>
            	 * The default setting is to run jobs concurrently.
            	 * 
            	 * @param concurrent whether one wants to execute multiple jobs created by this bean
            	 *            concurrently
            	 */
            	public void setConcurrent(boolean concurrent) {
            		this.concurrent = concurrent;
            	}
            
            	public void setBeanName(String beanName) {
            		this.beanName = beanName;
            	}
            
            	public void setBeanClassLoader(ClassLoader classLoader) {
            		this.beanClassLoader = classLoader;
            	}
            
            	protected Class resolveClassName(String className) throws ClassNotFoundException {
            		return ClassUtils.forName(className, this.beanClassLoader);
            	}
            
            	public void afterPropertiesSet() throws ClassNotFoundException, NoSuchMethodException {
            		// Use specific name if given, else fall back to bean name.
            		String name = (this.name != null ? this.name : this.beanName);
            
            		// Consider the concurrent flag to choose between stateful and stateless job.
            		Class jobClass = (this.concurrent ? (Class) PersistableMethodInvokingJob.class
            				: StatefulPersistableMethodInvokingJob.class);
            
            		this.jobDetail = new JobDetail(name, this.group, jobClass);
            		this.jobDetail.getJobDataMap().put("targetBeanName", targetBeanName);
            		this.jobDetail.getJobDataMap().put("targetMethod", targetMethod);
            		this.jobDetail.getJobDataMap().put("arguments", arguments);
            	}
            
            	public Object getObject() {
            		return this.jobDetail;
            	}
            
            	/**
            	 * @return the arguments
            	 */
            	public Object[] getArguments() {
            		return arguments;
            	}
            
            	/**
            	 * @param arguments the arguments to set
            	 */
            	public void setArguments(Object[] arguments) {
            		this.arguments = arguments;
            	}
            
            	/**
            	 * @return the targetBeanName
            	 */
            	public String getTargetBeanName() {
            		return targetBeanName;
            	}
            
            	/**
            	 * @param targetBeanName the targetBeanName to set
            	 */
            	public void setTargetBeanName(String targetBeanName) {
            		this.targetBeanName = targetBeanName;
            	}
            
            	/**
            	 * @return the targetMethod
            	 */
            	public String getTargetMethod() {
            		return targetMethod;
            	}
            
            	/**
            	 * @param targetMethod the targetMethod to set
            	 */
            	public void setTargetMethod(String targetMethod) {
            		this.targetMethod = targetMethod;
            	}
            }

            Comment


            • #7
              I also tried the code and it works great. minor enhancement for non-argument method invoking in PersistableMethodInvokingJobDetailFactoryBean.afte rPropertiesSet() by checking if the arguments is null.

              Comment


              • #8
                i also have a requirement very similar to this. But how do i create new persistent cron jobs programatically and schedlue them with JDBCStore?

                thanks

                Comment


                • #9
                  I am curious to know how the above code with the Object[] arugments worked as JDBC store doesnt allow anything other than string as values within the JobDataMAp when the useProperties is set.

                  Assuming that the above code had set the userproperties to false i wonder how did the properties(beanName ,TargetMethod & arugments) got referenced within the MethodInvokingJobBean as useproperties false will not retrieve the any details of the JOBDataMap.

                  can someone clarify me?

                  thanks

                  Comment


                  • #10
                    Help needed

                    can some one help me out in this.I am stuck up with this for a long time

                    thanks
                    Last edited by rusty.chn; Jan 15th, 2009, 11:41 PM.

                    Comment


                    • #11

                      Comment


                      • #12
                        Is it the right forum for this question?

                        Comment

                        Working...
                        X