001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.scheduling.quartz; 018 019import java.lang.reflect.InvocationTargetException; 020 021import org.apache.commons.logging.Log; 022import org.apache.commons.logging.LogFactory; 023import org.quartz.DisallowConcurrentExecution; 024import org.quartz.Job; 025import org.quartz.JobDetail; 026import org.quartz.JobExecutionContext; 027import org.quartz.JobExecutionException; 028import org.quartz.PersistJobDataAfterExecution; 029import org.quartz.Scheduler; 030import org.quartz.impl.JobDetailImpl; 031 032import org.springframework.beans.factory.BeanClassLoaderAware; 033import org.springframework.beans.factory.BeanFactory; 034import org.springframework.beans.factory.BeanFactoryAware; 035import org.springframework.beans.factory.BeanNameAware; 036import org.springframework.beans.factory.FactoryBean; 037import org.springframework.beans.factory.InitializingBean; 038import org.springframework.beans.support.ArgumentConvertingMethodInvoker; 039import org.springframework.lang.Nullable; 040import org.springframework.util.Assert; 041import org.springframework.util.ClassUtils; 042import org.springframework.util.MethodInvoker; 043 044/** 045 * {@link org.springframework.beans.factory.FactoryBean} that exposes a 046 * {@link org.quartz.JobDetail} object which delegates job execution to a 047 * specified (static or non-static) method. Avoids the need for implementing 048 * a one-line Quartz Job that just invokes an existing service method on a 049 * Spring-managed target bean. 050 * 051 * <p>Inherits common configuration properties from the {@link MethodInvoker} 052 * base class, such as {@link #setTargetObject "targetObject"} and 053 * {@link #setTargetMethod "targetMethod"}, adding support for lookup of the target 054 * bean by name through the {@link #setTargetBeanName "targetBeanName"} property 055 * (as alternative to specifying a "targetObject" directly, allowing for 056 * non-singleton target objects). 057 * 058 * <p>Supports both concurrently running jobs and non-currently running 059 * jobs through the "concurrent" property. Jobs created by this 060 * MethodInvokingJobDetailFactoryBean are by default volatile and durable 061 * (according to Quartz terminology). 062 * 063 * <p><b>NOTE: JobDetails created via this FactoryBean are <i>not</i> 064 * serializable and thus not suitable for persistent job stores.</b> 065 * You need to implement your own Quartz Job as a thin wrapper for each case 066 * where you want a persistent job to delegate to a specific service method. 067 * 068 * <p>Compatible with Quartz 2.1.4 and higher, as of Spring 4.1. 069 * 070 * @author Juergen Hoeller 071 * @author Alef Arendsen 072 * @since 18.02.2004 073 * @see #setTargetBeanName 074 * @see #setTargetObject 075 * @see #setTargetMethod 076 * @see #setConcurrent 077 */ 078public class MethodInvokingJobDetailFactoryBean extends ArgumentConvertingMethodInvoker 079 implements FactoryBean<JobDetail>, BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean { 080 081 @Nullable 082 private String name; 083 084 private String group = Scheduler.DEFAULT_GROUP; 085 086 private boolean concurrent = true; 087 088 @Nullable 089 private String targetBeanName; 090 091 @Nullable 092 private String beanName; 093 094 @Nullable 095 private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); 096 097 @Nullable 098 private BeanFactory beanFactory; 099 100 @Nullable 101 private JobDetail jobDetail; 102 103 104 /** 105 * Set the name of the job. 106 * <p>Default is the bean name of this FactoryBean. 107 */ 108 public void setName(String name) { 109 this.name = name; 110 } 111 112 /** 113 * Set the group of the job. 114 * <p>Default is the default group of the Scheduler. 115 * @see org.quartz.Scheduler#DEFAULT_GROUP 116 */ 117 public void setGroup(String group) { 118 this.group = group; 119 } 120 121 /** 122 * Specify whether or not multiple jobs should be run in a concurrent fashion. 123 * The behavior when one does not want concurrent jobs to be executed is 124 * realized through adding the {@code @PersistJobDataAfterExecution} and 125 * {@code @DisallowConcurrentExecution} markers. 126 * More information on stateful versus stateless jobs can be found 127 * <a href="https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/tutorial-lesson-03.html">here</a>. 128 * <p>The default setting is to run jobs concurrently. 129 */ 130 public void setConcurrent(boolean concurrent) { 131 this.concurrent = concurrent; 132 } 133 134 /** 135 * Set the name of the target bean in the Spring BeanFactory. 136 * <p>This is an alternative to specifying {@link #setTargetObject "targetObject"}, 137 * allowing for non-singleton beans to be invoked. Note that specified 138 * "targetObject" and {@link #setTargetClass "targetClass"} values will 139 * override the corresponding effect of this "targetBeanName" setting 140 * (i.e. statically pre-define the bean type or even the bean object). 141 */ 142 public void setTargetBeanName(String targetBeanName) { 143 this.targetBeanName = targetBeanName; 144 } 145 146 @Override 147 public void setBeanName(String beanName) { 148 this.beanName = beanName; 149 } 150 151 @Override 152 public void setBeanClassLoader(ClassLoader classLoader) { 153 this.beanClassLoader = classLoader; 154 } 155 156 @Override 157 public void setBeanFactory(BeanFactory beanFactory) { 158 this.beanFactory = beanFactory; 159 } 160 161 @Override 162 protected Class<?> resolveClassName(String className) throws ClassNotFoundException { 163 return ClassUtils.forName(className, this.beanClassLoader); 164 } 165 166 167 @Override 168 public void afterPropertiesSet() throws ClassNotFoundException, NoSuchMethodException { 169 prepare(); 170 171 // Use specific name if given, else fall back to bean name. 172 String name = (this.name != null ? this.name : this.beanName); 173 174 // Consider the concurrent flag to choose between stateful and stateless job. 175 Class<? extends Job> jobClass = (this.concurrent ? MethodInvokingJob.class : StatefulMethodInvokingJob.class); 176 177 // Build JobDetail instance. 178 JobDetailImpl jdi = new JobDetailImpl(); 179 jdi.setName(name != null ? name : toString()); 180 jdi.setGroup(this.group); 181 jdi.setJobClass(jobClass); 182 jdi.setDurability(true); 183 jdi.getJobDataMap().put("methodInvoker", this); 184 this.jobDetail = jdi; 185 186 postProcessJobDetail(this.jobDetail); 187 } 188 189 /** 190 * Callback for post-processing the JobDetail to be exposed by this FactoryBean. 191 * <p>The default implementation is empty. Can be overridden in subclasses. 192 * @param jobDetail the JobDetail prepared by this FactoryBean 193 */ 194 protected void postProcessJobDetail(JobDetail jobDetail) { 195 } 196 197 198 /** 199 * Overridden to support the {@link #setTargetBeanName "targetBeanName"} feature. 200 */ 201 @Override 202 public Class<?> getTargetClass() { 203 Class<?> targetClass = super.getTargetClass(); 204 if (targetClass == null && this.targetBeanName != null) { 205 Assert.state(this.beanFactory != null, "BeanFactory must be set when using 'targetBeanName'"); 206 targetClass = this.beanFactory.getType(this.targetBeanName); 207 } 208 return targetClass; 209 } 210 211 /** 212 * Overridden to support the {@link #setTargetBeanName "targetBeanName"} feature. 213 */ 214 @Override 215 public Object getTargetObject() { 216 Object targetObject = super.getTargetObject(); 217 if (targetObject == null && this.targetBeanName != null) { 218 Assert.state(this.beanFactory != null, "BeanFactory must be set when using 'targetBeanName'"); 219 targetObject = this.beanFactory.getBean(this.targetBeanName); 220 } 221 return targetObject; 222 } 223 224 225 @Override 226 @Nullable 227 public JobDetail getObject() { 228 return this.jobDetail; 229 } 230 231 @Override 232 public Class<? extends JobDetail> getObjectType() { 233 return (this.jobDetail != null ? this.jobDetail.getClass() : JobDetail.class); 234 } 235 236 @Override 237 public boolean isSingleton() { 238 return true; 239 } 240 241 242 /** 243 * Quartz Job implementation that invokes a specified method. 244 * Automatically applied by MethodInvokingJobDetailFactoryBean. 245 */ 246 public static class MethodInvokingJob extends QuartzJobBean { 247 248 protected static final Log logger = LogFactory.getLog(MethodInvokingJob.class); 249 250 @Nullable 251 private MethodInvoker methodInvoker; 252 253 /** 254 * Set the MethodInvoker to use. 255 */ 256 public void setMethodInvoker(MethodInvoker methodInvoker) { 257 this.methodInvoker = methodInvoker; 258 } 259 260 /** 261 * Invoke the method via the MethodInvoker. 262 */ 263 @Override 264 protected void executeInternal(JobExecutionContext context) throws JobExecutionException { 265 Assert.state(this.methodInvoker != null, "No MethodInvoker set"); 266 try { 267 context.setResult(this.methodInvoker.invoke()); 268 } 269 catch (InvocationTargetException ex) { 270 if (ex.getTargetException() instanceof JobExecutionException) { 271 // -> JobExecutionException, to be logged at info level by Quartz 272 throw (JobExecutionException) ex.getTargetException(); 273 } 274 else { 275 // -> "unhandled exception", to be logged at error level by Quartz 276 throw new JobMethodInvocationFailedException(this.methodInvoker, ex.getTargetException()); 277 } 278 } 279 catch (Exception ex) { 280 // -> "unhandled exception", to be logged at error level by Quartz 281 throw new JobMethodInvocationFailedException(this.methodInvoker, ex); 282 } 283 } 284 } 285 286 287 /** 288 * Extension of the MethodInvokingJob, implementing the StatefulJob interface. 289 * Quartz checks whether or not jobs are stateful and if so, 290 * won't let jobs interfere with each other. 291 */ 292 @PersistJobDataAfterExecution 293 @DisallowConcurrentExecution 294 public static class StatefulMethodInvokingJob extends MethodInvokingJob { 295 296 // No implementation, just an addition of the tag interface StatefulJob 297 // in order to allow stateful method invoking jobs. 298 } 299 300}