• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 18154768944

01 Oct 2025 07:26AM UTC coverage: 81.882% (-0.005%) from 81.887%
18154768944

push

github

web-flow
Merge pull request #290 from TAKETODAY/dev/jspecify

jspecify

59788 of 78013 branches covered (76.64%)

Branch coverage included in aggregate %.

141239 of 167496 relevant lines covered (84.32%)

3.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.34
today-context/src/main/java/infra/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.scheduling.annotation;
19

20
import org.jspecify.annotations.Nullable;
21

22
import java.lang.reflect.Method;
23
import java.time.Duration;
24
import java.util.ArrayList;
25
import java.util.Collection;
26
import java.util.Collections;
27
import java.util.IdentityHashMap;
28
import java.util.LinkedHashSet;
29
import java.util.List;
30
import java.util.Map;
31
import java.util.Set;
32
import java.util.TimeZone;
33
import java.util.concurrent.ConcurrentHashMap;
34
import java.util.concurrent.CopyOnWriteArrayList;
35
import java.util.concurrent.ScheduledExecutorService;
36
import java.util.concurrent.TimeUnit;
37

38
import infra.aop.AopInfrastructureBean;
39
import infra.aop.framework.AopProxyUtils;
40
import infra.aop.support.AopUtils;
41
import infra.beans.factory.BeanFactory;
42
import infra.beans.factory.BeanFactoryAware;
43
import infra.beans.factory.BeanNameAware;
44
import infra.beans.factory.DisposableBean;
45
import infra.beans.factory.InitializationBeanPostProcessor;
46
import infra.beans.factory.SmartInitializingSingleton;
47
import infra.beans.factory.config.DestructionAwareBeanPostProcessor;
48
import infra.beans.factory.config.SingletonBeanRegistry;
49
import infra.beans.factory.support.MergedBeanDefinitionPostProcessor;
50
import infra.beans.factory.support.RootBeanDefinition;
51
import infra.context.ApplicationContext;
52
import infra.context.ApplicationContextAware;
53
import infra.context.ApplicationListener;
54
import infra.context.event.ApplicationContextEvent;
55
import infra.context.event.ContextClosedEvent;
56
import infra.context.event.ContextRefreshedEvent;
57
import infra.context.expression.EmbeddedValueResolverAware;
58
import infra.core.MethodIntrospector;
59
import infra.core.Ordered;
60
import infra.core.ReactiveStreams;
61
import infra.core.StringValueResolver;
62
import infra.core.annotation.AnnotatedElementUtils;
63
import infra.core.annotation.AnnotationAwareOrderComparator;
64
import infra.core.annotation.AnnotationUtils;
65
import infra.format.annotation.DurationFormat;
66
import infra.format.datetime.standard.DurationFormatterUtils;
67
import infra.lang.Assert;
68
import infra.logging.Logger;
69
import infra.logging.LoggerFactory;
70
import infra.scheduling.TaskScheduler;
71
import infra.scheduling.Trigger;
72
import infra.scheduling.config.CronTask;
73
import infra.scheduling.config.FixedDelayTask;
74
import infra.scheduling.config.FixedRateTask;
75
import infra.scheduling.config.OneTimeTask;
76
import infra.scheduling.config.ScheduledTask;
77
import infra.scheduling.config.ScheduledTaskHolder;
78
import infra.scheduling.config.ScheduledTaskRegistrar;
79
import infra.scheduling.config.TaskSchedulerRouter;
80
import infra.scheduling.support.CronTrigger;
81
import infra.scheduling.support.ScheduledMethodRunnable;
82
import infra.util.StringUtils;
83

84
/**
85
 * Bean post-processor that registers methods annotated with
86
 * {@link Scheduled @Scheduled} to be invoked by a
87
 * {@link TaskScheduler} according to the
88
 * "fixedRate", "fixedDelay", or "cron" expression provided via the annotation.
89
 *
90
 * <p>This post-processor is automatically registered by
91
 * {@code <task:annotation-driven>} XML element, and also by the
92
 * {@link EnableScheduling @EnableScheduling} annotation.
93
 *
94
 * <p>Autodetects any {@link SchedulingConfigurer} instances in the container,
95
 * allowing for customization of the scheduler to be used or for fine-grained
96
 * control over task registration (e.g. registration of {@link Trigger} tasks).
97
 * See the {@link EnableScheduling @EnableScheduling} javadocs for complete usage
98
 * details.
99
 *
100
 * @author Mark Fisher
101
 * @author Juergen Hoeller
102
 * @author Chris Beams
103
 * @author Elizabeth Chatman
104
 * @author Victor Brown
105
 * @author Sam Brannen
106
 * @author Simon Baslé
107
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
108
 * @see Scheduled
109
 * @see EnableScheduling
110
 * @see SchedulingConfigurer
111
 * @see TaskScheduler
112
 * @see ScheduledTaskRegistrar
113
 * @see AsyncAnnotationBeanPostProcessor
114
 * @since 4.0
115
 */
116
public class ScheduledAnnotationBeanPostProcessor implements ScheduledTaskHolder, Ordered,
117
        DestructionAwareBeanPostProcessor, InitializationBeanPostProcessor, BeanNameAware,
118
        DisposableBean, BeanFactoryAware, ApplicationContextAware, MergedBeanDefinitionPostProcessor,
119
        EmbeddedValueResolverAware, SmartInitializingSingleton, ApplicationListener<ApplicationContextEvent> {
120

121
  private static final Logger log = LoggerFactory.getLogger(ScheduledAnnotationBeanPostProcessor.class);
4✔
122

123
  private final ScheduledTaskRegistrar registrar;
124

125
  @Nullable
126
  private Object scheduler;
127

128
  @Nullable
129
  private StringValueResolver embeddedValueResolver;
130

131
  @Nullable
132
  private String beanName;
133

134
  @Nullable
135
  private BeanFactory beanFactory;
136

137
  @Nullable
138
  private ApplicationContext applicationContext;
139

140
  @Nullable
141
  private TaskSchedulerRouter localScheduler;
142

143
  private final Set<Class<?>> nonAnnotatedClasses = ConcurrentHashMap.newKeySet(64);
4✔
144

145
  private final IdentityHashMap<Object, Set<ScheduledTask>> scheduledTasks = new IdentityHashMap<>(16);
6✔
146

147
  private final IdentityHashMap<Object, List<Runnable>> reactiveSubscriptions = new IdentityHashMap<>(16);
6✔
148

149
  private final Set<Object> manualCancellationOnContextClose = Collections.newSetFromMap(new IdentityHashMap<>(16));
7✔
150

151
  /**
152
   * Create a default {@code ScheduledAnnotationBeanPostProcessor}.
153
   */
154
  public ScheduledAnnotationBeanPostProcessor() {
2✔
155
    this.registrar = new ScheduledTaskRegistrar();
5✔
156
  }
1✔
157

158
  /**
159
   * Create a {@code ScheduledAnnotationBeanPostProcessor} delegating to the
160
   * specified {@link ScheduledTaskRegistrar}.
161
   *
162
   * @param registrar the ScheduledTaskRegistrar to register {@code @Scheduled}
163
   * tasks on
164
   */
165
  public ScheduledAnnotationBeanPostProcessor(ScheduledTaskRegistrar registrar) {
×
166
    Assert.notNull(registrar, "ScheduledTaskRegistrar is required");
×
167
    this.registrar = registrar;
×
168
  }
×
169

170
  @Override
171
  public int getOrder() {
172
    return LOWEST_PRECEDENCE;
2✔
173
  }
174

175
  /**
176
   * Set the {@link TaskScheduler} that will invoke
177
   * the scheduled methods, or a {@link java.util.concurrent.ScheduledExecutorService}
178
   * to be wrapped as a TaskScheduler.
179
   * <p>If not specified, default scheduler resolution will apply: searching for a
180
   * unique {@link TaskScheduler} bean in the context, or for a {@link TaskScheduler}
181
   * bean named "taskScheduler" otherwise; the same lookup will also be performed for
182
   * a {@link ScheduledExecutorService} bean. If neither of the two is resolvable,
183
   * a local single-threaded default scheduler will be created within the registrar.
184
   *
185
   * @see TaskSchedulerRouter#DEFAULT_TASK_SCHEDULER_BEAN_NAME
186
   */
187
  public void setScheduler(Object scheduler) {
188
    this.scheduler = scheduler;
3✔
189
  }
1✔
190

191
  @Override
192
  public void setEmbeddedValueResolver(StringValueResolver resolver) {
193
    this.embeddedValueResolver = resolver;
3✔
194
  }
1✔
195

196
  @Override
197
  public void setBeanName(String beanName) {
198
    this.beanName = beanName;
3✔
199
  }
1✔
200

201
  /**
202
   * Making a {@link BeanFactory} available is optional; if not set,
203
   * {@link SchedulingConfigurer} beans won't get autodetected and
204
   * a {@link #setScheduler scheduler} has to be explicitly configured.
205
   */
206
  @Override
207
  public void setBeanFactory(BeanFactory beanFactory) {
208
    this.beanFactory = beanFactory;
3✔
209
  }
1✔
210

211
  /**
212
   * Setting an {@link ApplicationContext} is optional: If set, registered
213
   * tasks will be activated in the {@link ContextRefreshedEvent} phase;
214
   * if not set, it will happen at {@link #afterSingletonsInstantiated} time.
215
   */
216
  @Override
217
  public void setApplicationContext(ApplicationContext applicationContext) {
218
    this.applicationContext = applicationContext;
3✔
219
    if (this.beanFactory == null) {
3!
220
      this.beanFactory = applicationContext;
×
221
    }
222
  }
1✔
223

224
  @Override
225
  public void afterSingletonsInstantiated() {
226
    // Remove resolved singleton classes from cache
227
    this.nonAnnotatedClasses.clear();
3✔
228

229
    if (this.applicationContext == null) {
3!
230
      // Not running in an ApplicationContext -> register tasks early...
231
      finishRegistration();
×
232
    }
233
  }
1✔
234

235
  private void finishRegistration() {
236
    if (this.scheduler != null) {
3✔
237
      this.registrar.setScheduler(this.scheduler);
6✔
238
    }
239
    else {
240
      this.localScheduler = new TaskSchedulerRouter();
5✔
241
      this.localScheduler.setBeanName(this.beanName);
5✔
242
      this.localScheduler.setBeanFactory(this.beanFactory);
5✔
243
      this.registrar.setTaskScheduler(this.localScheduler);
5✔
244
    }
245

246
    Assert.state(beanFactory != null, "No beanFactory");
7!
247

248
    Map<String, SchedulingConfigurer> beans = beanFactory.getBeansOfType(SchedulingConfigurer.class);
5✔
249
    List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());
6✔
250
    AnnotationAwareOrderComparator.sort(configurers);
2✔
251
    for (SchedulingConfigurer configurer : configurers) {
6!
252
      configurer.configureTasks(this.registrar);
×
253
    }
×
254

255
    this.registrar.afterPropertiesSet();
3✔
256
  }
1✔
257

258
  @Override
259
  public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
260

261
  }
1✔
262

263
  @Override
264
  public Object postProcessBeforeInitialization(Object bean, String beanName) {
265
    return bean;
2✔
266
  }
267

268
  @Override
269
  public Object postProcessAfterInitialization(Object bean, String beanName) {
270
    if (bean instanceof AopInfrastructureBean
9✔
271
            || bean instanceof TaskScheduler
272
            || bean instanceof ScheduledExecutorService) {
273
      // Ignore AOP infrastructure such as scoped proxies.
274
      return bean;
2✔
275
    }
276

277
    Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
3✔
278
    if (!this.nonAnnotatedClasses.contains(targetClass)
8!
279
            && AnnotationUtils.isCandidateClass(targetClass, Scheduled.class, Schedules.class)) {
2!
280
      Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(
4✔
281
              targetClass, method -> {
282
                Set<Scheduled> scheduledAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
5✔
283
                        method, Scheduled.class, Schedules.class);
284
                return (!scheduledAnnotations.isEmpty() ? scheduledAnnotations : null);
7✔
285
              });
286
      if (annotatedMethods.isEmpty()) {
3✔
287
        this.nonAnnotatedClasses.add(targetClass);
5✔
288
        if (log.isTraceEnabled()) {
3!
289
          log.trace("No @Scheduled annotations found on bean class: {}", targetClass);
×
290
        }
291
      }
292
      else {
293
        // Non-empty set of methods
294
        for (Map.Entry<Method, Set<Scheduled>> entry : annotatedMethods.entrySet()) {
11✔
295
          Method method = entry.getKey();
4✔
296
          for (Scheduled scheduled : entry.getValue()) {
12✔
297
            processScheduled(scheduled, method, bean);
5✔
298
          }
1✔
299
        }
1✔
300
        if (log.isTraceEnabled()) {
3!
301
          log.trace("{} @Scheduled methods processed on bean '{}': {}",
×
302
                  annotatedMethods.size(), beanName, annotatedMethods);
×
303
        }
304

305
        if ((this.beanFactory != null
6!
306
                && (!this.beanFactory.containsBean(beanName) || !this.beanFactory.isSingleton(beanName))
7!
307
                || (this.beanFactory instanceof SingletonBeanRegistry sbr && sbr.containsSingleton(beanName)))) {
13!
308
          // Either a prototype/scoped bean or a FactoryBean with a pre-existing managed singleton
309
          // -> trigger manual cancellation when ContextClosedEvent comes in
310
          this.manualCancellationOnContextClose.add(bean);
5✔
311
        }
312
      }
313
    }
314
    return bean;
2✔
315
  }
316

317
  /**
318
   * Process the given {@code @Scheduled} method declaration on the given bean,
319
   * attempting to distinguish {@linkplain #processScheduledAsync(Scheduled, Method, Object)
320
   * reactive} methods from {@linkplain #processScheduledSync(Scheduled, Method, Object)
321
   * synchronous} methods.
322
   *
323
   * @param scheduled the {@code @Scheduled} annotation
324
   * @param method the method that the annotation has been declared on
325
   * @param bean the target bean instance
326
   * @see #processScheduledSync(Scheduled, Method, Object)
327
   * @see #processScheduledAsync(Scheduled, Method, Object)
328
   */
329
  protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
330
    // Is the method a Kotlin suspending function? Throws if true and the reactor bridge isn't on the classpath.
331
    // Does the method return a reactive type? Throws if true and it isn't a deferred Publisher type.
332
    if (ReactiveStreams.isPresent && ScheduledAnnotationReactiveSupport.isReactive(method)) {
5!
333
      processScheduledAsync(scheduled, method, bean);
×
334
      return;
×
335
    }
336
    processScheduledSync(scheduled, method, bean);
5✔
337
  }
1✔
338

339
  /**
340
   * Process the given {@code @Scheduled} method declaration on the given bean,
341
   * as a synchronous method. The method must accept no arguments. Its return value
342
   * is ignored (if any), and the scheduled invocations of the method take place
343
   * using the underlying {@link TaskScheduler} infrastructure.
344
   *
345
   * @param scheduled the {@code @Scheduled} annotation
346
   * @param method the method that the annotation has been declared on
347
   * @param bean the target bean instance
348
   */
349
  private void processScheduledSync(Scheduled scheduled, Method method, Object bean) {
350
    Runnable task;
351
    try {
352
      task = createRunnable(bean, method, scheduled.scheduler());
7✔
353
    }
354
    catch (IllegalArgumentException ex) {
1✔
355
      throw new IllegalStateException("Could not create recurring task for @Scheduled method '%s': %s"
8✔
356
              .formatted(method.getName(), ex.getMessage()), ex);
11✔
357
    }
1✔
358
    processScheduledTask(scheduled, task, method, bean);
6✔
359
  }
1✔
360

361
  /**
362
   * Process the given {@code @Scheduled} bean method declaration which returns
363
   * a {@code Publisher}, or the given Kotlin suspending function converted to a
364
   * {@code Publisher}. A {@code Runnable} which subscribes to that publisher is
365
   * then repeatedly scheduled according to the annotation configuration.
366
   * <p>Note that for fixed delay configuration, the subscription is turned into a blocking
367
   * call instead. Types for which a {@code ReactiveAdapter} is registered but which cannot
368
   * be deferred (i.e. not a {@code Publisher}) are not supported.
369
   *
370
   * @param scheduled the {@code @Scheduled} annotation
371
   * @param method the method that the annotation has been declared on, which
372
   * must either return a Publisher-adaptable type or be a Kotlin suspending function
373
   * @param bean the target bean instance
374
   * @see ScheduledAnnotationReactiveSupport
375
   */
376
  private void processScheduledAsync(Scheduled scheduled, Method method, Object bean) {
377
    Runnable task;
378
    try {
379
      task = ScheduledAnnotationReactiveSupport.createSubscriptionRunnable(method, bean, scheduled,
×
380
              this.reactiveSubscriptions.computeIfAbsent(bean, k -> new CopyOnWriteArrayList<>()));
×
381
    }
382
    catch (IllegalArgumentException ex) {
×
383
      throw new IllegalStateException(
×
384
              "Could not create recurring task for @Scheduled method '%s': %s".formatted(method.getName(), ex.getMessage()), ex);
×
385
    }
×
386
    processScheduledTask(scheduled, task, method, bean);
×
387
  }
×
388

389
  /**
390
   * Parse the {@code Scheduled} annotation and schedule the provided {@code Runnable}
391
   * accordingly. The Runnable can represent either a synchronous method invocation
392
   * (see {@link #processScheduledSync(Scheduled, Method, Object)}) or an asynchronous
393
   * one (see {@link #processScheduledAsync(Scheduled, Method, Object)}).
394
   *
395
   * @param scheduled the {@code @Scheduled} annotation
396
   * @param runnable the runnable to be scheduled
397
   * @param method the method that the annotation has been declared on
398
   * @param bean the target bean instance
399
   */
400
  private void processScheduledTask(Scheduled scheduled, Runnable runnable, Method method, Object bean) {
401
    try {
402
      boolean processedSchedule = false;
2✔
403
      String errorMessage = "Exactly one of the 'cron', 'fixedDelay' or 'fixedRate' attributes is required";
2✔
404

405
      LinkedHashSet<ScheduledTask> tasks = new LinkedHashSet<>(4);
5✔
406

407
      // Determine initial delay
408
      Duration initialDelay = toDuration(scheduled.initialDelay(), scheduled.timeUnit());
6✔
409
      String initialDelayString = scheduled.initialDelayString();
3✔
410
      if (StringUtils.hasText(initialDelayString)) {
3✔
411
        Assert.isTrue(initialDelay.isNegative(), "Specify 'initialDelay' or 'initialDelayString', not both");
4✔
412
        if (this.embeddedValueResolver != null) {
3!
413
          initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
5✔
414
        }
415
        if (StringUtils.isNotEmpty(initialDelayString)) {
3!
416
          try {
417
            initialDelay = toDuration(initialDelayString, scheduled.timeUnit());
5✔
418
          }
419
          catch (RuntimeException ex) {
×
420
            throw new IllegalArgumentException(
×
421
                    "Invalid initialDelayString value \"%s\" - cannot parse into long".formatted(initialDelayString), ex);
×
422
          }
1✔
423
        }
424
      }
425

426
      // Check cron expression
427
      String cron = scheduled.cron();
3✔
428
      if (StringUtils.hasText(cron)) {
3✔
429
        String zone = scheduled.zone();
3✔
430
        if (this.embeddedValueResolver != null) {
3!
431
          cron = this.embeddedValueResolver.resolveStringValue(cron);
5✔
432
          zone = this.embeddedValueResolver.resolveStringValue(zone);
5✔
433
        }
434
        if (StringUtils.isNotEmpty(cron)) {
3!
435
          Assert.isTrue(initialDelay.isNegative(), "'initialDelay' not supported for cron triggers");
4✔
436
          processedSchedule = true;
2✔
437
          if (!Scheduled.CRON_DISABLED.equals(cron)) {
4✔
438
            TimeZone timeZone;
439
            if (StringUtils.hasText(zone)) {
3✔
440
              timeZone = StringUtils.parseTimeZoneString(zone);
4✔
441
            }
442
            else {
443
              timeZone = TimeZone.getDefault();
2✔
444
            }
445
            tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
15✔
446
          }
447
        }
448
      }
449

450
      // At this point we don't need to differentiate between initial delay set or not anymore
451
      Duration delayToUse = initialDelay.isNegative() ? Duration.ZERO : initialDelay;
7✔
452

453
      // Check fixed delay
454
      Duration fixedDelay = toDuration(scheduled.fixedDelay(), scheduled.timeUnit());
6✔
455
      if (!fixedDelay.isNegative()) {
3✔
456
        Assert.isTrue(!processedSchedule, errorMessage);
6!
457
        processedSchedule = true;
2✔
458
        tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, delayToUse)));
12✔
459
      }
460

461
      String fixedDelayString = scheduled.fixedDelayString();
3✔
462
      if (StringUtils.hasText(fixedDelayString)) {
3✔
463
        if (this.embeddedValueResolver != null) {
3!
464
          fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
5✔
465
        }
466
        if (StringUtils.isNotEmpty(fixedDelayString)) {
3!
467
          Assert.isTrue(!processedSchedule, errorMessage);
6!
468
          processedSchedule = true;
2✔
469
          try {
470
            fixedDelay = toDuration(fixedDelayString, scheduled.timeUnit());
5✔
471
          }
472
          catch (RuntimeException ex) {
×
473
            throw new IllegalArgumentException(
×
474
                    "Invalid fixedDelayString value \"%s\" - cannot parse into long".formatted(fixedDelayString), ex);
×
475
          }
1✔
476
          tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, delayToUse)));
12✔
477
        }
478
      }
479

480
      // Check fixed rate
481
      Duration fixedRate = toDuration(scheduled.fixedRate(), scheduled.timeUnit());
6✔
482
      if (!fixedRate.isNegative()) {
3✔
483
        Assert.isTrue(!processedSchedule, errorMessage);
6!
484
        processedSchedule = true;
2✔
485
        tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, delayToUse)));
12✔
486
      }
487
      String fixedRateString = scheduled.fixedRateString();
3✔
488
      if (StringUtils.hasText(fixedRateString)) {
3✔
489
        if (this.embeddedValueResolver != null) {
3!
490
          fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
5✔
491
        }
492
        if (StringUtils.isNotEmpty(fixedRateString)) {
3!
493
          Assert.isTrue(!processedSchedule, errorMessage);
6!
494
          processedSchedule = true;
2✔
495
          try {
496
            fixedRate = toDuration(fixedRateString, scheduled.timeUnit());
5✔
497
          }
498
          catch (RuntimeException ex) {
×
499
            throw new IllegalArgumentException(
×
500
                    "Invalid fixedRateString value \"%s\" - cannot parse into long".formatted(fixedRateString), ex);
×
501
          }
1✔
502
          tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, delayToUse)));
12✔
503
        }
504
      }
505

506
      if (!processedSchedule) {
2✔
507
        if (initialDelay.isNegative()) {
3✔
508
          throw new IllegalArgumentException("One-time task only supported with specified initial delay");
5✔
509
        }
510
        tasks.add(this.registrar.scheduleOneTimeTask(new OneTimeTask(runnable, delayToUse)));
11✔
511
      }
512

513
      // Finally register the scheduled tasks
514
      synchronized(this.scheduledTasks) {
5✔
515
        Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
12✔
516
        regTasks.addAll(tasks);
4✔
517
      }
3✔
518
    }
519
    catch (IllegalArgumentException ex) {
1✔
520
      throw new IllegalStateException(
8✔
521
              "Encountered invalid @Scheduled method '%s': %s".formatted(method.getName(), ex.getMessage()));
10✔
522
    }
1✔
523
  }
1✔
524

525
  /**
526
   * Create a {@link Runnable} for the given bean instance,
527
   * calling the specified scheduled method.
528
   * <p>The default implementation creates a {@link ScheduledMethodRunnable}.
529
   *
530
   * @param target the target bean instance
531
   * @param method the scheduled method to call
532
   */
533
  protected Runnable createRunnable(Object target, Method method, @Nullable String qualifier) {
534
    Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled");
8✔
535
    Method invocableMethod = AopUtils.selectInvocableMethod(method, target.getClass());
5✔
536
    return new ScheduledMethodRunnable(target, invocableMethod, qualifier);
7✔
537
  }
538

539
  private static Duration toDuration(long value, TimeUnit timeUnit) {
540
    try {
541
      return Duration.of(value, timeUnit.toChronoUnit());
5✔
542
    }
543
    catch (Exception ex) {
×
544
      throw new IllegalArgumentException(
×
545
              "Unsupported unit %s for value \"%d\": %s".formatted(timeUnit, value, ex.getMessage()), ex);
×
546
    }
547
  }
548

549
  private static Duration toDuration(String value, TimeUnit timeUnit) {
550
    DurationFormat.Unit unit = DurationFormat.Unit.fromChronoUnit(timeUnit.toChronoUnit());
4✔
551
    return DurationFormatterUtils.detectAndParse(value, unit); // interpreting as long as fallback already
4✔
552
  }
553

554
  /**
555
   * Return all currently scheduled tasks, from {@link Scheduled} methods
556
   * as well as from programmatic {@link SchedulingConfigurer} interaction.
557
   * <p>Note that this includes upcoming scheduled subscriptions for reactive
558
   * methods but doesn't cover any currently active subscription for such methods.
559
   */
560
  @Override
561
  public Set<ScheduledTask> getScheduledTasks() {
562
    Set<ScheduledTask> result = new LinkedHashSet<>();
4✔
563
    synchronized(this.scheduledTasks) {
5✔
564
      Collection<Set<ScheduledTask>> allTasks = this.scheduledTasks.values();
4✔
565
      for (Set<ScheduledTask> tasks : allTasks) {
10✔
566
        result.addAll(tasks);
4✔
567
      }
1✔
568
    }
3✔
569
    result.addAll(this.registrar.getScheduledTasks());
6✔
570
    return result;
2✔
571
  }
572

573
  @Override
574
  public void postProcessBeforeDestruction(Object bean, String beanName) {
575
    cancelScheduledTasks(bean);
3✔
576
    this.manualCancellationOnContextClose.remove(bean);
5✔
577
  }
1✔
578

579
  @Override
580
  public boolean requiresDestruction(Object bean) {
581
    synchronized(this.scheduledTasks) {
5✔
582
      return (this.scheduledTasks.containsKey(bean) || this.reactiveSubscriptions.containsKey(bean));
16!
583
    }
584
  }
585

586
  private void cancelScheduledTasks(Object bean) {
587
    Set<ScheduledTask> tasks;
588
    List<Runnable> liveSubscriptions;
589
    synchronized(this.scheduledTasks) {
5✔
590
      tasks = this.scheduledTasks.remove(bean);
6✔
591
      liveSubscriptions = this.reactiveSubscriptions.remove(bean);
6✔
592
    }
3✔
593
    if (tasks != null) {
2✔
594
      for (ScheduledTask task : tasks) {
10✔
595
        task.cancel(false);
3✔
596
      }
1✔
597
    }
598
    if (liveSubscriptions != null) {
2!
599
      for (Runnable subscription : liveSubscriptions) {
×
600
        subscription.run();  // equivalent to cancelling the subscription
×
601
      }
×
602
    }
603
  }
1✔
604

605
  @Override
606
  public void destroy() {
607
    synchronized(this.scheduledTasks) {
5✔
608
      Collection<Set<ScheduledTask>> allTasks = this.scheduledTasks.values();
4✔
609
      for (Set<ScheduledTask> tasks : allTasks) {
10✔
610
        for (ScheduledTask task : tasks) {
10✔
611
          task.cancel(false);
3✔
612
        }
1✔
613
      }
1✔
614
      this.scheduledTasks.clear();
3✔
615
      Collection<List<Runnable>> allLiveSubscriptions = this.reactiveSubscriptions.values();
4✔
616
      for (List<Runnable> liveSubscriptions : allLiveSubscriptions) {
6!
617
        for (Runnable liveSubscription : liveSubscriptions) {
×
618
          liveSubscription.run();  // equivalent to cancelling the subscription
×
619
        }
×
620
      }
×
621
      this.reactiveSubscriptions.clear();
3✔
622
      this.manualCancellationOnContextClose.clear();
3✔
623
    }
3✔
624
    this.registrar.destroy();
3✔
625
    if (this.localScheduler != null) {
3✔
626
      this.localScheduler.destroy();
3✔
627
    }
628
  }
1✔
629

630
  /**
631
   * Reacts to {@link ContextRefreshedEvent} as well as {@link ContextClosedEvent}:
632
   * performing {@link #finishRegistration()} and early cancelling of scheduled tasks,
633
   * respectively.
634
   */
635
  @Override
636
  public void onApplicationEvent(ApplicationContextEvent event) {
637
    if (event.getApplicationContext() == this.applicationContext) {
5!
638
      if (event instanceof ContextRefreshedEvent) {
3✔
639
        // Running in an ApplicationContext -> register tasks this late...
640
        // giving other ContextRefreshedEvent listeners a chance to perform
641
        // their work at the same time (e.g. Infra Batch's job registration).
642
        finishRegistration();
3✔
643
      }
644
      else if (event instanceof ContextClosedEvent) {
3!
645
        for (Object bean : this.manualCancellationOnContextClose) {
10✔
646
          cancelScheduledTasks(bean);
3✔
647
        }
1✔
648
        this.manualCancellationOnContextClose.clear();
3✔
649
      }
650
    }
651
  }
1✔
652

653
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc