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

TAKETODAY / today-infrastructure / 20224960533

15 Dec 2025 08:11AM UTC coverage: 84.388% (-0.02%) from 84.404%
20224960533

push

github

TAKETODAY
:white_check_mark: 在测试中排除 jacoco 初始化方法以避免干扰

61869 of 78367 branches covered (78.95%)

Branch coverage included in aggregate %.

145916 of 167860 relevant lines covered (86.93%)

3.71 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.concurrent.ConcurrentHashMap;
33
import java.util.concurrent.CopyOnWriteArrayList;
34
import java.util.concurrent.ScheduledExecutorService;
35
import java.util.concurrent.TimeUnit;
36

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

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

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

122
  private final ScheduledTaskRegistrar registrar;
123

124
  @Nullable
125
  private Object scheduler;
126

127
  @Nullable
128
  private StringValueResolver embeddedValueResolver;
129

130
  @Nullable
131
  private String beanName;
132

133
  @Nullable
134
  private BeanFactory beanFactory;
135

136
  @Nullable
137
  private ApplicationContext applicationContext;
138

139
  @Nullable
140
  private TaskSchedulerRouter localScheduler;
141

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

260
  }
1✔
261

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

© 2026 Coveralls, Inc