diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java index 69ec0b65562..50a62a0f84a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java @@ -43,7 +43,6 @@ import org.springframework.core.task.support.TaskExecutorAdapter; import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; -import org.springframework.util.StringValueResolver; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.function.SingletonSupplier; @@ -212,9 +211,8 @@ public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware { " to access qualified executor '" + qualifier + "'"); } if (beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory) { - StringValueResolver embeddedValueResolver = new EmbeddedValueResolver(configurableBeanFactory); - String resolvedValue = embeddedValueResolver.resolveStringValue(qualifier); - qualifier = resolvedValue != null ? resolvedValue : ""; + EmbeddedValueResolver embeddedValueResolver = new EmbeddedValueResolver(configurableBeanFactory); + qualifier = embeddedValueResolver.resolveStringValue(qualifier); } return BeanFactoryAnnotationUtils.qualifiedBeanOfType(beanFactory, Executor.class, qualifier); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Async.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Async.java index b288c32c058..adc9b1359e4 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/Async.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Async.java @@ -24,7 +24,8 @@ import java.lang.annotation.Target; /** * Annotation that marks a method as a candidate for asynchronous execution. - * Can also be used at the type level, in which case all of the type's methods are + * + *

Can also be used at the type level, in which case all of the type's methods are * considered as asynchronous. Note, however, that {@code @Async} is not supported * on methods declared within a * {@link org.springframework.context.annotation.Configuration @Configuration} class. @@ -41,7 +42,7 @@ import java.lang.annotation.Target; * {@code Future} that can be used to track the result of the asynchronous method * execution. However, since the target method needs to implement the same signature, * it will have to return a temporary {@code Future} handle that just passes a value - * through: e.g. Spring's {@link AsyncResult}, EJB 3.1's {@link jakarta.ejb.AsyncResult}, + * through: for example, Spring's {@link AsyncResult}, EJB 3.1's {@link jakarta.ejb.AsyncResult}, * or {@link java.util.concurrent.CompletableFuture#completedFuture(Object)}. * * @author Juergen Hoeller @@ -57,16 +58,18 @@ public @interface Async { /** * A qualifier value for the specified asynchronous operation(s). - *

The value support expression such as #{systemProperties.myExecutor} - * or property placeholder such as ${my.app.myExecutor}. *

May be used to determine the target executor to be used when executing * the asynchronous operation(s), matching the qualifier value (or the bean * name) of a specific {@link java.util.concurrent.Executor Executor} or * {@link org.springframework.core.task.TaskExecutor TaskExecutor} * bean definition. - *

When specified on a class-level {@code @Async} annotation, indicates that the + *

When specified in a class-level {@code @Async} annotation, indicates that the * given executor should be used for all methods within the class. Method-level use - * of {@code Async#value} always overrides any value set at the class level. + * of {@code Async#value} always overrides any qualifier value configured at + * the class level. + *

The qualifier value will be resolved dynamically if supplied as a SpEL + * expression (for example, {@code "#{environment['myExecutor']}"}) or a + * property placeholder (for example, {@code "${my.app.myExecutor}"}). * @since 3.1.2 */ String value() default ""; diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java index 5bde1c950bd..08c182785c2 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java @@ -21,7 +21,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; -import java.util.Properties; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -42,13 +41,13 @@ import org.springframework.beans.factory.BeanNotOfRequiredTypeException; import org.springframework.beans.factory.UnsatisfiedDependencyException; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AdviceMode; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Lazy; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.core.Ordered; import org.springframework.lang.Nullable; import org.springframework.scheduling.concurrent.CustomizableThreadFactory; @@ -133,26 +132,25 @@ public class EnableAsyncTests { } @Test - public void withAsyncBeanWithExecutorQualifiedByExpression() throws ExecutionException, InterruptedException { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - System.getProperties().put("myExecutor", "myExecutor1"); - PropertySourcesPlaceholderConfigurer placeholderConfigurer = new PropertySourcesPlaceholderConfigurer(); - placeholderConfigurer.setProperties(new Properties() {{ - put("my.app.myExecutor", "myExecutor2"); - }}); - placeholderConfigurer.postProcessBeanFactory(context.getBeanFactory()); + public void withAsyncBeanWithExecutorQualifiedByExpressionOrPlaceholder() throws Exception { + System.setProperty("myExecutor", "myExecutor1"); + System.setProperty("my.app.myExecutor", "myExecutor2"); - context.register(AsyncWithExecutorQualifiedByExpressionConfig.class); - context.refresh(); + Class configClass = AsyncWithExecutorQualifiedByExpressionConfig.class; + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(configClass)) { + AsyncBeanWithExecutorQualifiedByExpressionOrPlaceholder asyncBean = + context.getBean(AsyncBeanWithExecutorQualifiedByExpressionOrPlaceholder.class); - AsyncBeanWithExecutorQualifiedByExpression asyncBean = context.getBean(AsyncBeanWithExecutorQualifiedByExpression.class); - Future workerThread1 = asyncBean.myWork1(); - assertThat(workerThread1.get().getName()).doesNotStartWith("myExecutor2-").startsWith("myExecutor1-"); + Future workerThread1 = asyncBean.myWork1(); + assertThat(workerThread1.get().getName()).startsWith("myExecutor1-"); - Future workerThread2 = asyncBean.myWork2(); - assertThat(workerThread2.get().getName()).startsWith("myExecutor2-"); - - context.close(); + Future workerThread2 = asyncBean.myWork2(); + assertThat(workerThread2.get().getName()).startsWith("myExecutor2-"); + } + finally { + System.clearProperty("myExecutor"); + System.clearProperty("my.app.myExecutor"); + } } @Test @@ -371,9 +369,9 @@ public class EnableAsyncTests { } } - static class AsyncBeanWithExecutorQualifiedByExpression { + static class AsyncBeanWithExecutorQualifiedByExpressionOrPlaceholder { - @Async("#{systemProperties.myExecutor}") + @Async("#{environment['myExecutor']}") public Future myWork1() { return new AsyncResult<>(Thread.currentThread()); } @@ -518,8 +516,8 @@ public class EnableAsyncTests { static class AsyncWithExecutorQualifiedByExpressionConfig { @Bean - public AsyncBeanWithExecutorQualifiedByExpression asyncBean() { - return new AsyncBeanWithExecutorQualifiedByExpression(); + public AsyncBeanWithExecutorQualifiedByExpressionOrPlaceholder asyncBean() { + return new AsyncBeanWithExecutorQualifiedByExpressionOrPlaceholder(); } @Bean