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}