Skip to content

Commit e6ad203

Browse files
Allow getting annotated bean instance with NamedContextFactory. (#1352)
1 parent 77d221d commit e6ad203

File tree

3 files changed

+162
-29
lines changed

3 files changed

+162
-29
lines changed

spring-cloud-context/src/main/java/org/springframework/cloud/context/named/NamedContextFactory.java

+22-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 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.
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.cloud.context.named;
1818

19+
import java.lang.annotation.Annotation;
20+
import java.util.ArrayList;
1921
import java.util.Collection;
2022
import java.util.Collections;
2123
import java.util.HashMap;
@@ -46,9 +48,8 @@
4648

4749
/**
4850
* Creates a set of child contexts that allows a set of Specifications to define the beans
49-
* in each child context.
50-
*
51-
* Ported from spring-cloud-netflix FeignClientFactory and SpringClientFactory
51+
* in each child context. Ported from spring-cloud-netflix FeignClientFactory and
52+
* SpringClientFactory
5253
*
5354
* @param <C> specification
5455
* @author Spencer Gibb
@@ -230,6 +231,23 @@ public <T> T getInstance(String name, ResolvableType type) {
230231
return null;
231232
}
232233

234+
@SuppressWarnings("unchecked")
235+
public <T> T getAnnotatedInstance(String name, ResolvableType type, Class<? extends Annotation> annotationType) {
236+
GenericApplicationContext context = getContext(name);
237+
String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(context, annotationType);
238+
239+
List<T> beans = new ArrayList<>();
240+
for (String beanName : beanNames) {
241+
if (context.isTypeMatch(beanName, type)) {
242+
beans.add((T) context.getBean(beanName));
243+
}
244+
}
245+
if (beans.size() > 1) {
246+
throw new IllegalStateException("Only one annotated bean for type expected.");
247+
}
248+
return beans.isEmpty() ? null : beans.get(0);
249+
}
250+
233251
public <T> Map<String, T> getInstances(String name, Class<T> type) {
234252
GenericApplicationContext context = getContext(name);
235253

spring-cloud-context/src/test/java/org/springframework/cloud/context/named/NamedContextFactoryTests.java

+139-25
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2024 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.
@@ -16,23 +16,32 @@
1616

1717
package org.springframework.cloud.context.named;
1818

19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Inherited;
22+
import java.lang.annotation.Retention;
23+
import java.lang.annotation.RetentionPolicy;
24+
import java.lang.annotation.Target;
1925
import java.util.Arrays;
26+
import java.util.List;
2027
import java.util.Map;
2128
import java.util.concurrent.ExecutionException;
2229
import java.util.concurrent.ExecutorService;
2330
import java.util.concurrent.Executors;
2431
import java.util.concurrent.TimeUnit;
2532
import java.util.concurrent.TimeoutException;
2633

27-
import org.assertj.core.api.Assertions;
2834
import org.junit.jupiter.api.Test;
2935

3036
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
3137
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3238
import org.springframework.context.annotation.Bean;
3339
import org.springframework.context.support.GenericApplicationContext;
40+
import org.springframework.core.ResolvableType;
3441
import org.springframework.util.ClassUtils;
3542

43+
import static org.assertj.core.api.Assertions.assertThat;
44+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
3645
import static org.assertj.core.api.BDDAssertions.then;
3746

3847
/**
@@ -50,6 +59,64 @@ public void testChildContexts() {
5059
testChildContexts(parent);
5160
}
5261

62+
@Test
63+
void testBadThreadContextClassLoader() throws InterruptedException, ExecutionException, TimeoutException {
64+
AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
65+
parent.setClassLoader(ClassUtils.getDefaultClassLoader());
66+
parent.register(BaseConfig.class);
67+
parent.refresh();
68+
69+
ExecutorService es = Executors.newSingleThreadExecutor(r -> {
70+
Thread t = new Thread(r);
71+
t.setContextClassLoader(new ThrowingClassLoader());
72+
return t;
73+
});
74+
es.submit(() -> this.testChildContexts(parent)).get(5, TimeUnit.SECONDS);
75+
76+
}
77+
78+
@Test
79+
void testGetAnnotatedBeanInstance() {
80+
AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
81+
parent.register(BaseConfig.class);
82+
parent.refresh();
83+
TestClientFactory factory = new TestClientFactory();
84+
factory.setApplicationContext(parent);
85+
factory.setConfigurations(List.of(getSpec("annotated", AnnotatedConfig.class)));
86+
87+
TestType annotatedBean = factory.getAnnotatedInstance("annotated", ResolvableType.forType(TestType.class),
88+
TestBean.class);
89+
90+
assertThat(annotatedBean.value()).isEqualTo(2);
91+
}
92+
93+
@Test
94+
void testNoAnnotatedBeanInstance() {
95+
AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
96+
parent.register(BaseConfig.class);
97+
parent.refresh();
98+
TestClientFactory factory = new TestClientFactory();
99+
factory.setApplicationContext(parent);
100+
factory.setConfigurations(List.of(getSpec("not-annotated", NotAnnotatedConfig.class)));
101+
102+
TestType annotatedBean = factory.getAnnotatedInstance("not-annotated", ResolvableType.forType(TestType.class),
103+
TestBean.class);
104+
assertThat(annotatedBean).isNull();
105+
}
106+
107+
@Test
108+
void testMoreThanOneAnnotatedBeanInstance() {
109+
AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
110+
parent.register(BaseConfig.class);
111+
parent.refresh();
112+
TestClientFactory factory = new TestClientFactory();
113+
factory.setApplicationContext(parent);
114+
factory.setConfigurations(List.of(getSpec("many-annotated", ManyAnnotatedConfig.class)));
115+
116+
assertThatIllegalStateException().isThrownBy(() -> factory.getAnnotatedInstance("many-annotated",
117+
ResolvableType.forType(TestType.class), TestBean.class));
118+
}
119+
53120
private void testChildContexts(GenericApplicationContext parent) {
54121
TestClientFactory factory = new TestClientFactory();
55122
factory.setApplicationContext(parent);
@@ -97,7 +164,7 @@ private void testChildContexts(GenericApplicationContext parent) {
97164
.as("foo context bean factory classloader does not match parent")
98165
.isSameAs(parent.getBeanFactory().getBeanClassLoader());
99166

100-
Assertions.assertThat(fooContext).hasFieldOrPropertyWithValue("customClassLoader", true);
167+
assertThat(fooContext).hasFieldOrPropertyWithValue("customClassLoader", true);
101168

102169
factory.destroy();
103170

@@ -106,22 +173,6 @@ private void testChildContexts(GenericApplicationContext parent) {
106173
then(barContext.isActive()).as("bar context wasn't closed").isFalse();
107174
}
108175

109-
@Test
110-
void testBadThreadContextClassLoader() throws InterruptedException, ExecutionException, TimeoutException {
111-
AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
112-
parent.setClassLoader(ClassUtils.getDefaultClassLoader());
113-
parent.register(BaseConfig.class);
114-
parent.refresh();
115-
116-
ExecutorService es = Executors.newSingleThreadExecutor(r -> {
117-
Thread t = new Thread(r);
118-
t.setContextClassLoader(new ThrowingClassLoader());
119-
return t;
120-
});
121-
122-
es.submit(() -> this.testChildContexts(parent)).get(5, TimeUnit.SECONDS);
123-
}
124-
125176
private TestSpec getSpec(String name, Class<?> configClass) {
126177
return new TestSpec(name, new Class[] { configClass });
127178
}
@@ -236,18 +287,81 @@ static class Bar {
236287

237288
}
238289

239-
static class Container<T> {
290+
record Container<T> (T item) {
291+
292+
}
293+
294+
static class AnnotatedConfig {
295+
296+
@Bean
297+
TestType test1() {
298+
return new TestType(1);
299+
}
240300

241-
private final T item;
301+
@TestBean
302+
@Bean
303+
TestType test2() {
304+
return new TestType(2);
305+
}
242306

243-
Container(T item) {
244-
this.item = item;
307+
@TestBean
308+
@Bean
309+
Bar bar() {
310+
return new Bar();
245311
}
246312

247-
public T getItem() {
248-
return this.item;
313+
}
314+
315+
static class NotAnnotatedConfig {
316+
317+
@Bean
318+
TestType test1() {
319+
return new TestType(1);
249320
}
250321

322+
@Bean
323+
TestType test2() {
324+
return new TestType(2);
325+
}
326+
327+
@TestBean
328+
@Bean
329+
Bar bar() {
330+
return new Bar();
331+
}
332+
333+
}
334+
335+
static class ManyAnnotatedConfig {
336+
337+
@TestBean
338+
@Bean
339+
TestType test1() {
340+
return new TestType(1);
341+
}
342+
343+
@TestBean
344+
@Bean
345+
TestType test2() {
346+
return new TestType(2);
347+
}
348+
349+
@Bean
350+
Bar bar() {
351+
return new Bar();
352+
}
353+
354+
}
355+
356+
record TestType(int value) {
357+
}
358+
359+
@Target({ ElementType.TYPE, ElementType.METHOD })
360+
@Retention(RetentionPolicy.RUNTIME)
361+
@Documented
362+
@Inherited
363+
@interface TestBean {
364+
251365
}
252366

253367
}

src/checkstyle/checkstyle-suppressions.xml

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<suppress files=".*Tests.*" checks="JavadocVariable"/>
1919
<suppress files=".*Tests.*" checks="JavadocMethod"/>
2020
<suppress files=".*Tests.*" checks="HideUtilityClassConstructor"/>
21+
<suppress files=".*Tests.*" checks="MethodParamPad"/>
2122
<suppress files=".*AutoConfiguration.*" checks="HideUtilityClassConstructor"/>
2223
<suppress files=".*AutoConfiguration.*" checks="FinalClass"/>
2324
<suppress files=".*ReactiveDiscoveryClient.*" checks="JavadocVariable"/>

0 commit comments

Comments
 (0)