Skip to content

Commit 01214b3

Browse files
terminuxsbrannen
authored andcommitted
Allow @async qualifier to be declared with a placeholder or SpEL expression
Closes gh-27818
1 parent 6a73d26 commit 01214b3

File tree

3 files changed

+71
-2
lines changed

3 files changed

+71
-2
lines changed

spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java

+8
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,16 @@
3434
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
3535
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
3636
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
37+
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
38+
import org.springframework.beans.factory.config.EmbeddedValueResolver;
3739
import org.springframework.core.task.AsyncListenableTaskExecutor;
3840
import org.springframework.core.task.AsyncTaskExecutor;
3941
import org.springframework.core.task.TaskExecutor;
4042
import org.springframework.core.task.support.TaskExecutorAdapter;
4143
import org.springframework.lang.Nullable;
4244
import org.springframework.util.ReflectionUtils;
4345
import org.springframework.util.StringUtils;
46+
import org.springframework.util.StringValueResolver;
4447
import org.springframework.util.concurrent.ListenableFuture;
4548
import org.springframework.util.function.SingletonSupplier;
4649

@@ -208,6 +211,11 @@ protected Executor findQualifiedExecutor(@Nullable BeanFactory beanFactory, Stri
208211
throw new IllegalStateException("BeanFactory must be set on " + getClass().getSimpleName() +
209212
" to access qualified executor '" + qualifier + "'");
210213
}
214+
if (beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory) {
215+
StringValueResolver embeddedValueResolver = new EmbeddedValueResolver(configurableBeanFactory);
216+
String resolvedValue = embeddedValueResolver.resolveStringValue(qualifier);
217+
qualifier = resolvedValue != null ? resolvedValue : "";
218+
}
211219
return BeanFactoryAnnotationUtils.qualifiedBeanOfType(beanFactory, Executor.class, qualifier);
212220
}
213221

spring-context/src/main/java/org/springframework/scheduling/annotation/Async.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -57,6 +57,8 @@
5757

5858
/**
5959
* A qualifier value for the specified asynchronous operation(s).
60+
* <p>The value support expression such as <code>#{systemProperties.myExecutor}</code>
61+
* or property placeholder such as <code>${my.app.myExecutor}</code>.
6062
* <p>May be used to determine the target executor to be used when executing
6163
* the asynchronous operation(s), matching the qualifier value (or the bean
6264
* name) of a specific {@link java.util.concurrent.Executor Executor} or

spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java

+60-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
2121
import java.lang.annotation.RetentionPolicy;
2222
import java.lang.annotation.Target;
2323
import java.lang.reflect.Method;
24+
import java.util.Properties;
2425
import java.util.concurrent.ExecutionException;
2526
import java.util.concurrent.Executor;
2627
import java.util.concurrent.Executors;
@@ -47,6 +48,7 @@
4748
import org.springframework.context.annotation.Configuration;
4849
import org.springframework.context.annotation.Import;
4950
import org.springframework.context.annotation.Lazy;
51+
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
5052
import org.springframework.core.Ordered;
5153
import org.springframework.lang.Nullable;
5254
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
@@ -130,6 +132,29 @@ public void withAsyncBeanWithExecutorQualifiedByName() throws ExecutionException
130132
ctx.close();
131133
}
132134

135+
@Test
136+
public void withAsyncBeanWithExecutorQualifiedByExpression() throws ExecutionException, InterruptedException {
137+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
138+
System.getProperties().put("myExecutor", "myExecutor1");
139+
PropertySourcesPlaceholderConfigurer placeholderConfigurer = new PropertySourcesPlaceholderConfigurer();
140+
placeholderConfigurer.setProperties(new Properties() {{
141+
put("my.app.myExecutor", "myExecutor2");
142+
}});
143+
placeholderConfigurer.postProcessBeanFactory(context.getBeanFactory());
144+
145+
context.register(AsyncWithExecutorQualifiedByExpressionConfig.class);
146+
context.refresh();
147+
148+
AsyncBeanWithExecutorQualifiedByExpression asyncBean = context.getBean(AsyncBeanWithExecutorQualifiedByExpression.class);
149+
Future<Thread> workerThread1 = asyncBean.myWork1();
150+
assertThat(workerThread1.get().getName()).doesNotStartWith("myExecutor2-").startsWith("myExecutor1-");
151+
152+
Future<Thread> workerThread2 = asyncBean.myWork2();
153+
assertThat(workerThread2.get().getName()).startsWith("myExecutor2-");
154+
155+
context.close();
156+
}
157+
133158
@Test
134159
public void asyncProcessorIsOrderedLowestPrecedenceByDefault() {
135160
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
@@ -346,6 +371,19 @@ public Future<Thread> work3() {
346371
}
347372
}
348373

374+
static class AsyncBeanWithExecutorQualifiedByExpression {
375+
376+
@Async("#{systemProperties.myExecutor}")
377+
public Future<Thread> myWork1() {
378+
return new AsyncResult<>(Thread.currentThread());
379+
}
380+
381+
@Async("${my.app.myExecutor}")
382+
public Future<Thread> myWork2() {
383+
return new AsyncResult<>(Thread.currentThread());
384+
}
385+
}
386+
349387

350388
static class AsyncBean {
351389

@@ -475,6 +513,27 @@ public Executor otherExecutor() {
475513
}
476514
}
477515

516+
@Configuration
517+
@EnableAsync
518+
static class AsyncWithExecutorQualifiedByExpressionConfig {
519+
520+
@Bean
521+
public AsyncBeanWithExecutorQualifiedByExpression asyncBean() {
522+
return new AsyncBeanWithExecutorQualifiedByExpression();
523+
}
524+
525+
@Bean
526+
public Executor myExecutor1() {
527+
return new ThreadPoolTaskExecutor();
528+
}
529+
530+
@Bean
531+
@Qualifier("myExecutor")
532+
public Executor myExecutor2() {
533+
return new ThreadPoolTaskExecutor();
534+
}
535+
}
536+
478537

479538
@Configuration
480539
@EnableAsync

0 commit comments

Comments
 (0)