ScheduledAnnotationBeanPostProcessor.java 15.5 KB
Newer Older
1
/*
2
 * Copyright 2002-2015 the original author or authors.
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.scheduling.annotation;

import java.lang.reflect.Method;
20
import java.lang.reflect.Modifier;
21 22
import java.util.Collections;
import java.util.LinkedHashSet;
23
import java.util.Map;
24
import java.util.Set;
25
import java.util.TimeZone;
26
import java.util.concurrent.ConcurrentHashMap;
C
Chris Beams 已提交
27
import java.util.concurrent.ScheduledExecutorService;
28

29 30 31
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

32
import org.springframework.aop.support.AopUtils;
33 34
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
35
import org.springframework.beans.factory.DisposableBean;
36
import org.springframework.beans.factory.ListableBeanFactory;
37 38
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
39
import org.springframework.beans.factory.SmartInitializingSingleton;
40
import org.springframework.beans.factory.config.BeanPostProcessor;
41
import org.springframework.context.ApplicationContext;
42 43
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
44
import org.springframework.context.EmbeddedValueResolverAware;
45
import org.springframework.context.event.ContextRefreshedEvent;
46 47
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
C
Chris Beams 已提交
48
import org.springframework.scheduling.TaskScheduler;
C
Chris Beams 已提交
49
import org.springframework.scheduling.Trigger;
50 51
import org.springframework.scheduling.config.CronTask;
import org.springframework.scheduling.config.IntervalTask;
52
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
53
import org.springframework.scheduling.support.CronTrigger;
54
import org.springframework.scheduling.support.ScheduledMethodRunnable;
55 56 57
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.MethodCallback;
58
import org.springframework.util.StringUtils;
59
import org.springframework.util.StringValueResolver;
60 61

/**
C
Chris Beams 已提交
62
 * Bean post-processor that registers methods annotated with @{@link Scheduled}
63 64
 * to be invoked by a {@link org.springframework.scheduling.TaskScheduler} according
 * to the "fixedRate", "fixedDelay", or "cron" expression provided via the annotation.
65
 *
C
Chris Beams 已提交
66
 * <p>This post-processor is automatically registered by Spring's
67 68
 * {@code <task:annotation-driven>} XML element, and also by the
 * @{@link EnableScheduling} annotation.
C
Chris Beams 已提交
69
 *
70 71 72
 * <p>Autodetects any {@link SchedulingConfigurer} instances in the container,
 * allowing for customization of the scheduler to be used or for fine-grained
 * control over task registration (e.g. registration of {@link Trigger} tasks.
73
 * See the @{@link EnableScheduling} javadocs for complete usage details.
C
Chris Beams 已提交
74
 *
75
 * @author Mark Fisher
76
 * @author Juergen Hoeller
C
Chris Beams 已提交
77
 * @author Chris Beams
78
 * @author Elizabeth Chatman
79 80
 * @since 3.0
 * @see Scheduled
C
Chris Beams 已提交
81
 * @see EnableScheduling
C
Chris Beams 已提交
82
 * @see SchedulingConfigurer
83
 * @see org.springframework.scheduling.TaskScheduler
C
Chris Beams 已提交
84
 * @see org.springframework.scheduling.config.ScheduledTaskRegistrar
85
 */
86
public class ScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, Ordered,
87 88
		EmbeddedValueResolverAware, BeanFactoryAware, ApplicationContextAware,
		SmartInitializingSingleton, ApplicationListener<ContextRefreshedEvent>, DisposableBean {
89

90 91 92 93 94 95 96 97
	/**
	 * The default name of the TaskScheduler bean to pick up: "taskScheduler".
	 * <p>Note that the initial lookup happens by type; this is just the fallback
	 * in case of multiple scheduler beans found in the context.
	 */
	public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler";


98 99
	protected final Log logger = LogFactory.getLog(getClass());

100 101
	private Object scheduler;

102 103
	private StringValueResolver embeddedValueResolver;

104
	private BeanFactory beanFactory;
105

106 107
	private ApplicationContext applicationContext;

108
	private final ScheduledTaskRegistrar registrar = new ScheduledTaskRegistrar();
109

110 111 112
	private final Set<Class<?>> nonAnnotatedClasses =
			Collections.newSetFromMap(new ConcurrentHashMap<Class<?>, Boolean>(64));

113

114 115 116 117 118
	@Override
	public int getOrder() {
		return LOWEST_PRECEDENCE;
	}

119
	/**
120 121 122
	 * Set the {@link org.springframework.scheduling.TaskScheduler} that will invoke
	 * the scheduled methods, or a {@link java.util.concurrent.ScheduledExecutorService}
	 * to be wrapped as a TaskScheduler.
123 124 125 126 127
	 */
	public void setScheduler(Object scheduler) {
		this.scheduler = scheduler;
	}

128
	@Override
129 130 131 132
	public void setEmbeddedValueResolver(StringValueResolver resolver) {
		this.embeddedValueResolver = resolver;
	}

133 134 135 136 137
	/**
	 * Making a {@link BeanFactory} available is optional; if not set,
	 * {@link SchedulingConfigurer} beans won't get autodetected and
	 * a {@link #setScheduler scheduler} has to be explicitly configured.
	 */
138
	@Override
139
	public void setBeanFactory(BeanFactory beanFactory) {
140
		this.beanFactory = beanFactory;
141 142 143
	}

	/**
144 145 146
	 * Setting an {@link ApplicationContext} is optional: If set, registered
	 * tasks will be activated in the {@link ContextRefreshedEvent} phase;
	 * if not set, it will happen at {@link #afterSingletonsInstantiated} time.
147
	 */
148
	@Override
149
	public void setApplicationContext(ApplicationContext applicationContext) {
150
		this.applicationContext = applicationContext;
151 152 153
		if (this.beanFactory == null) {
			this.beanFactory = applicationContext;
		}
154 155
	}

156

157
	@Override
158
	public void afterSingletonsInstantiated() {
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
		if (this.applicationContext == null) {
			// Not running in an ApplicationContext -> register tasks early...
			finishRegistration();
		}
	}

	@Override
	public void onApplicationEvent(ContextRefreshedEvent event) {
		if (event.getApplicationContext() == this.applicationContext) {
			// Running in an ApplicationContext -> register tasks this late...
			// giving other ContextRefreshedEvent listeners a chance to perform
			// their work at the same time (e.g. Spring Batch's job registration).
			finishRegistration();
		}
	}

	private void finishRegistration() {
176 177 178 179
		if (this.scheduler != null) {
			this.registrar.setScheduler(this.scheduler);
		}

180 181 182
		if (this.beanFactory instanceof ListableBeanFactory) {
			Map<String, SchedulingConfigurer> configurers =
					((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
183 184 185 186 187 188 189
			for (SchedulingConfigurer configurer : configurers.values()) {
				configurer.configureTasks(this.registrar);
			}
		}

		if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
			Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type");
190 191
			try {
				// Search for TaskScheduler bean...
192
				this.registrar.setTaskScheduler(this.beanFactory.getBean(TaskScheduler.class));
193
			}
194
			catch (NoUniqueBeanDefinitionException ex) {
195 196 197 198 199 200 201 202 203 204
				try {
					this.registrar.setTaskScheduler(
							this.beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, TaskScheduler.class));
				}
				catch (NoSuchBeanDefinitionException ex2) {
					throw new IllegalStateException("More than one TaskScheduler bean exists within the context, and " +
							"none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' "+
							"(possibly as an alias); or implement the SchedulingConfigurer interface and call " +
							"ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback.", ex);
				}
205
			}
206 207 208 209 210 211 212
			catch (NoSuchBeanDefinitionException ex) {
				logger.debug("Could not find default TaskScheduler bean", ex);
				// Search for ScheduledExecutorService bean next...
				try {
					this.registrar.setScheduler(this.beanFactory.getBean(ScheduledExecutorService.class));
				}
				catch (NoUniqueBeanDefinitionException ex2) {
213 214 215 216
					throw new IllegalStateException("More than one ScheduledExecutorService bean exists within " +
							"the context. Mark one of them as primary; or implement the SchedulingConfigurer " +
							"interface and call ScheduledTaskRegistrar#setScheduler explicitly within the " +
							"configureTasks() callback.", ex);
217 218 219 220 221
				}
				catch (NoSuchBeanDefinitionException ex2) {
					logger.debug("Could not find default ScheduledExecutorService bean", ex);
					// Giving up -> falling back to default scheduler within the registrar...
				}
222 223 224 225
			}
		}

		this.registrar.afterPropertiesSet();
226 227
	}

228

229
	@Override
230
	public Object postProcessBeforeInitialization(Object bean, String beanName) {
231 232 233
		return bean;
	}

234
	@Override
235
	public Object postProcessAfterInitialization(final Object bean, String beanName) {
236 237
		Class<?> targetClass = AopUtils.getTargetClass(bean);
		if (!this.nonAnnotatedClasses.contains(targetClass)) {
238 239 240 241 242
			final Set<Method> annotatedMethods = new LinkedHashSet<Method>(1);
			ReflectionUtils.doWithMethods(targetClass, new MethodCallback() {
				@Override
				public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
					for (Scheduled scheduled :
S
Sam Brannen 已提交
243
							AnnotationUtils.getRepeatableAnnotations(method, Scheduled.class, Schedules.class)) {
244 245 246 247 248 249
						processScheduled(scheduled, method, bean);
						annotatedMethods.add(method);
					}
				}
			});
			if (annotatedMethods.isEmpty()) {
250
				this.nonAnnotatedClasses.add(targetClass);
251 252
				if (logger.isTraceEnabled()) {
					logger.trace("No @Scheduled annotations found on bean class: " + bean.getClass());
253 254 255 256 257 258 259
				}
			}
			else {
				// Non-empty set of methods
				if (logger.isDebugEnabled()) {
					logger.debug(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
							"': " + annotatedMethods);
260 261
				}
			}
262
		}
263 264 265
		return bean;
	}

266 267
	protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
		try {
268
			Assert.isTrue(void.class == method.getReturnType(),
269 270 271 272 273 274
					"Only void-returning methods may be annotated with @Scheduled");
			Assert.isTrue(method.getParameterTypes().length == 0,
					"Only no-arg methods may be annotated with @Scheduled");

			if (AopUtils.isJdkDynamicProxy(bean)) {
				try {
275 276
					// Found a @Scheduled method on the target class for this JDK proxy ->
					// is it also present on the proxy itself?
277 278 279 280 281 282 283
					method = bean.getClass().getMethod(method.getName(), method.getParameterTypes());
				}
				catch (SecurityException ex) {
					ReflectionUtils.handleReflectionException(ex);
				}
				catch (NoSuchMethodException ex) {
					throw new IllegalStateException(String.format(
284 285 286 287 288 289 290 291 292 293 294 295 296 297
							"@Scheduled method '%s' found on bean target class '%s' but not " +
							"found in any interface(s) for a dynamic proxy. Either pull the " +
							"method up to a declared interface or switch to subclass (CGLIB) " +
							"proxies by setting proxy-target-class/proxyTargetClass to 'true'",
							method.getName(), method.getDeclaringClass().getSimpleName()));
				}
			}
			else if (AopUtils.isCglibProxy(bean)) {
				// Common problem: private methods end up in the proxy instance, not getting delegated.
				if (Modifier.isPrivate(method.getModifiers())) {
					throw new IllegalStateException(String.format(
							"@Scheduled method '%s' found on CGLIB proxy for target class '%s' but cannot " +
							"be delegated to target bean. Switch its visibility to package or protected.",
							method.getName(), method.getDeclaringClass().getSimpleName()));
298 299 300 301 302
				}
			}

			Runnable runnable = new ScheduledMethodRunnable(bean, method);
			boolean processedSchedule = false;
303 304
			String errorMessage =
					"Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
305 306 307 308

			// Determine initial delay
			long initialDelay = scheduled.initialDelay();
			String initialDelayString = scheduled.initialDelayString();
309
			if (StringUtils.hasText(initialDelayString)) {
310 311 312 313 314
				Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
				if (this.embeddedValueResolver != null) {
					initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
				}
				try {
315
					initialDelay = Long.parseLong(initialDelayString);
316 317 318 319 320 321 322 323 324
				}
				catch (NumberFormatException ex) {
					throw new IllegalArgumentException(
							"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into integer");
				}
			}

			// Check cron expression
			String cron = scheduled.cron();
325
			if (StringUtils.hasText(cron)) {
326 327
				Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
				processedSchedule = true;
328
				String zone = scheduled.zone();
329 330
				if (this.embeddedValueResolver != null) {
					cron = this.embeddedValueResolver.resolveStringValue(cron);
331
					zone = this.embeddedValueResolver.resolveStringValue(zone);
332
				}
333
				TimeZone timeZone;
334
				if (StringUtils.hasText(zone)) {
335
					timeZone = StringUtils.parseTimeZoneString(zone);
336 337 338 339 340
				}
				else {
					timeZone = TimeZone.getDefault();
				}
				this.registrar.addCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone)));
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
			}

			// At this point we don't need to differentiate between initial delay set or not anymore
			if (initialDelay < 0) {
				initialDelay = 0;
			}

			// Check fixed delay
			long fixedDelay = scheduled.fixedDelay();
			if (fixedDelay >= 0) {
				Assert.isTrue(!processedSchedule, errorMessage);
				processedSchedule = true;
				this.registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay));
			}
			String fixedDelayString = scheduled.fixedDelayString();
356
			if (StringUtils.hasText(fixedDelayString)) {
357 358 359 360 361 362
				Assert.isTrue(!processedSchedule, errorMessage);
				processedSchedule = true;
				if (this.embeddedValueResolver != null) {
					fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
				}
				try {
363
					fixedDelay = Long.parseLong(fixedDelayString);
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
				}
				catch (NumberFormatException ex) {
					throw new IllegalArgumentException(
							"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into integer");
				}
				this.registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay));
			}

			// Check fixed rate
			long fixedRate = scheduled.fixedRate();
			if (fixedRate >= 0) {
				Assert.isTrue(!processedSchedule, errorMessage);
				processedSchedule = true;
				this.registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
			}
			String fixedRateString = scheduled.fixedRateString();
380
			if (StringUtils.hasText(fixedRateString)) {
381 382 383 384 385 386
				Assert.isTrue(!processedSchedule, errorMessage);
				processedSchedule = true;
				if (this.embeddedValueResolver != null) {
					fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
				}
				try {
387
					fixedRate = Long.parseLong(fixedRateString);
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
				}
				catch (NumberFormatException ex) {
					throw new IllegalArgumentException(
							"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into integer");
				}
				this.registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
			}

			// Check whether we had any attribute set
			Assert.isTrue(processedSchedule, errorMessage);
		}
		catch (IllegalArgumentException ex) {
			throw new IllegalStateException(
					"Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
		}
	}

405

406
	@Override
407 408
	public void destroy() {
		this.registrar.destroy();
409 410 411
	}

}