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 33beb7c9e62a..e47cf6e425f5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -34,6 +34,8 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.EmbeddedValueResolver; import org.springframework.core.task.AsyncListenableTaskExecutor; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.TaskExecutor; @@ -41,6 +43,7 @@ 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; @@ -208,6 +211,11 @@ protected Executor findQualifiedExecutor(@Nullable BeanFactory beanFactory, Stri throw new IllegalStateException("BeanFactory must be set on " + getClass().getSimpleName() + " to access qualified executor '" + qualifier + "'"); } + if (beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory) { + StringValueResolver embeddedValueResolver = new EmbeddedValueResolver(configurableBeanFactory); + String resolvedValue = embeddedValueResolver.resolveStringValue(qualifier); + qualifier = resolvedValue != null ? resolvedValue : ""; + } 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 40e572f0e8cc..b288c32c0583 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -57,6 +57,8 @@ /** * 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 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 e60333950f40..5bde1c950bd1 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -21,6 +21,7 @@ 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; @@ -47,6 +48,7 @@ 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; @@ -130,6 +132,29 @@ public void withAsyncBeanWithExecutorQualifiedByName() throws ExecutionException ctx.close(); } + @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()); + + context.register(AsyncWithExecutorQualifiedByExpressionConfig.class); + context.refresh(); + + AsyncBeanWithExecutorQualifiedByExpression asyncBean = context.getBean(AsyncBeanWithExecutorQualifiedByExpression.class); + Future workerThread1 = asyncBean.myWork1(); + assertThat(workerThread1.get().getName()).doesNotStartWith("myExecutor2-").startsWith("myExecutor1-"); + + Future workerThread2 = asyncBean.myWork2(); + assertThat(workerThread2.get().getName()).startsWith("myExecutor2-"); + + context.close(); + } + @Test public void asyncProcessorIsOrderedLowestPrecedenceByDefault() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); @@ -346,6 +371,19 @@ public Future work3() { } } + static class AsyncBeanWithExecutorQualifiedByExpression { + + @Async("#{systemProperties.myExecutor}") + public Future myWork1() { + return new AsyncResult<>(Thread.currentThread()); + } + + @Async("${my.app.myExecutor}") + public Future myWork2() { + return new AsyncResult<>(Thread.currentThread()); + } + } + static class AsyncBean { @@ -475,6 +513,27 @@ public Executor otherExecutor() { } } + @Configuration + @EnableAsync + static class AsyncWithExecutorQualifiedByExpressionConfig { + + @Bean + public AsyncBeanWithExecutorQualifiedByExpression asyncBean() { + return new AsyncBeanWithExecutorQualifiedByExpression(); + } + + @Bean + public Executor myExecutor1() { + return new ThreadPoolTaskExecutor(); + } + + @Bean + @Qualifier("myExecutor") + public Executor myExecutor2() { + return new ThreadPoolTaskExecutor(); + } + } + @Configuration @EnableAsync