diff --git a/framework-docs/modules/ROOT/pages/integration/scheduling.adoc b/framework-docs/modules/ROOT/pages/integration/scheduling.adoc index 77908e9f93..886f2941aa 100644 --- a/framework-docs/modules/ROOT/pages/integration/scheduling.adoc +++ b/framework-docs/modules/ROOT/pages/integration/scheduling.adoc @@ -429,7 +429,6 @@ to `Publisher` but doesn't support deferred subscription. Its `ReactiveAdapter` registry denotes that by having the `getDescriptor().isDeferred()` method return `false`. ==== - - Kotlin suspending functions, like in the following example: [source,kotlin,indent=0,subs="verbatim,quotes"] @@ -453,12 +452,12 @@ registry denotes that by having the `getDescriptor().isDeferred()` method return ---- All these types of methods must be declared without any arguments. In the case of Kotlin -suspending functions the `kotlinx.coroutines.reactor` bridge must also be present to allow +suspending functions, the `kotlinx.coroutines.reactor` bridge must also be present to allow the framework to invoke a suspending function as a `Publisher`. -The Spring Framework will obtain a `Publisher` out of the annotated method once and will +The Spring Framework will obtain a `Publisher` for the annotated method once and will schedule a `Runnable` in which it subscribes to said `Publisher`. These inner regular -subscriptions happen according to the `cron`/fixedDelay`/`fixedRate` configuration. +subscriptions occur according to the corresponding `cron`/fixedDelay`/`fixedRate` configuration. If the `Publisher` emits `onNext` signal(s), these are ignored and discarded (the same way return values from synchronous `@Scheduled` methods are ignored). @@ -474,15 +473,15 @@ seconds, but these values are unused: } ---- -If the `Publisher` emits an `onError` signal, it is logged at WARN level and recovered. +If the `Publisher` emits an `onError` signal, it is logged at `WARN` level and recovered. Because of the asynchronous and lazy nature of `Publisher` instances, exceptions are -not thrown from the Runnable task: this means that the `ErrorHandler` contract is not -involved for Reactive methods. +not thrown from the `Runnable` task: this means that the `ErrorHandler` contract is not +involved for reactive methods. -As a result, further scheduled subscription do happen despite the error. +As a result, further scheduled subscription occurs despite the error. -In the following example, the `Mono` subscription fails twice in the first five seconds -then subscriptions start succeeding, printing a message to the standard output every five +In the following example, the `Mono` subscription fails twice in the first five seconds. +Then subscriptions start succeeding, printing a message to the standard output every five seconds: [source,java,indent=0,subs="verbatim,quotes"] @@ -502,9 +501,9 @@ seconds: [NOTE] ==== -When destroying the annotated bean or closing the application context Spring Framework cancels +When destroying the annotated bean or closing the application context, Spring Framework cancels scheduled tasks, which includes the next scheduled subscription to the `Publisher` as well -as any past subscription that is still currently active (e.g. for long-running publishers, +as any past subscription that is still currently active (e.g. for long-running publishers or even infinite publishers). ==== diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java index 84ddfe4f28..5f02c4a783 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java index 5f2fe23b0a..4e42490a38 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,28 +32,28 @@ import org.springframework.scheduling.config.ScheduledTaskRegistrar; * {@link #cron}, {@link #fixedDelay}, or {@link #fixedRate} attributes must be * specified. * - *
The annotated method must expect no arguments. It will typically have + *
The annotated method must not accept arguments. It will typically have * a {@code void} return type; if not, the returned value will be ignored * when called through the scheduler. * *
Methods that return a reactive {@code Publisher} or a type which can be adapted * to {@code Publisher} by the default {@code ReactiveAdapterRegistry} are supported. - * The {@code Publisher} MUST support multiple subsequent subscriptions (i.e. be cold). - * The returned Publisher is only produced once, and the scheduling infrastructure - * then periodically {@code subscribe()} to it according to configuration. - * Values emitted by the publisher are ignored. Errors are logged at WARN level, which - * doesn't prevent further iterations. If a {@code fixed delay} is configured, the - * subscription is blocked upon in order to respect the fixed delay semantics. + * The {@code Publisher} must support multiple subsequent subscriptions (i.e. be cold). + * The returned {@code Publisher} is only produced once, and the scheduling infrastructure + * then periodically subscribes to it according to configuration. Values emitted by + * the publisher are ignored. Errors are logged at {@code WARN} level, which + * doesn't prevent further iterations. If a fixed delay is configured, the + * subscription is blocked in order to respect the fixed delay semantics. * *
Kotlin suspending functions are also supported, provided the coroutine-reactor * bridge ({@code kotlinx.coroutine.reactor}) is present at runtime. This bridge is - * used to adapt the suspending function into a {@code Publisher} which is treated + * used to adapt the suspending function to a {@code Publisher} which is treated * the same way as in the reactive method case (see above). * - *
Processing of {@code @Scheduled} annotations is performed by
- * registering a {@link ScheduledAnnotationBeanPostProcessor}. This can be
- * done manually or, more conveniently, through the {@code
Processing of {@code @Scheduled} annotations is performed by registering a
+ * {@link ScheduledAnnotationBeanPostProcessor}. This can be done manually or,
+ * more conveniently, through the {@code
This annotation can be used as a {@linkplain Repeatable repeatable} * annotation. diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java index 01a5244d84..6dedc4e864 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java @@ -84,7 +84,7 @@ import org.springframework.util.StringValueResolver; * "fixedRate", "fixedDelay", or "cron" expression provided via the annotation. * *
This post-processor is automatically registered by Spring's
- * {@code Autodetects any {@link SchedulingConfigurer} instances in the container,
@@ -390,8 +390,9 @@ public class ScheduledAnnotationBeanPostProcessor
/**
* Process the given {@code @Scheduled} method declaration on the given bean,
- * attempting to distinguish {@link #processScheduledAsync(Scheduled, Method, Object) reactive}
- * methods from {@link #processScheduledSync(Scheduled, Method, Object) synchronous} methods.
+ * attempting to distinguish {@linkplain #processScheduledAsync(Scheduled, Method, Object)
+ * reactive} methods from {@linkplain #processScheduledSync(Scheduled, Method, Object)
+ * synchronous} methods.
* @param scheduled the {@code @Scheduled} annotation
* @param method the method that the annotation has been declared on
* @param bean the target bean instance
@@ -399,8 +400,8 @@ public class ScheduledAnnotationBeanPostProcessor
* @see #processScheduledAsync(Scheduled, Method, Object)
*/
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
- // Is method a Kotlin suspending function? Throws if true but reactor bridge isn't on the classpath.
- // Is method returning a reactive type? Throws if true, but it isn't a deferred Publisher type.
+ // Is the method a Kotlin suspending function? Throws if true and the reactor bridge isn't on the classpath.
+ // Does the method return a reactive type? Throws if true and it isn't a deferred Publisher type.
if (ScheduledAnnotationReactiveSupport.isReactive(method)) {
processScheduledAsync(scheduled, method, bean);
return;
@@ -540,8 +541,8 @@ public class ScheduledAnnotationBeanPostProcessor
/**
* Process the given {@code @Scheduled} method declaration on the given bean,
- * as a synchronous method. The method MUST take no arguments. Its return value
- * is ignored (if any) and the scheduled invocations of the method take place
+ * as a synchronous method. The method must accept no arguments. Its return value
+ * is ignored (if any), and the scheduled invocations of the method take place
* using the underlying {@link TaskScheduler} infrastructure.
* @param scheduled the {@code @Scheduled} annotation
* @param method the method that the annotation has been declared on
@@ -562,14 +563,14 @@ public class ScheduledAnnotationBeanPostProcessor
/**
* Process the given {@code @Scheduled} bean method declaration which returns
* a {@code Publisher}, or the given Kotlin suspending function converted to a
- * Publisher. A {@code Runnable} which subscribes to that publisher is then repeatedly
- * scheduled according to the annotation configuration.
+ * {@code Publisher}. A {@code Runnable} which subscribes to that publisher is
+ * then repeatedly scheduled according to the annotation configuration.
* Note that for fixed delay configuration, the subscription is turned into a blocking
* call instead. Types for which a {@code ReactiveAdapter} is registered but which cannot
- * be deferred (i.e. not a {@code Publisher}) are not supported.
+ * be deferred (i.e. not a {@code Publisher}) are not supported.
* @param scheduled the {@code @Scheduled} annotation
* @param method the method that the annotation has been declared on, which
- * MUST either return a Publisher-adaptable type or be a Kotlin suspending function
+ * must either return a Publisher-adaptable type or be a Kotlin suspending function
* @param bean the target bean instance
* @see ScheduledAnnotationReactiveSupport
*/
@@ -623,8 +624,8 @@ public class ScheduledAnnotationBeanPostProcessor
/**
* Return all currently scheduled tasks, from {@link Scheduled} methods
* as well as from programmatic {@link SchedulingConfigurer} interaction.
- * Note this includes upcoming scheduled subscriptions for reactive methods,
- * but doesn't cover any currently active subscription for such methods.
+ * Note that this includes upcoming scheduled subscriptions for reactive
+ * methods but doesn't cover any currently active subscription for such methods.
* @since 5.0.2
*/
@Override
diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupport.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupport.java
index f5998132d3..e34dcfc282 100644
--- a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupport.java
+++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupport.java
@@ -40,8 +40,9 @@ import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
- * Helper class for @{@link ScheduledAnnotationBeanPostProcessor} to support reactive cases
- * without a dependency on optional classes.
+ * Helper class for @{@link ScheduledAnnotationBeanPostProcessor} to support reactive
+ * cases without a dependency on optional classes.
+ *
* @author Simon Baslé
* @since 6.1.0
*/
@@ -58,23 +59,23 @@ abstract class ScheduledAnnotationReactiveSupport {
/**
* Checks that if the method is reactive, it can be scheduled. Methods are considered
* eligible for reactive scheduling if they either return an instance of a type that
- * can be converted to {@code Publisher} or are a Kotlin Suspending Function.
- * If the method isn't matching these criteria then this check returns {@code false}.
- * For scheduling of Kotlin Suspending Functions, the Coroutine-Reactor bridge
- * {@code kotlinx.coroutines.reactor} MUST be present at runtime (in order to invoke
- * suspending functions as a {@code Publisher}).
- * Provided that is the case, this method returns {@code true}. Otherwise, it throws
- * an {@code IllegalStateException}.
+ * can be converted to {@code Publisher} or are a Kotlin suspending function.
+ * If the method doesn't match these criteria, this check returns {@code false}.
+ * For scheduling of Kotlin suspending functions, the Coroutine-Reactor bridge
+ * {@code kotlinx.coroutines.reactor} must be present at runtime (in order to invoke
+ * suspending functions as a {@code Publisher}). Provided that is the case, this
+ * method returns {@code true}. Otherwise, it throws an {@code IllegalStateException}.
* @throws IllegalStateException if the method is reactive but Reactor and/or the
* Kotlin coroutines bridge are not present at runtime
*/
static boolean isReactive(Method method) {
if (KotlinDetector.isKotlinPresent() && KotlinDetector.isSuspendingFunction(method)) {
- //Note that suspending functions declared without args have a single Continuation parameter in reflective inspection
- Assert.isTrue(method.getParameterCount() == 1,"Kotlin suspending functions may only be"
- + " annotated with @Scheduled if declared without arguments");
- Assert.isTrue(coroutinesReactorPresent, "Kotlin suspending functions may only be annotated with"
- + " @Scheduled if the Coroutine-Reactor bridge (kotlinx.coroutines.reactor) is present at runtime");
+ // Note that suspending functions declared without args have a single Continuation
+ // parameter in reflective inspection
+ Assert.isTrue(method.getParameterCount() == 1,"Kotlin suspending functions may only be "
+ + "annotated with @Scheduled if declared without arguments");
+ Assert.isTrue(coroutinesReactorPresent, "Kotlin suspending functions may only be annotated with "
+ + "@Scheduled if the Coroutine-Reactor bridge (kotlinx.coroutines.reactor) is present at runtime");
return true;
}
ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance();
@@ -86,10 +87,10 @@ abstract class ScheduledAnnotationReactiveSupport {
if (candidateAdapter == null) {
return false;
}
- Assert.isTrue(method.getParameterCount() == 0, "Reactive methods may only be annotated with"
- + " @Scheduled if declared without arguments");
- Assert.isTrue(candidateAdapter.getDescriptor().isDeferred(), "Reactive methods may only be annotated with"
- + " @Scheduled if the return type supports deferred execution");
+ Assert.isTrue(method.getParameterCount() == 0, "Reactive methods may only be annotated with "
+ + "@Scheduled if declared without arguments");
+ Assert.isTrue(candidateAdapter.getDescriptor().isDeferred(), "Reactive methods may only be annotated with "
+ + "@Scheduled if the return type supports deferred execution");
return true;
}
@@ -98,9 +99,9 @@ abstract class ScheduledAnnotationReactiveSupport {
* either by reflectively invoking it and converting the result to a {@code Publisher}
* via {@link ReactiveAdapterRegistry} or by converting a Kotlin suspending function
* into a {@code Publisher} via {@link CoroutinesUtils}.
- * The {@link #isReactive(Method)} check is a precondition to calling this method.
- * If Reactor is present at runtime, the Publisher is additionally converted to a {@code Flux}
- * with a checkpoint String, allowing for better debugging.
+ * The {@link #isReactive(Method)} check is a precondition to calling this method.
+ * If Reactor is present at runtime, the {@code Publisher} is additionally converted
+ * to a {@code Flux} with a checkpoint String, allowing for better debugging.
*/
static Publisher> getPublisherFor(Method method, Object bean) {
if (KotlinDetector.isKotlinPresent() && KotlinDetector.isSuspendingFunction(method)) {
@@ -114,7 +115,7 @@ abstract class ScheduledAnnotationReactiveSupport {
throw new IllegalArgumentException("Cannot convert the @Scheduled reactive method return type to Publisher");
}
if (!adapter.getDescriptor().isDeferred()) {
- throw new IllegalArgumentException("Cannot convert the @Scheduled reactive method return type to Publisher, "
+ throw new IllegalArgumentException("Cannot convert the @Scheduled reactive method return type to Publisher: "
+ returnType.getSimpleName() + " is not a deferred reactive type");
}
Method invocableMethod = AopUtils.selectInvocableMethod(method, bean.getClass());
@@ -123,7 +124,7 @@ abstract class ScheduledAnnotationReactiveSupport {
Object r = invocableMethod.invoke(bean);
Publisher> publisher = adapter.toPublisher(r);
- //if Reactor is on the classpath, we could benefit from having a checkpoint for debuggability
+ // If Reactor is on the classpath, we could benefit from having a checkpoint for debuggability
if (reactorPresent) {
final String checkpoint = "@Scheduled '"+ method.getName() + "()' in bean '"
+ method.getDeclaringClass().getName() + "'";
@@ -134,10 +135,13 @@ abstract class ScheduledAnnotationReactiveSupport {
}
}
catch (InvocationTargetException ex) {
- throw new IllegalArgumentException("Cannot obtain a Publisher-convertible value from the @Scheduled reactive method", ex.getTargetException());
+ throw new IllegalArgumentException(
+ "Cannot obtain a Publisher-convertible value from the @Scheduled reactive method",
+ ex.getTargetException());
}
catch (IllegalAccessException ex) {
- throw new IllegalArgumentException("Cannot obtain a Publisher-convertible value from the @Scheduled reactive method", ex);
+ throw new IllegalArgumentException(
+ "Cannot obtain a Publisher-convertible value from the @Scheduled reactive method", ex);
}
}
@@ -146,10 +150,10 @@ abstract class ScheduledAnnotationReactiveSupport {
* subscription to the publisher produced by a reactive method.
* Note that the reactive method is invoked once, but the resulting {@code Publisher}
* is subscribed to repeatedly, once per each invocation of the {@code Runnable}.
- * In the case of a {@code fixed delay} configuration, the subscription inside the
- * Runnable is turned into a blocking call in order to maintain fixedDelay semantics
- * (i.e. the task blocks until completion of the Publisher, then the delay is applied
- * until next iteration).
+ * In the case of a fixed-delay configuration, the subscription inside the
+ * {@link Runnable} is turned into a blocking call in order to maintain fixed-delay
+ * semantics (i.e. the task blocks until completion of the Publisher, and the
+ * delay is applied until the next iteration).
*/
static Runnable createSubscriptionRunnable(Method method, Object targetBean, Scheduled scheduled,
List