001/* 002 * Copyright 2002-2018 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.annotation; 018 019import java.lang.reflect.Method; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.IdentityHashMap; 024import java.util.LinkedHashSet; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import java.util.TimeZone; 029import java.util.concurrent.ConcurrentHashMap; 030import java.util.concurrent.ScheduledExecutorService; 031 032import org.apache.commons.logging.Log; 033import org.apache.commons.logging.LogFactory; 034 035import org.springframework.aop.framework.AopInfrastructureBean; 036import org.springframework.aop.framework.AopProxyUtils; 037import org.springframework.aop.support.AopUtils; 038import org.springframework.beans.factory.BeanFactory; 039import org.springframework.beans.factory.BeanFactoryAware; 040import org.springframework.beans.factory.BeanNameAware; 041import org.springframework.beans.factory.DisposableBean; 042import org.springframework.beans.factory.ListableBeanFactory; 043import org.springframework.beans.factory.NoSuchBeanDefinitionException; 044import org.springframework.beans.factory.NoUniqueBeanDefinitionException; 045import org.springframework.beans.factory.SmartInitializingSingleton; 046import org.springframework.beans.factory.config.AutowireCapableBeanFactory; 047import org.springframework.beans.factory.config.ConfigurableBeanFactory; 048import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; 049import org.springframework.beans.factory.config.NamedBeanHolder; 050import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; 051import org.springframework.beans.factory.support.RootBeanDefinition; 052import org.springframework.context.ApplicationContext; 053import org.springframework.context.ApplicationContextAware; 054import org.springframework.context.ApplicationListener; 055import org.springframework.context.EmbeddedValueResolverAware; 056import org.springframework.context.event.ContextRefreshedEvent; 057import org.springframework.core.MethodIntrospector; 058import org.springframework.core.Ordered; 059import org.springframework.core.annotation.AnnotatedElementUtils; 060import org.springframework.core.annotation.AnnotationAwareOrderComparator; 061import org.springframework.scheduling.TaskScheduler; 062import org.springframework.scheduling.Trigger; 063import org.springframework.scheduling.config.CronTask; 064import org.springframework.scheduling.config.IntervalTask; 065import org.springframework.scheduling.config.ScheduledTask; 066import org.springframework.scheduling.config.ScheduledTaskRegistrar; 067import org.springframework.scheduling.support.CronTrigger; 068import org.springframework.scheduling.support.ScheduledMethodRunnable; 069import org.springframework.util.Assert; 070import org.springframework.util.StringUtils; 071import org.springframework.util.StringValueResolver; 072 073/** 074 * Bean post-processor that registers methods annotated with @{@link Scheduled} 075 * to be invoked by a {@link org.springframework.scheduling.TaskScheduler} according 076 * to the "fixedRate", "fixedDelay", or "cron" expression provided via the annotation. 077 * 078 * <p>This post-processor is automatically registered by Spring's 079 * {@code <task:annotation-driven>} XML element, and also by the 080 * {@link EnableScheduling @EnableScheduling} annotation. 081 * 082 * <p>Autodetects any {@link SchedulingConfigurer} instances in the container, 083 * allowing for customization of the scheduler to be used or for fine-grained 084 * control over task registration (e.g. registration of {@link Trigger} tasks. 085 * See the @{@link EnableScheduling} javadocs for complete usage details. 086 * 087 * @author Mark Fisher 088 * @author Juergen Hoeller 089 * @author Chris Beams 090 * @author Elizabeth Chatman 091 * @since 3.0 092 * @see Scheduled 093 * @see EnableScheduling 094 * @see SchedulingConfigurer 095 * @see org.springframework.scheduling.TaskScheduler 096 * @see org.springframework.scheduling.config.ScheduledTaskRegistrar 097 * @see AsyncAnnotationBeanPostProcessor 098 */ 099public class ScheduledAnnotationBeanPostProcessor 100 implements MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor, 101 Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware, 102 SmartInitializingSingleton, ApplicationListener<ContextRefreshedEvent>, DisposableBean { 103 104 /** 105 * The default name of the {@link TaskScheduler} bean to pick up: {@value}. 106 * <p>Note that the initial lookup happens by type; this is just the fallback 107 * in case of multiple scheduler beans found in the context. 108 * @since 4.2 109 */ 110 public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler"; 111 112 113 protected final Log logger = LogFactory.getLog(getClass()); 114 115 private Object scheduler; 116 117 private StringValueResolver embeddedValueResolver; 118 119 private String beanName; 120 121 private BeanFactory beanFactory; 122 123 private ApplicationContext applicationContext; 124 125 private final ScheduledTaskRegistrar registrar = new ScheduledTaskRegistrar(); 126 127 private final Set<Class<?>> nonAnnotatedClasses = 128 Collections.newSetFromMap(new ConcurrentHashMap<Class<?>, Boolean>(64)); 129 130 private final Map<Object, Set<ScheduledTask>> scheduledTasks = 131 new IdentityHashMap<Object, Set<ScheduledTask>>(16); 132 133 134 @Override 135 public int getOrder() { 136 return LOWEST_PRECEDENCE; 137 } 138 139 /** 140 * Set the {@link org.springframework.scheduling.TaskScheduler} that will invoke 141 * the scheduled methods, or a {@link java.util.concurrent.ScheduledExecutorService} 142 * to be wrapped as a TaskScheduler. 143 * <p>If not specified, default scheduler resolution will apply: searching for a 144 * unique {@link TaskScheduler} bean in the context, or for a {@link TaskScheduler} 145 * bean named "taskScheduler" otherwise; the same lookup will also be performed for 146 * a {@link ScheduledExecutorService} bean. If neither of the two is resolvable, 147 * a local single-threaded default scheduler will be created within the registrar. 148 * @see #DEFAULT_TASK_SCHEDULER_BEAN_NAME 149 */ 150 public void setScheduler(Object scheduler) { 151 this.scheduler = scheduler; 152 } 153 154 @Override 155 public void setEmbeddedValueResolver(StringValueResolver resolver) { 156 this.embeddedValueResolver = resolver; 157 } 158 159 @Override 160 public void setBeanName(String beanName) { 161 this.beanName = beanName; 162 } 163 164 /** 165 * Making a {@link BeanFactory} available is optional; if not set, 166 * {@link SchedulingConfigurer} beans won't get autodetected and 167 * a {@link #setScheduler scheduler} has to be explicitly configured. 168 */ 169 @Override 170 public void setBeanFactory(BeanFactory beanFactory) { 171 this.beanFactory = beanFactory; 172 } 173 174 /** 175 * Setting an {@link ApplicationContext} is optional: If set, registered 176 * tasks will be activated in the {@link ContextRefreshedEvent} phase; 177 * if not set, it will happen at {@link #afterSingletonsInstantiated} time. 178 */ 179 @Override 180 public void setApplicationContext(ApplicationContext applicationContext) { 181 this.applicationContext = applicationContext; 182 if (this.beanFactory == null) { 183 this.beanFactory = applicationContext; 184 } 185 } 186 187 188 @Override 189 public void afterSingletonsInstantiated() { 190 // Remove resolved singleton classes from cache 191 this.nonAnnotatedClasses.clear(); 192 193 if (this.applicationContext == null) { 194 // Not running in an ApplicationContext -> register tasks early... 195 finishRegistration(); 196 } 197 } 198 199 @Override 200 public void onApplicationEvent(ContextRefreshedEvent event) { 201 if (event.getApplicationContext() == this.applicationContext) { 202 // Running in an ApplicationContext -> register tasks this late... 203 // giving other ContextRefreshedEvent listeners a chance to perform 204 // their work at the same time (e.g. Spring Batch's job registration). 205 finishRegistration(); 206 } 207 } 208 209 private void finishRegistration() { 210 if (this.scheduler != null) { 211 this.registrar.setScheduler(this.scheduler); 212 } 213 214 if (this.beanFactory instanceof ListableBeanFactory) { 215 Map<String, SchedulingConfigurer> beans = 216 ((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class); 217 List<SchedulingConfigurer> configurers = new ArrayList<SchedulingConfigurer>(beans.values()); 218 AnnotationAwareOrderComparator.sort(configurers); 219 for (SchedulingConfigurer configurer : configurers) { 220 configurer.configureTasks(this.registrar); 221 } 222 } 223 224 if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) { 225 Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type"); 226 try { 227 // Search for TaskScheduler bean... 228 this.registrar.setTaskScheduler(resolveSchedulerBean(TaskScheduler.class, false)); 229 } 230 catch (NoUniqueBeanDefinitionException ex) { 231 logger.debug("Could not find unique TaskScheduler bean", ex); 232 try { 233 this.registrar.setTaskScheduler(resolveSchedulerBean(TaskScheduler.class, true)); 234 } 235 catch (NoSuchBeanDefinitionException ex2) { 236 if (logger.isInfoEnabled()) { 237 logger.info("More than one TaskScheduler bean exists within the context, and " + 238 "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " + 239 "(possibly as an alias); or implement the SchedulingConfigurer interface and call " + 240 "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " + 241 ex.getBeanNamesFound()); 242 } 243 } 244 } 245 catch (NoSuchBeanDefinitionException ex) { 246 logger.debug("Could not find default TaskScheduler bean", ex); 247 // Search for ScheduledExecutorService bean next... 248 try { 249 this.registrar.setScheduler(resolveSchedulerBean(ScheduledExecutorService.class, false)); 250 } 251 catch (NoUniqueBeanDefinitionException ex2) { 252 logger.debug("Could not find unique ScheduledExecutorService bean", ex2); 253 try { 254 this.registrar.setScheduler(resolveSchedulerBean(ScheduledExecutorService.class, true)); 255 } 256 catch (NoSuchBeanDefinitionException ex3) { 257 if (logger.isInfoEnabled()) { 258 logger.info("More than one ScheduledExecutorService bean exists within the context, and " + 259 "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " + 260 "(possibly as an alias); or implement the SchedulingConfigurer interface and call " + 261 "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " + 262 ex2.getBeanNamesFound()); 263 } 264 } 265 } 266 catch (NoSuchBeanDefinitionException ex2) { 267 logger.debug("Could not find default ScheduledExecutorService bean", ex2); 268 // Giving up -> falling back to default scheduler within the registrar... 269 logger.info("No TaskScheduler/ScheduledExecutorService bean found for scheduled processing"); 270 } 271 } 272 } 273 274 this.registrar.afterPropertiesSet(); 275 } 276 277 private <T> T resolveSchedulerBean(Class<T> schedulerType, boolean byName) { 278 if (byName) { 279 T scheduler = this.beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, schedulerType); 280 if (this.beanFactory instanceof ConfigurableBeanFactory) { 281 ((ConfigurableBeanFactory) this.beanFactory).registerDependentBean( 282 DEFAULT_TASK_SCHEDULER_BEAN_NAME, this.beanName); 283 } 284 return scheduler; 285 } 286 else if (this.beanFactory instanceof AutowireCapableBeanFactory) { 287 NamedBeanHolder<T> holder = ((AutowireCapableBeanFactory) this.beanFactory).resolveNamedBean(schedulerType); 288 if (this.beanFactory instanceof ConfigurableBeanFactory) { 289 ((ConfigurableBeanFactory) this.beanFactory).registerDependentBean( 290 holder.getBeanName(), this.beanName); 291 } 292 return holder.getBeanInstance(); 293 } 294 else { 295 return this.beanFactory.getBean(schedulerType); 296 } 297 } 298 299 300 @Override 301 public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) { 302 } 303 304 @Override 305 public Object postProcessBeforeInitialization(Object bean, String beanName) { 306 return bean; 307 } 308 309 @Override 310 public Object postProcessAfterInitialization(Object bean, String beanName) { 311 if (bean instanceof AopInfrastructureBean) { 312 // Ignore AOP infrastructure such as scoped proxies. 313 return bean; 314 } 315 316 Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean); 317 if (!this.nonAnnotatedClasses.contains(targetClass)) { 318 Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass, 319 new MethodIntrospector.MetadataLookup<Set<Scheduled>>() { 320 @Override 321 public Set<Scheduled> inspect(Method method) { 322 Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations( 323 method, Scheduled.class, Schedules.class); 324 return (!scheduledMethods.isEmpty() ? scheduledMethods : null); 325 } 326 }); 327 if (annotatedMethods.isEmpty()) { 328 this.nonAnnotatedClasses.add(targetClass); 329 if (logger.isTraceEnabled()) { 330 logger.trace("No @Scheduled annotations found on bean class: " + targetClass); 331 } 332 } 333 else { 334 // Non-empty set of methods 335 for (Map.Entry<Method, Set<Scheduled>> entry : annotatedMethods.entrySet()) { 336 Method method = entry.getKey(); 337 for (Scheduled scheduled : entry.getValue()) { 338 processScheduled(scheduled, method, bean); 339 } 340 } 341 if (logger.isDebugEnabled()) { 342 logger.debug(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName + 343 "': " + annotatedMethods); 344 } 345 } 346 } 347 return bean; 348 } 349 350 /** 351 * Process the given {@code @Scheduled} method declaration on the given bean. 352 * @param scheduled the @Scheduled annotation 353 * @param method the method that the annotation has been declared on 354 * @param bean the target bean instance 355 */ 356 protected void processScheduled(Scheduled scheduled, Method method, Object bean) { 357 try { 358 Assert.isTrue(method.getParameterTypes().length == 0, 359 "Only no-arg methods may be annotated with @Scheduled"); 360 361 Method invocableMethod = AopUtils.selectInvocableMethod(method, bean.getClass()); 362 Runnable runnable = new ScheduledMethodRunnable(bean, invocableMethod); 363 boolean processedSchedule = false; 364 String errorMessage = 365 "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required"; 366 367 Set<ScheduledTask> tasks = new LinkedHashSet<ScheduledTask>(4); 368 369 // Determine initial delay 370 long initialDelay = scheduled.initialDelay(); 371 String initialDelayString = scheduled.initialDelayString(); 372 if (StringUtils.hasText(initialDelayString)) { 373 Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both"); 374 if (this.embeddedValueResolver != null) { 375 initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString); 376 } 377 try { 378 initialDelay = Long.parseLong(initialDelayString); 379 } 380 catch (NumberFormatException ex) { 381 throw new IllegalArgumentException( 382 "Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into integer"); 383 } 384 } 385 386 // Check cron expression 387 String cron = scheduled.cron(); 388 if (StringUtils.hasText(cron)) { 389 Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers"); 390 processedSchedule = true; 391 String zone = scheduled.zone(); 392 if (this.embeddedValueResolver != null) { 393 cron = this.embeddedValueResolver.resolveStringValue(cron); 394 zone = this.embeddedValueResolver.resolveStringValue(zone); 395 } 396 TimeZone timeZone; 397 if (StringUtils.hasText(zone)) { 398 timeZone = StringUtils.parseTimeZoneString(zone); 399 } 400 else { 401 timeZone = TimeZone.getDefault(); 402 } 403 tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone)))); 404 } 405 406 // At this point we don't need to differentiate between initial delay set or not anymore 407 if (initialDelay < 0) { 408 initialDelay = 0; 409 } 410 411 // Check fixed delay 412 long fixedDelay = scheduled.fixedDelay(); 413 if (fixedDelay >= 0) { 414 Assert.isTrue(!processedSchedule, errorMessage); 415 processedSchedule = true; 416 tasks.add(this.registrar.scheduleFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay))); 417 } 418 String fixedDelayString = scheduled.fixedDelayString(); 419 if (StringUtils.hasText(fixedDelayString)) { 420 Assert.isTrue(!processedSchedule, errorMessage); 421 processedSchedule = true; 422 if (this.embeddedValueResolver != null) { 423 fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString); 424 } 425 try { 426 fixedDelay = Long.parseLong(fixedDelayString); 427 } 428 catch (NumberFormatException ex) { 429 throw new IllegalArgumentException( 430 "Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into integer"); 431 } 432 tasks.add(this.registrar.scheduleFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay))); 433 } 434 435 // Check fixed rate 436 long fixedRate = scheduled.fixedRate(); 437 if (fixedRate >= 0) { 438 Assert.isTrue(!processedSchedule, errorMessage); 439 processedSchedule = true; 440 tasks.add(this.registrar.scheduleFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay))); 441 } 442 String fixedRateString = scheduled.fixedRateString(); 443 if (StringUtils.hasText(fixedRateString)) { 444 Assert.isTrue(!processedSchedule, errorMessage); 445 processedSchedule = true; 446 if (this.embeddedValueResolver != null) { 447 fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString); 448 } 449 try { 450 fixedRate = Long.parseLong(fixedRateString); 451 } 452 catch (NumberFormatException ex) { 453 throw new IllegalArgumentException( 454 "Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into integer"); 455 } 456 tasks.add(this.registrar.scheduleFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay))); 457 } 458 459 // Check whether we had any attribute set 460 Assert.isTrue(processedSchedule, errorMessage); 461 462 // Finally register the scheduled tasks 463 synchronized (this.scheduledTasks) { 464 Set<ScheduledTask> registeredTasks = this.scheduledTasks.get(bean); 465 if (registeredTasks == null) { 466 registeredTasks = new LinkedHashSet<ScheduledTask>(4); 467 this.scheduledTasks.put(bean, registeredTasks); 468 } 469 registeredTasks.addAll(tasks); 470 } 471 } 472 catch (IllegalArgumentException ex) { 473 throw new IllegalStateException( 474 "Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage()); 475 } 476 } 477 478 479 @Override 480 public void postProcessBeforeDestruction(Object bean, String beanName) { 481 Set<ScheduledTask> tasks; 482 synchronized (this.scheduledTasks) { 483 tasks = this.scheduledTasks.remove(bean); 484 } 485 if (tasks != null) { 486 for (ScheduledTask task : tasks) { 487 task.cancel(); 488 } 489 } 490 } 491 492 @Override 493 public boolean requiresDestruction(Object bean) { 494 synchronized (this.scheduledTasks) { 495 return this.scheduledTasks.containsKey(bean); 496 } 497 } 498 499 @Override 500 public void destroy() { 501 synchronized (this.scheduledTasks) { 502 Collection<Set<ScheduledTask>> allTasks = this.scheduledTasks.values(); 503 for (Set<ScheduledTask> tasks : allTasks) { 504 for (ScheduledTask task : tasks) { 505 task.cancel(); 506 } 507 } 508 this.scheduledTasks.clear(); 509 } 510 this.registrar.destroy(); 511 } 512 513}