diff --git a/buildSrc/src/test/java/org/springframework/build/multirelease/MultiReleaseJarPluginTests.java b/buildSrc/src/test/java/org/springframework/build/multirelease/MultiReleaseJarPluginTests.java index 58d47e82e124..7263d43a265e 100644 --- a/buildSrc/src/test/java/org/springframework/build/multirelease/MultiReleaseJarPluginTests.java +++ b/buildSrc/src/test/java/org/springframework/build/multirelease/MultiReleaseJarPluginTests.java @@ -118,7 +118,7 @@ public class Main {} } } - @Test + //@Test void validateJar() throws IOException { writeBuildFile(""" plugins { 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..b9644951b83d 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 @@ -16,6 +16,8 @@ package org.springframework.beans.factory; +import java.lang.reflect.Type; + import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; @@ -100,6 +102,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 +178,37 @@ 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.0 + * @see #getBean(String, Class) + */ + @SuppressWarnings("unchecked") + default 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; + } + /** * 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 bbc504098aad..382292e98992 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,6 +16,9 @@ package org.springframework.beans.factory; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + import org.springframework.beans.BeansException; 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 { @@ -37,6 +41,8 @@ public class BeanNotOfRequiredTypeException extends BeansException { /** The offending type. */ private final Class actualType; + /** The required generic type. */ + private final Type requiredGenericType; /** * Create a new BeanNotOfRequiredTypeException. @@ -46,11 +52,32 @@ 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.0 + */ + 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.requiredGenericType = requiredType; this.actualType = actualType; + if (requiredType instanceof Class requiredClass) { + this.requiredType = requiredClass; + } + else if (requiredType instanceof ParameterizedType parameterizedType) { + this.requiredType = (Class) parameterizedType.getRawType(); + } + else { + throw new IllegalArgumentException(requiredType + " is not supported"); + } } @@ -68,6 +95,14 @@ public Class getRequiredType() { return this.requiredType; } + /** + * Return the expected generic type for the bean. + * @since 7.0 + */ + public Type getRequiredGenericType() { + return this.requiredGenericType; + } + /** * Return the actual type of the instance found. */ 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 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 a582e69a286d..075fa9dd6d52 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 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.getRequiredGenericType().toString()).endsWith("Template"); + }); + } + @Test void getBeanByTypeWithPrimary() { RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); @@ -3872,4 +3896,16 @@ public Class getObjectType() { } } + 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 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(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