Skip to content

Commit 37a334e

Browse files
committed
Add typesafe method to get generic bean by name with type reference
Fix GH-34687 Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
1 parent d5adfd7 commit 37a334e

File tree

10 files changed

+168
-7
lines changed

10 files changed

+168
-7
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
* @author Rod Johnson
101101
* @author Juergen Hoeller
102102
* @author Chris Beams
103+
* @author Yanming Zhou
103104
* @since 13 April 2001
104105
* @see BeanNameAware#setBeanName
105106
* @see BeanClassLoaderAware#setBeanClassLoader
@@ -175,6 +176,29 @@ public interface BeanFactory {
175176
*/
176177
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
177178

179+
/**
180+
* Return an instance, which may be shared or independent, of the specified bean.
181+
* <p>Behaves the same as {@link #getBean(String)}, but provides a measure of type
182+
* safety by throwing a BeanNotOfRequiredTypeException if the bean is not of the
183+
* required type. This means that ClassCastException can't be thrown on casting
184+
* the result correctly, as can happen with {@link #getBean(String)}.
185+
* <p>Translates aliases back to the corresponding canonical bean name.
186+
* <p>Will ask the parent factory if the bean cannot be found in this factory instance.
187+
* @param name the name of the bean to retrieve
188+
* @param typeReference the reference to obtain type the bean must match
189+
* @return an instance of the bean.
190+
* Note that the return value will never be {@code null}. In case of a stub for
191+
* {@code null} from a factory method having been resolved for the requested bean, a
192+
* {@code BeanNotOfRequiredTypeException} against the NullBean stub will be raised.
193+
* Consider using {@link #getBeanProvider(Class)} for resolving optional dependencies.
194+
* @throws NoSuchBeanDefinitionException if there is no such bean definition
195+
* @throws BeanNotOfRequiredTypeException if the bean is not of the required type
196+
* @throws BeansException if the bean could not be created
197+
* @since 7.1
198+
* @see #getBean(String, Class)
199+
*/
200+
<T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException;
201+
178202
/**
179203
* Return an instance, which may be shared or independent, of the specified bean.
180204
* <p>Allows for specifying explicit constructor arguments / factory method arguments,

spring-beans/src/main/java/org/springframework/beans/factory/BeanNotOfRequiredTypeException.java

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@
1616

1717
package org.springframework.beans.factory;
1818

19+
import java.lang.reflect.Type;
20+
1921
import org.springframework.beans.BeansException;
22+
import org.springframework.core.ResolvableType;
2023
import org.springframework.util.ClassUtils;
2124

2225
/**
2326
* Thrown when a bean doesn't match the expected type.
2427
*
2528
* @author Rod Johnson
2629
* @author Juergen Hoeller
30+
* @author Yanming Zhou
2731
*/
2832
@SuppressWarnings("serial")
2933
public class BeanNotOfRequiredTypeException extends BeansException {
@@ -32,7 +36,7 @@ public class BeanNotOfRequiredTypeException extends BeansException {
3236
private final String beanName;
3337

3438
/** The required type. */
35-
private final Class<?> requiredType;
39+
private final Type genericRequiredType;
3640

3741
/** The offending type. */
3842
private final Class<?> actualType;
@@ -46,10 +50,22 @@ public class BeanNotOfRequiredTypeException extends BeansException {
4650
* the expected type
4751
*/
4852
public BeanNotOfRequiredTypeException(String beanName, Class<?> requiredType, Class<?> actualType) {
49-
super("Bean named '" + beanName + "' is expected to be of type '" + ClassUtils.getQualifiedName(requiredType) +
53+
this(beanName, (Type) requiredType, actualType);
54+
}
55+
56+
/**
57+
* Create a new BeanNotOfRequiredTypeException.
58+
* @param beanName the name of the bean requested
59+
* @param requiredType the required type
60+
* @param actualType the actual type returned, which did not match
61+
* the expected type
62+
* @since 7.1
63+
*/
64+
public BeanNotOfRequiredTypeException(String beanName, Type requiredType, Class<?> actualType) {
65+
super("Bean named '" + beanName + "' is expected to be of type '" + requiredType.getTypeName() +
5066
"' but was actually of type '" + ClassUtils.getQualifiedName(actualType) + "'");
5167
this.beanName = beanName;
52-
this.requiredType = requiredType;
68+
this.genericRequiredType = requiredType;
5369
this.actualType = actualType;
5470
}
5571

@@ -65,7 +81,15 @@ public String getBeanName() {
6581
* Return the expected type for the bean.
6682
*/
6783
public Class<?> getRequiredType() {
68-
return this.requiredType;
84+
return (this.genericRequiredType instanceof Class<?> clazz ? clazz : ResolvableType.forType(this.genericRequiredType).toClass());
85+
}
86+
87+
/**
88+
* Return the expected generic type for the bean.
89+
* @since 7.1
90+
*/
91+
public Type getGenericRequiredType() {
92+
return this.genericRequiredType;
6993
}
7094

7195
/**

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.beans.factory.support;
1818

1919
import java.beans.PropertyEditor;
20+
import java.lang.reflect.Type;
2021
import java.util.ArrayList;
2122
import java.util.Arrays;
2223
import java.util.Collection;
@@ -66,6 +67,7 @@
6667
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
6768
import org.springframework.core.DecoratingClassLoader;
6869
import org.springframework.core.NamedThreadLocal;
70+
import org.springframework.core.ParameterizedTypeReference;
6971
import org.springframework.core.ResolvableType;
7072
import org.springframework.core.convert.ConversionService;
7173
import org.springframework.core.log.LogMessage;
@@ -201,6 +203,17 @@ public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
201203
return doGetBean(name, requiredType, null, false);
202204
}
203205

206+
@Override
207+
@SuppressWarnings("unchecked")
208+
public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
209+
Object bean = getBean(name);
210+
Type requiredType = typeReference.getType();
211+
if (!ResolvableType.forType(requiredType).isInstance(bean)) {
212+
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
213+
}
214+
return (T) bean;
215+
}
216+
204217
@Override
205218
public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
206219
return doGetBean(name, null, args, false);

spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.beans.factory.support;
1818

1919
import java.lang.annotation.Annotation;
20+
import java.lang.reflect.Type;
2021
import java.util.ArrayList;
2122
import java.util.Arrays;
2223
import java.util.Collections;
@@ -64,6 +65,7 @@
6465
* @author Rod Johnson
6566
* @author Juergen Hoeller
6667
* @author Sam Brannen
68+
* @author Yanming Zhou
6769
* @since 06.01.2003
6870
* @see DefaultListableBeanFactory
6971
*/
@@ -149,6 +151,17 @@ else if (bean instanceof FactoryBean<?> factoryBean) {
149151
return (T) bean;
150152
}
151153

154+
@Override
155+
@SuppressWarnings("unchecked")
156+
public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
157+
Object bean = getBean(name);
158+
Type requiredType = typeReference.getType();
159+
if (!ResolvableType.forType(requiredType).isInstance(bean)) {
160+
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
161+
}
162+
return (T) bean;
163+
}
164+
152165
@Override
153166
public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
154167
if (!ObjectUtils.isEmpty(args)) {

spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanFactoryExtensions.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,22 @@ import org.springframework.core.ResolvableType
2424
* This extension is not subject to type erasure and retains actual generic type arguments.
2525
*
2626
* @author Sebastien Deleuze
27+
* @author Yanming Zhou
2728
* @since 5.0
2829
*/
2930
inline fun <reified T : Any> BeanFactory.getBean(): T =
3031
getBeanProvider<T>().getObject()
3132

3233
/**
3334
* Extension for [BeanFactory.getBean] providing a `getBean<Foo>("foo")` variant.
34-
* Like the original Java method, this extension is subject to type erasure.
35+
* This extension is not subject to type erasure and retains actual generic type arguments.
3536
*
3637
* @see BeanFactory.getBean(String, Class<T>)
3738
* @author Sebastien Deleuze
3839
* @since 5.0
3940
*/
4041
inline fun <reified T : Any> BeanFactory.getBean(name: String): T =
41-
getBean(name, T::class.java)
42+
getBean(name, (object : ParameterizedTypeReference<T>() {}))
4243

4344
/**
4445
* Extension for [BeanFactory.getBean] providing a `getBean<Foo>(arg1, arg2)` variant.

spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
import org.springframework.beans.testfixture.beans.factory.DummyFactory;
8080
import org.springframework.core.MethodParameter;
8181
import org.springframework.core.Ordered;
82+
import org.springframework.core.ParameterizedTypeReference;
8283
import org.springframework.core.ResolvableType;
8384
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
8485
import org.springframework.core.annotation.Order;
@@ -1682,6 +1683,29 @@ void getBeanByTypeWithAmbiguity() {
16821683
lbf.getBean(TestBean.class));
16831684
}
16841685

1686+
@Test
1687+
void getBeanByNameWithTypeReference() {
1688+
RootBeanDefinition bd1 = new RootBeanDefinition(StringTemplate.class);
1689+
RootBeanDefinition bd2 = new RootBeanDefinition(NumberTemplate.class);
1690+
lbf.registerBeanDefinition("bd1", bd1);
1691+
lbf.registerBeanDefinition("bd2", bd2);
1692+
1693+
Template<String> stringTemplate = lbf.getBean("bd1", new ParameterizedTypeReference<>() {});
1694+
Template<Number> numberTemplate = lbf.getBean("bd2", new ParameterizedTypeReference<>() {});
1695+
1696+
assertThat(stringTemplate).isInstanceOf(StringTemplate.class);
1697+
assertThat(numberTemplate).isInstanceOf(NumberTemplate.class);
1698+
1699+
assertThatExceptionOfType(BeanNotOfRequiredTypeException.class)
1700+
.isThrownBy(() -> lbf.getBean("bd2", new ParameterizedTypeReference<Template<String>>() {}))
1701+
.satisfies(ex -> {
1702+
assertThat(ex.getBeanName()).isEqualTo("bd2");
1703+
assertThat(ex.getRequiredType()).isEqualTo(Template.class);
1704+
assertThat(ex.getActualType()).isEqualTo(NumberTemplate.class);
1705+
assertThat(ex.getGenericRequiredType().toString()).endsWith("Template<java.lang.String>");
1706+
});
1707+
}
1708+
16851709
@Test
16861710
void getBeanByTypeWithPrimary() {
16871711
RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class);
@@ -3872,4 +3896,16 @@ public Class<?> getObjectType() {
38723896
}
38733897
}
38743898

3899+
private static class Template<T> {
3900+
3901+
}
3902+
3903+
private static class StringTemplate extends Template<String> {
3904+
3905+
}
3906+
3907+
private static class NumberTemplate extends Template<Number> {
3908+
3909+
}
3910+
38753911
}

spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import io.mockk.mockk
2121
import io.mockk.verify
2222
import org.assertj.core.api.Assertions.assertThat
2323
import org.junit.jupiter.api.Test
24+
import org.springframework.core.ParameterizedTypeReference
2425
import org.springframework.core.ResolvableType
2526

2627
/**
@@ -53,7 +54,16 @@ class BeanFactoryExtensionsTests {
5354
fun `getBean with String and reified type parameters`() {
5455
val name = "foo"
5556
bf.getBean<Foo>(name)
56-
verify { bf.getBean(name, Foo::class.java) }
57+
verify { bf.getBean(name, ofType<ParameterizedTypeReference<Foo>>()) }
58+
}
59+
60+
@Test
61+
fun `getBean with String and reified generic type parameters`() {
62+
val name = "foo"
63+
val foo = listOf(Foo())
64+
every { bf.getBean(name, ofType<ParameterizedTypeReference<List<Foo>>>()) } returns foo
65+
assertThat(bf.getBean<List<Foo>>("foo")).isSameAs(foo)
66+
verify { bf.getBean(name, ofType<ParameterizedTypeReference<List<Foo>>>()) }
5767
}
5868

5969
@Test

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.io.IOException;
2020
import java.lang.annotation.Annotation;
21+
import java.lang.reflect.Type;
2122
import java.util.ArrayList;
2223
import java.util.Collection;
2324
import java.util.Date;
@@ -132,6 +133,7 @@
132133
* @author Sam Brannen
133134
* @author Sebastien Deleuze
134135
* @author Brian Clozel
136+
* @author Yanming Zhou
135137
* @since January 21, 2001
136138
* @see #refreshBeanFactory
137139
* @see #getBeanFactory
@@ -1305,6 +1307,17 @@ public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
13051307
return getBeanFactory().getBean(name, requiredType);
13061308
}
13071309

1310+
@Override
1311+
@SuppressWarnings("unchecked")
1312+
public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
1313+
Object bean = getBean(name);
1314+
Type requiredType = typeReference.getType();
1315+
if (!ResolvableType.forType(requiredType).isInstance(bean)) {
1316+
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
1317+
}
1318+
return (T) bean;
1319+
}
1320+
13081321
@Override
13091322
public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
13101323
assertBeanFactoryActive();

spring-context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.jndi.support;
1818

19+
import java.lang.reflect.Type;
1920
import java.util.Collections;
2021
import java.util.HashMap;
2122
import java.util.HashSet;
@@ -59,6 +60,7 @@
5960
* in particular if BeanFactory-style type checking is required.
6061
*
6162
* @author Juergen Hoeller
63+
* @author Yanming Zhou
6264
* @since 2.5
6365
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory
6466
* @see org.springframework.context.annotation.CommonAnnotationBeanPostProcessor
@@ -132,6 +134,17 @@ public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
132134
}
133135
}
134136

137+
@Override
138+
@SuppressWarnings("unchecked")
139+
public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
140+
Object bean = getBean(name);
141+
Type requiredType = typeReference.getType();
142+
if (!ResolvableType.forType(requiredType).isInstance(bean)) {
143+
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
144+
}
145+
return (T) bean;
146+
}
147+
135148
@Override
136149
public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
137150
if (args != null) {

spring-test/src/main/java/org/springframework/test/web/servlet/setup/StubWebApplicationContext.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.io.IOException;
2020
import java.lang.annotation.Annotation;
21+
import java.lang.reflect.Type;
2122
import java.util.List;
2223
import java.util.Locale;
2324
import java.util.Map;
@@ -30,6 +31,7 @@
3031
import org.springframework.beans.BeansException;
3132
import org.springframework.beans.TypeConverter;
3233
import org.springframework.beans.factory.BeanFactory;
34+
import org.springframework.beans.factory.BeanNotOfRequiredTypeException;
3335
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
3436
import org.springframework.beans.factory.ObjectProvider;
3537
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
@@ -65,6 +67,7 @@
6567
*
6668
* @author Rossen Stoyanchev
6769
* @author Juergen Hoeller
70+
* @author Yanming Zhou
6871
* @since 3.2
6972
*/
7073
class StubWebApplicationContext implements WebApplicationContext {
@@ -168,6 +171,17 @@ public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
168171
return this.beanFactory.getBean(name, requiredType);
169172
}
170173

174+
@Override
175+
@SuppressWarnings("unchecked")
176+
public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
177+
Object bean = getBean(name);
178+
Type requiredType = typeReference.getType();
179+
if (!ResolvableType.forType(requiredType).isInstance(bean)) {
180+
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
181+
}
182+
return (T) bean;
183+
}
184+
171185
@Override
172186
public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
173187
return this.beanFactory.getBean(name, args);

0 commit comments

Comments
 (0)