28
28
import java .util .Set ;
29
29
import java .util .TimeZone ;
30
30
import java .util .concurrent .ConcurrentHashMap ;
31
+ import java .util .concurrent .CopyOnWriteArrayList ;
31
32
import java .util .concurrent .ScheduledExecutorService ;
32
33
import java .util .concurrent .TimeUnit ;
33
34
98
99
* @author Elizabeth Chatman
99
100
* @author Victor Brown
100
101
* @author Sam Brannen
102
+ * @author Simon Baslé
101
103
* @since 3.0
102
104
* @see Scheduled
103
105
* @see EnableScheduling
@@ -143,6 +145,8 @@ public class ScheduledAnnotationBeanPostProcessor
143
145
144
146
private final Map <Object , Set <ScheduledTask >> scheduledTasks = new IdentityHashMap <>(16 );
145
147
148
+ private final Map <Object , List <Runnable >> reactiveSubscriptions = new IdentityHashMap <>(16 );
149
+
146
150
147
151
/**
148
152
* Create a default {@code ScheduledAnnotationBeanPostProcessor}.
@@ -385,15 +389,33 @@ public Object postProcessAfterInitialization(Object bean, String beanName) {
385
389
}
386
390
387
391
/**
388
- * Process the given {@code @Scheduled} method declaration on the given bean.
392
+ * Process the given {@code @Scheduled} method declaration on the given bean,
393
+ * attempting to distinguish {@link #processScheduledAsync(Scheduled, Method, Object) reactive}
394
+ * methods from {@link #processScheduledSync(Scheduled, Method, Object) synchronous} methods.
389
395
* @param scheduled the {@code @Scheduled} annotation
390
396
* @param method the method that the annotation has been declared on
391
397
* @param bean the target bean instance
392
- * @see #createRunnable(Object, Method)
398
+ * @see #processScheduledSync(Scheduled, Method, Object)
399
+ * @see #processScheduledAsync(Scheduled, Method, Object)
393
400
*/
394
401
protected void processScheduled (Scheduled scheduled , Method method , Object bean ) {
402
+ // Is method a Kotlin suspending function? Throws if true but reactor bridge isn't on the classpath.
403
+ // Is method returning a reactive type? Throws if true, but it isn't a deferred Publisher type.
404
+ if (ScheduledAnnotationReactiveSupport .isReactive (method )) {
405
+ processScheduledAsync (scheduled , method , bean );
406
+ return ;
407
+ }
408
+ processScheduledSync (scheduled , method , bean );
409
+ }
410
+
411
+ /**
412
+ * Parse the {@code Scheduled} annotation and schedule the provided {@code Runnable}
413
+ * accordingly. The Runnable can represent either a synchronous method invocation
414
+ * (see {@link #processScheduledSync(Scheduled, Method, Object)}) or an asynchronous
415
+ * one (see {@link #processScheduledAsync(Scheduled, Method, Object)}).
416
+ */
417
+ protected void processScheduledTask (Scheduled scheduled , Runnable runnable , Method method , Object bean ) {
395
418
try {
396
- Runnable runnable = createRunnable (bean , method );
397
419
boolean processedSchedule = false ;
398
420
String errorMessage =
399
421
"Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required" ;
@@ -516,6 +538,53 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean)
516
538
}
517
539
}
518
540
541
+ /**
542
+ * Process the given {@code @Scheduled} method declaration on the given bean,
543
+ * as a synchronous method. The method MUST take no arguments. Its return value
544
+ * is ignored (if any) and the scheduled invocations of the method take place
545
+ * using the underlying {@link TaskScheduler} infrastructure.
546
+ * @param scheduled the {@code @Scheduled} annotation
547
+ * @param method the method that the annotation has been declared on
548
+ * @param bean the target bean instance
549
+ * @see #createRunnable(Object, Method)
550
+ */
551
+ protected void processScheduledSync (Scheduled scheduled , Method method , Object bean ) {
552
+ Runnable task ;
553
+ try {
554
+ task = createRunnable (bean , method );
555
+ }
556
+ catch (IllegalArgumentException ex ) {
557
+ throw new IllegalStateException ("Could not create recurring task for @Scheduled method '" + method .getName () + "': " + ex .getMessage ());
558
+ }
559
+ processScheduledTask (scheduled , task , method , bean );
560
+ }
561
+
562
+ /**
563
+ * Process the given {@code @Scheduled} bean method declaration which returns
564
+ * a {@code Publisher}, or the given Kotlin suspending function converted to a
565
+ * Publisher. A {@code Runnable} which subscribes to that publisher is then repeatedly
566
+ * scheduled according to the annotation configuration.
567
+ * <p>Note that for fixed delay configuration, the subscription is turned into a blocking
568
+ * call instead. Types for which a {@code ReactiveAdapter} is registered but which cannot
569
+ * be deferred (i.e. not a {@code Publisher}) are not supported.
570
+ * @param scheduled the {@code @Scheduled} annotation
571
+ * @param method the method that the annotation has been declared on, which
572
+ * MUST either return a Publisher-adaptable type or be a Kotlin suspending function
573
+ * @param bean the target bean instance
574
+ * @see ScheduledAnnotationReactiveSupport
575
+ */
576
+ protected void processScheduledAsync (Scheduled scheduled , Method method , Object bean ) {
577
+ Runnable task ;
578
+ try {
579
+ task = ScheduledAnnotationReactiveSupport .createSubscriptionRunnable (method , bean , scheduled ,
580
+ this .reactiveSubscriptions .computeIfAbsent (bean , k -> new CopyOnWriteArrayList <>()));
581
+ }
582
+ catch (IllegalArgumentException ex ) {
583
+ throw new IllegalStateException ("Could not create recurring task for @Scheduled method '" + method .getName () + "': " + ex .getMessage ());
584
+ }
585
+ processScheduledTask (scheduled , task , method , bean );
586
+ }
587
+
519
588
/**
520
589
* Create a {@link Runnable} for the given bean instance,
521
590
* calling the specified scheduled method.
@@ -554,6 +623,8 @@ private static boolean isP(char ch) {
554
623
/**
555
624
* Return all currently scheduled tasks, from {@link Scheduled} methods
556
625
* as well as from programmatic {@link SchedulingConfigurer} interaction.
626
+ * <p>Note this includes upcoming scheduled subscriptions for reactive methods,
627
+ * but doesn't cover any currently active subscription for such methods.
557
628
* @since 5.0.2
558
629
*/
559
630
@ Override
@@ -572,20 +643,27 @@ public Set<ScheduledTask> getScheduledTasks() {
572
643
@ Override
573
644
public void postProcessBeforeDestruction (Object bean , String beanName ) {
574
645
Set <ScheduledTask > tasks ;
646
+ List <Runnable > liveSubscriptions ;
575
647
synchronized (this .scheduledTasks ) {
576
648
tasks = this .scheduledTasks .remove (bean );
649
+ liveSubscriptions = this .reactiveSubscriptions .remove (bean );
577
650
}
578
651
if (tasks != null ) {
579
652
for (ScheduledTask task : tasks ) {
580
653
task .cancel ();
581
654
}
582
655
}
656
+ if (liveSubscriptions != null ) {
657
+ for (Runnable subscription : liveSubscriptions ) {
658
+ subscription .run (); // equivalent to cancelling the subscription
659
+ }
660
+ }
583
661
}
584
662
585
663
@ Override
586
664
public boolean requiresDestruction (Object bean ) {
587
665
synchronized (this .scheduledTasks ) {
588
- return this .scheduledTasks .containsKey (bean );
666
+ return this .scheduledTasks .containsKey (bean ) || this . reactiveSubscriptions . containsKey ( bean ) ;
589
667
}
590
668
}
591
669
@@ -599,6 +677,12 @@ public void destroy() {
599
677
}
600
678
}
601
679
this .scheduledTasks .clear ();
680
+ Collection <List <Runnable >> allLiveSubscriptions = this .reactiveSubscriptions .values ();
681
+ for (List <Runnable > liveSubscriptions : allLiveSubscriptions ) {
682
+ for (Runnable liveSubscription : liveSubscriptions ) {
683
+ liveSubscription .run (); //equivalent to cancelling the subscription
684
+ }
685
+ }
602
686
}
603
687
this .registrar .destroy ();
604
688
}
0 commit comments