Repository: spring-projects/spring-framework (2025-11-13T07:07:44Z)

Issue:
Add typesafe method to get generic bean by name with `ParameterizedTypeReference`
Related to GH-34669, but it requires the bean name matched first, and throw `BeanNotOfRequiredTypeException` if the bean type doesn't match required type from type reference.

Take codes in Spring Batch for example:

```java
		@SuppressWarnings("unchecked")
		ItemReader<CustomerCredit> outputReader = (ItemReader<CustomerCredit>) applicationContext
			.getBean("outputTestReader");
```
I'd like it could be rewrite to:
```java
		ItemReader<CustomerCredit> outputReader = applicationContext.getBean("outputTestReader", new ParameterizedTypeReference<>() {});
```


Discussion:
Here is prototype: https://github.com/spring-projects/spring-framework/compare/main...quaff:spring-framework:patch-112?expand=1

I'd like to create PR if the proposal is accepted.
Any chance target it in 7.0?
This is effectively a duplicate of the just reopened #31444, so I'll close it as such. We are about to introduce `getBeanProvider(ParameterizedTypeReference)` for 7.0 RC1; no `getBean` overloads though since those do not have `ResolvableType` variant either.
> This is effectively a duplicate of the just reopened [#31444](https://github.com/spring-projects/spring-framework/issues/31444), so I'll close it as such. We are about to introduce `getBeanProvider(ParameterizedTypeReference)` for 7.0 RC1; no `getBean` overloads though since those do not have `ResolvableType` variant either.

I don't think it's duplicate of #31444, this issue requests adding new variant of `getBean(name, requiredType)` not `getBean(requiredType)` (#34669), it get bean by name, and type safe for generic type. @jhoeller 
> > This is effectively a duplicate of the just reopened [#31444](https://github.com/spring-projects/spring-framework/issues/31444), so I'll close it as such. We are about to introduce `getBeanProvider(ParameterizedTypeReference)` for 7.0 RC1; no `getBean` overloads though since those do not have `ResolvableType` variant either.
> 
> I don't think it's duplicate of [#31444](https://github.com/spring-projects/spring-framework/issues/31444), this issue requests adding new variant of `getBean(name, requiredType)` not `getBean(requiredType)` ([#34669](https://github.com/spring-projects/spring-framework/pull/34669)), it get bean by name, and type safe for generic type. [@jhoeller](https://github.com/jhoeller)

@jhoeller Please consider to reopen it.
@quaff indeed, I mistook it as a duplicate while it is rather a related issue.

As for this specific case, we don't have `getBean(name, requiredType)` overloads for `ResolvableType` either, so we'd have to add both that and a `ParameterizedTypeReference` overload.

However, since that type does not have lookup semantics, just cast semantics, I wonder whether we wouldn't be better off providing a variant of `getBean(name)` that declares an unconditional `T` return type which accepts any assignment to a typed variable on the left-hand side, for example `<T> T getTypedBean(String name)`. No assignability check of our own then, just an optimistic assignment that leads to a runtime cast by the JVM if not actually assignable.

FWIW at some point we had that in a separate convenience adapter (see https://docs.spring.io/spring-framework/docs/2.5.x/javadoc-api/org/springframework/beans/factory/generic/GenericBeanFactoryAccessor.html) but never rolled it into the main `BeanFactory` interface.
On review, such a T-bound lookup by name only really saves a cast but does not provide an actual type check. Maybe it's more practical to provide a `getBean(name, ParameterizedTypeReference)` method indeed - and no `ResolvableType` variant either since that relies on a T-bound return value as well, driven by the result assignment and not by the argument-provided type.

However, an explicit generic type check - just for type safety - is somewhat expensive. For a lookup by name, that's actually the most expensive part since the lookup itself is just a hash retrieval. To some degree it does seem attractive to leave that up to the JVM when it performs the cast for the result assignment.
> However, an explicit generic type check - just for type safety - is somewhat expensive. For a lookup by name, that's actually the most expensive part since the lookup itself is just a hash retrieval. To some degree it does seem attractive to leave that up to the JVM when it performs the cast for the result assignment.

The most important thing is that `BeanNotOfRequiredTypeException` should be thrown like `getBean(String name, Class<T> requiredType)`, application could catch `BeanNotOfRequiredTypeException` immediately, not later `ClassCastException` on actual use.

I've created #35808 to fix it, please review.



Superseded by PR #35808. Let's work on the PR toward 7.1 M1 (which we aim to release in April).



Patch:
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java
index ad47efb2b2ae..b7ffe33ede7c 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java
@@ -100,6 +100,7 @@
  * @author Rod Johnson
  * @author Juergen Hoeller
  * @author Chris Beams
+ * @author Yanming Zhou
  * @since 13 April 2001
  * @see BeanNameAware#setBeanName
  * @see BeanClassLoaderAware#setBeanClassLoader
@@ -175,6 +176,29 @@ public interface BeanFactory {
 	 */
 	<T> T getBean(String name, Class<T> requiredType) throws BeansException;
 
+	/**
+	 * Return an instance, which may be shared or independent, of the specified bean.
+	 * <p>Behaves the same as {@link #getBean(String)}, but provides a measure of type
+	 * safety by throwing a BeanNotOfRequiredTypeException if the bean is not of the
+	 * required type. This means that ClassCastException can't be thrown on casting
+	 * the result correctly, as can happen with {@link #getBean(String)}.
+	 * <p>Translates aliases back to the corresponding canonical bean name.
+	 * <p>Will ask the parent factory if the bean cannot be found in this factory instance.
+	 * @param name the name of the bean to retrieve
+	 * @param typeReference the reference to obtain type the bean must match
+	 * @return an instance of the bean.
+	 * Note that the return value will never be {@code null}. In case of a stub for
+	 * {@code null} from a factory method having been resolved for the requested bean, a
+	 * {@code BeanNotOfRequiredTypeException} against the NullBean stub will be raised.
+	 * Consider using {@link #getBeanProvider(Class)} for resolving optional dependencies.
+	 * @throws NoSuchBeanDefinitionException if there is no such bean definition
+	 * @throws BeanNotOfRequiredTypeException if the bean is not of the required type
+	 * @throws BeansException if the bean could not be created
+	 * @since 7.1
+	 * @see #getBean(String, Class)
+	 */
+	<T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException;
+
 	/**
 	 * Return an instance, which may be shared or independent, of the specified bean.
 	 * <p>Allows for specifying explicit constructor arguments / factory method arguments,
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanNotOfRequiredTypeException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanNotOfRequiredTypeException.java
index bbc504098aad..e574aec05af4 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanNotOfRequiredTypeException.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanNotOfRequiredTypeException.java
@@ -16,7 +16,10 @@
 
 package org.springframework.beans.factory;
 
+import java.lang.reflect.Type;
+
 import org.springframework.beans.BeansException;
+import org.springframework.core.ResolvableType;
 import org.springframework.util.ClassUtils;
 
 /**
@@ -24,6 +27,7 @@
  *
  * @author Rod Johnson
  * @author Juergen Hoeller
+ * @author Yanming Zhou
  */
 @SuppressWarnings("serial")
 public class BeanNotOfRequiredTypeException extends BeansException {
@@ -32,7 +36,7 @@ public class BeanNotOfRequiredTypeException extends BeansException {
 	private final String beanName;
 
 	/** The required type. */
-	private final Class<?> requiredType;
+	private final Type genericRequiredType;
 
 	/** The offending type. */
 	private final Class<?> actualType;
@@ -46,10 +50,22 @@ public class BeanNotOfRequiredTypeException extends BeansException {
 	 * the expected type
 	 */
 	public BeanNotOfRequiredTypeException(String beanName, Class<?> requiredType, Class<?> actualType) {
-		super("Bean named '" + beanName + "' is expected to be of type '" + ClassUtils.getQualifiedName(requiredType) +
+		this(beanName, (Type) requiredType, actualType);
+	}
+
+	/**
+	 * Create a new BeanNotOfRequiredTypeException.
+	 * @param beanName the name of the bean requested
+	 * @param requiredType the required type
+	 * @param actualType the actual type returned, which did not match
+	 * the expected type
+	 * @since 7.1
+	 */
+	public BeanNotOfRequiredTypeException(String beanName, Type requiredType, Class<?> actualType) {
+		super("Bean named '" + beanName + "' is expected to be of type '" + requiredType.getTypeName() +
 				"' but was actually of type '" + ClassUtils.getQualifiedName(actualType) + "'");
 		this.beanName = beanName;
-		this.requiredType = requiredType;
+		this.genericRequiredType = requiredType;
 		this.actualType = actualType;
 	}
 
@@ -65,7 +81,15 @@ public String getBeanName() {
 	 * Return the expected type for the bean.
 	 */
 	public Class<?> getRequiredType() {
-		return this.requiredType;
+		return (this.genericRequiredType instanceof Class<?> clazz ? clazz : ResolvableType.forType(this.genericRequiredType).toClass());
+	}
+
+	/**
+	 * Return the expected generic type for the bean.
+	 * @since 7.1
+	 */
+	public Type getGenericRequiredType() {
+		return this.genericRequiredType;
 	}
 
 	/**
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java
index 54a5921b546c..4ce57a8a8963 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java
@@ -17,6 +17,7 @@
 package org.springframework.beans.factory.support;
 
 import java.beans.PropertyEditor;
+import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -66,6 +67,7 @@
 import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
 import org.springframework.core.DecoratingClassLoader;
 import org.springframework.core.NamedThreadLocal;
+import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.core.ResolvableType;
 import org.springframework.core.convert.ConversionService;
 import org.springframework.core.log.LogMessage;
@@ -201,6 +203,17 @@ public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
 		return doGetBean(name, requiredType, null, false);
 	}
 
+	@Override
+	@SuppressWarnings("unchecked")
+	public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
+		Object bean = getBean(name);
+		Type requiredType = typeReference.getType();
+		if (!ResolvableType.forType(requiredType).isInstance(bean)) {
+			throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
+		}
+		return (T) bean;
+	}
+
 	@Override
 	public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
 		return doGetBean(name, null, args, false);
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java
index 87fbc1a27512..7a1570c660d3 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java
@@ -17,6 +17,7 @@
 package org.springframework.beans.factory.support;
 
 import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -64,6 +65,7 @@
  * @author Rod Johnson
  * @author Juergen Hoeller
  * @author Sam Brannen
+ * @author Yanming Zhou
  * @since 06.01.2003
  * @see DefaultListableBeanFactory
  */
@@ -149,6 +151,17 @@ else if (bean instanceof FactoryBean<?> factoryBean) {
 		return (T) bean;
 	}
 
+	@Override
+	@SuppressWarnings("unchecked")
+	public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
+		Object bean = getBean(name);
+		Type requiredType = typeReference.getType();
+		if (!ResolvableType.forType(requiredType).isInstance(bean)) {
+			throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
+		}
+		return (T) bean;
+	}
+
 	@Override
 	public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
 		if (!ObjectUtils.isEmpty(args)) {
diff --git a/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanFactoryExtensions.kt b/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanFactoryExtensions.kt
index 608b11a50dc5..3bd4d67bb2ea 100644
--- a/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanFactoryExtensions.kt
+++ b/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanFactoryExtensions.kt
@@ -24,6 +24,7 @@ import org.springframework.core.ResolvableType
  * This extension is not subject to type erasure and retains actual generic type arguments.
  *
  * @author Sebastien Deleuze
+ * @author Yanming Zhou
  * @since 5.0
  */
 inline fun <reified T : Any> BeanFactory.getBean(): T =
@@ -31,14 +32,14 @@ inline fun <reified T : Any> BeanFactory.getBean(): T =
 
 /**
  * Extension for [BeanFactory.getBean] providing a `getBean<Foo>("foo")` variant.
- * Like the original Java method, this extension is subject to type erasure.
+ * This extension is not subject to type erasure and retains actual generic type arguments.
  *
  * @see BeanFactory.getBean(String, Class<T>)
  * @author Sebastien Deleuze
  * @since 5.0
  */
 inline fun <reified T : Any> BeanFactory.getBean(name: String): T =
-		getBean(name, T::class.java)
+		getBean(name, (object : ParameterizedTypeReference<T>() {}))
 
 /**
  * Extension for [BeanFactory.getBean] providing a `getBean<Foo>(arg1, arg2)` variant.
diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java
index dc3da9309eba..776283617942 100644
--- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java
+++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java
@@ -132,6 +132,7 @@
  * @author Sam Brannen
  * @author Sebastien Deleuze
  * @author Brian Clozel
+ * @author Yanming Zhou
  * @since January 21, 2001
  * @see #refreshBeanFactory
  * @see #getBeanFactory
@@ -1305,6 +1306,12 @@ public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
 		return getBeanFactory().getBean(name, requiredType);
 	}
 
+	@Override
+	public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
+		assertBeanFactoryActive();
+		return getBeanFactory().getBean(name, typeReference);
+	}
+
 	@Override
 	public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
 		assertBeanFactoryActive();
diff --git a/spring-context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java b/spring-context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java
index cd6f5feb498e..224df23e114d 100644
--- a/spring-context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java
+++ b/spring-context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java
@@ -16,6 +16,7 @@
 
 package org.springframework.jndi.support;
 
+import java.lang.reflect.Type;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -59,6 +60,7 @@
  * in particular if BeanFactory-style type checking is required.
  *
  * @author Juergen Hoeller
+ * @author Yanming Zhou
  * @since 2.5
  * @see org.springframework.beans.factory.support.DefaultListableBeanFactory
  * @see org.springframework.context.annotation.CommonAnnotationBeanPostProcessor
@@ -132,6 +134,17 @@ public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
 		}
 	}
 
+	@Override
+	@SuppressWarnings("unchecked")
+	public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
+		Object bean = getBean(name);
+		Type requiredType = typeReference.getType();
+		if (!ResolvableType.forType(requiredType).isInstance(bean)) {
+			throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
+		}
+		return (T) bean;
+	}
+
 	@Override
 	public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
 		if (args != null) {


Test patch:
diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java
index e5e024a9313f..c9831eeaec19 100644
--- a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java
+++ b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java
@@ -79,6 +79,7 @@
 import org.springframework.beans.testfixture.beans.factory.DummyFactory;
 import org.springframework.core.MethodParameter;
 import org.springframework.core.Ordered;
+import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.core.ResolvableType;
 import org.springframework.core.annotation.AnnotationAwareOrderComparator;
 import org.springframework.core.annotation.Order;
@@ -1682,6 +1683,29 @@ void getBeanByTypeWithAmbiguity() {
 				lbf.getBean(TestBean.class));
 	}
 
+	@Test
+	void getBeanByNameWithTypeReference() {
+		RootBeanDefinition bd1 = new RootBeanDefinition(StringTemplate.class);
+		RootBeanDefinition bd2 = new RootBeanDefinition(NumberTemplate.class);
+		lbf.registerBeanDefinition("bd1", bd1);
+		lbf.registerBeanDefinition("bd2", bd2);
+
+		Template<String> stringTemplate = lbf.getBean("bd1", new ParameterizedTypeReference<>() {});
+		Template<Number> numberTemplate = lbf.getBean("bd2", new ParameterizedTypeReference<>() {});
+
+		assertThat(stringTemplate).isInstanceOf(StringTemplate.class);
+		assertThat(numberTemplate).isInstanceOf(NumberTemplate.class);
+
+		assertThatExceptionOfType(BeanNotOfRequiredTypeException.class)
+				.isThrownBy(() -> lbf.getBean("bd2", new ParameterizedTypeReference<Template<String>>() {}))
+				.satisfies(ex -> {
+					assertThat(ex.getBeanName()).isEqualTo("bd2");
+					assertThat(ex.getRequiredType()).isEqualTo(Template.class);
+					assertThat(ex.getActualType()).isEqualTo(NumberTemplate.class);
+					assertThat(ex.getGenericRequiredType().toString()).endsWith("Template<java.lang.String>");
+				});
+	}
+
 	@Test
 	void getBeanByTypeWithPrimary() {
 		RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class);
@@ -3872,4 +3896,16 @@ public Class<?> getObjectType() {
 		}
 	}
 
+	private static class Template<T> {
+
+	}
+
+	private static class StringTemplate extends Template<String> {
+
+	}
+
+	private static class NumberTemplate extends Template<Number> {
+
+	}
+
 }
diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt
index fcca43c61f28..750fc095efda 100644
--- a/spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt
+++ b/spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt
@@ -21,6 +21,7 @@ import io.mockk.mockk
 import io.mockk.verify
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.jupiter.api.Test
+import org.springframework.core.ParameterizedTypeReference
 import org.springframework.core.ResolvableType
 
 /**
@@ -53,7 +54,16 @@ class BeanFactoryExtensionsTests {
 	fun `getBean with String and reified type parameters`() {
 		val name = "foo"
 		bf.getBean<Foo>(name)
-		verify { bf.getBean(name, Foo::class.java) }
+		verify { bf.getBean(name, ofType<ParameterizedTypeReference<Foo>>()) }
+	}
+
+	@Test
+	fun `getBean with String and reified generic type parameters`() {
+		val name = "foo"
+		val foo = listOf(Foo())
+		every { bf.getBean(name, ofType<ParameterizedTypeReference<List<Foo>>>()) } returns foo
+		assertThat(bf.getBean<List<Foo>>("foo")).isSameAs(foo)
+		verify { bf.getBean(name, ofType<ParameterizedTypeReference<List<Foo>>>()) }
 	}
 
 	@Test
diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StubWebApplicationContext.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StubWebApplicationContext.java
index 678db294465e..6b1495d7830b 100644
--- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StubWebApplicationContext.java
+++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StubWebApplicationContext.java
@@ -65,6 +65,7 @@
  *
  * @author Rossen Stoyanchev
  * @author Juergen Hoeller
+ * @author Yanming Zhou
  * @since 3.2
  */
 class StubWebApplicationContext implements WebApplicationContext {
@@ -168,6 +169,11 @@ public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
 		return this.beanFactory.getBean(name, requiredType);
 	}
 
+	@Override
+	public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
+		return this.beanFactory.getBean(name, typeReference);
+	}
+
 	@Override
 	public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
 		return this.beanFactory.getBean(name, args);