From 0eba6f0da3fbd879326226c9310791ceb329bb82 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Tue, 1 Apr 2025 12:06:14 +0800 Subject: [PATCH] Add typesafe method to get generic bean by name with type reference Fix GH-34687 Signed-off-by: Yanming Zhou --- .../beans/factory/BeanFactory.java | 24 +++++++++++++ .../BeanNotOfRequiredTypeException.java | 32 ++++++++++++++--- .../factory/support/AbstractBeanFactory.java | 13 +++++++ .../support/StaticListableBeanFactory.java | 13 +++++++ .../beans/factory/BeanFactoryExtensions.kt | 5 +-- .../DefaultListableBeanFactoryTests.java | 36 +++++++++++++++++++ .../factory/BeanFactoryExtensionsTests.kt | 12 ++++++- .../support/AbstractApplicationContext.java | 7 ++++ .../jndi/support/SimpleJndiBeanFactory.java | 13 +++++++ .../setup/StubWebApplicationContext.java | 6 ++++ 10 files changed, 154 insertions(+), 7 deletions(-) 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 ad47efb2b2a..b7ffe33ede7 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 @@ import org.springframework.core.ResolvableType; * @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 getBean(String name, Class requiredType) throws BeansException; + /** + * Return an instance, which may be shared or independent, of the specified bean. + *

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)}. + *

Translates aliases back to the corresponding canonical bean name. + *

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 getBean(String name, ParameterizedTypeReference typeReference) throws BeansException; + /** * Return an instance, which may be shared or independent, of the specified bean. *

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 bbc504098aa..e574aec05af 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 @@ import org.springframework.util.ClassUtils; * * @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 class BeanNotOfRequiredTypeException extends BeansException { * 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 54a5921b546..4ce57a8a896 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.Scope; 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 abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp return doGetBean(name, requiredType, null, false); } + @Override + @SuppressWarnings("unchecked") + public T getBean(String name, ParameterizedTypeReference 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 87fbc1a2751..7a1570c660d 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 @@ import org.springframework.util.StringUtils; * @author Rod Johnson * @author Juergen Hoeller * @author Sam Brannen + * @author Yanming Zhou * @since 06.01.2003 * @see DefaultListableBeanFactory */ @@ -149,6 +151,17 @@ public class StaticListableBeanFactory implements ListableBeanFactory { return (T) bean; } + @Override + @SuppressWarnings("unchecked") + public T getBean(String name, ParameterizedTypeReference 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 608b11a50dc..3bd4d67bb2e 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 BeanFactory.getBean(): T = @@ -31,14 +32,14 @@ inline fun BeanFactory.getBean(): T = /** * Extension for [BeanFactory.getBean] providing a `getBean("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) * @author Sebastien Deleuze * @since 5.0 */ inline fun BeanFactory.getBean(name: String): T = - getBean(name, T::class.java) + getBean(name, (object : ParameterizedTypeReference() {})) /** * Extension for [BeanFactory.getBean] providing a `getBean(arg1, arg2)` variant. 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 e5e024a9313..c9831eeaec1 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.TestBean; 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 @@ class DefaultListableBeanFactoryTests { 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 stringTemplate = lbf.getBean("bd1", new ParameterizedTypeReference<>() {}); + Template 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>() {})) + .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"); + }); + } + @Test void getBeanByTypeWithPrimary() { RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); @@ -3872,4 +3896,16 @@ class DefaultListableBeanFactoryTests { } } + private static class Template { + + } + + private static class StringTemplate extends Template { + + } + + private static class NumberTemplate extends Template { + + } + } 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 fcca43c61f2..750fc095efd 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(name) - verify { bf.getBean(name, Foo::class.java) } + verify { bf.getBean(name, ofType>()) } + } + + @Test + fun `getBean with String and reified generic type parameters`() { + val name = "foo" + val foo = listOf(Foo()) + every { bf.getBean(name, ofType>>()) } returns foo + assertThat(bf.getBean>("foo")).isSameAs(foo) + verify { bf.getBean(name, ofType>>()) } } @Test 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 dc3da9309eb..77628361794 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 @@ import org.springframework.util.ReflectionUtils; * @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 abstract class AbstractApplicationContext extends DefaultResourceLoader return getBeanFactory().getBean(name, requiredType); } + @Override + public T getBean(String name, ParameterizedTypeReference 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 cd6f5feb498..224df23e114 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 @@ import org.springframework.jndi.TypeMismatchNamingException; * 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 class SimpleJndiBeanFactory extends JndiLocatorSupport implements BeanFac } } + @Override + @SuppressWarnings("unchecked") + public T getBean(String name, ParameterizedTypeReference 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) { 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 678db294465..6b1495d7830 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 @@ import org.springframework.web.context.support.ServletContextResourcePatternReso * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Yanming Zhou * @since 3.2 */ class StubWebApplicationContext implements WebApplicationContext { @@ -168,6 +169,11 @@ class StubWebApplicationContext implements WebApplicationContext { return this.beanFactory.getBean(name, requiredType); } + @Override + public T getBean(String name, ParameterizedTypeReference 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);