From 7e67da8a267b1d41c3bd33fbd94dcf0b26e816c4 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 15 Feb 2024 16:51:09 +0100 Subject: [PATCH] Support for matching partial generics Closes gh-20727 --- ...ricTypeAwareAutowireCandidateResolver.java | 12 ++++-- .../context/annotation/Spr16179Tests.java | 6 +-- .../springframework/core/ResolvableType.java | 42 +++++++++++++++---- 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java index 3a3c84b11a..31c493740c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -144,12 +144,16 @@ public class GenericTypeAwareAutowireCandidateResolver extends SimpleAutowireCan if (cacheType) { rbd.targetType = targetType; } - if (descriptor.fallbackMatchAllowed() && - (targetType.hasUnresolvableGenerics() || targetType.resolve() == Properties.class)) { + if (descriptor.fallbackMatchAllowed()) { // Fallback matches allow unresolvable generics, e.g. plain HashMap to Map; // and pragmatically also java.util.Properties to any Map (since despite formally being a // Map, java.util.Properties is usually perceived as a Map). - return true; + if (targetType.hasUnresolvableGenerics()) { + return dependencyType.isAssignableFromResolvedPart(targetType); + } + else if (targetType.resolve() == Properties.class) { + return true; + } } // Full check for complex generic type match... return dependencyType.isAssignableFrom(targetType); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr16179Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr16179Tests.java index cb6b1a751a..eb9ce35162 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Spr16179Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr16179Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,8 +32,8 @@ class Spr16179Tests { void repro() { try (AnnotationConfigApplicationContext bf = new AnnotationConfigApplicationContext(AssemblerConfig.class, AssemblerInjection.class)) { assertThat(bf.getBean(AssemblerInjection.class).assembler0).isSameAs(bf.getBean("someAssembler")); - // assertNull(bf.getBean(AssemblerInjection.class).assembler1); TODO: accidental match - // assertNull(bf.getBean(AssemblerInjection.class).assembler2); + assertThat(bf.getBean(AssemblerInjection.class).assembler1).isNull(); + assertThat(bf.getBean(AssemblerInjection.class).assembler2).isSameAs(bf.getBean("pageAssembler")); assertThat(bf.getBean(AssemblerInjection.class).assembler3).isSameAs(bf.getBean("pageAssembler")); assertThat(bf.getBean(AssemblerInjection.class).assembler4).isSameAs(bf.getBean("pageAssembler")); assertThat(bf.getBean(AssemblerInjection.class).assembler5).isSameAs(bf.getBean("pageAssembler")); diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index fc70a6c9eb..037c280d43 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -265,7 +265,7 @@ public class ResolvableType implements Serializable { public boolean isAssignableFrom(Class other) { // As of 6.1: shortcut assignability check for top-level Class references return (this.type instanceof Class clazz ? ClassUtils.isAssignable(clazz, other) : - isAssignableFrom(forClass(other), false, null)); + isAssignableFrom(forClass(other), false, null, false)); } /** @@ -280,10 +280,24 @@ public class ResolvableType implements Serializable { * {@code ResolvableType}; {@code false} otherwise */ public boolean isAssignableFrom(ResolvableType other) { - return isAssignableFrom(other, false, null); + return isAssignableFrom(other, false, null, false); } - private boolean isAssignableFrom(ResolvableType other, boolean strict, @Nullable Map matchedBefore) { + /** + * Determine whether this {@code ResolvableType} is assignable from the + * specified other type, as far as the other type is actually resolvable. + * @param other the type to be checked against (as a {@code ResolvableType}) + * @return {@code true} if the specified other type can be assigned to this + * {@code ResolvableType} as far as it is resolvable; {@code false} otherwise + * @since 6.2 + */ + public boolean isAssignableFromResolvedPart(ResolvableType other) { + return isAssignableFrom(other, false, null, true); + } + + private boolean isAssignableFrom(ResolvableType other, boolean strict, + @Nullable Map matchedBefore, boolean upUntilUnresolvable) { + Assert.notNull(other, "ResolvableType must not be null"); // If we cannot resolve types, we are not assignable @@ -305,7 +319,12 @@ public class ResolvableType implements Serializable { // Deal with array by delegating to the component type if (isArray()) { - return (other.isArray() && getComponentType().isAssignableFrom(other.getComponentType(), true, matchedBefore)); + return (other.isArray() && getComponentType().isAssignableFrom( + other.getComponentType(), true, matchedBefore, upUntilUnresolvable)); + } + + if (upUntilUnresolvable && other.isUnresolvableTypeVariable()) { + return true; } // Deal with wildcard bounds @@ -314,8 +333,15 @@ public class ResolvableType implements Serializable { // In the form X is assignable to if (typeBounds != null) { - return (ourBounds != null && ourBounds.isSameKind(typeBounds) && - ourBounds.isAssignableFrom(typeBounds.getBounds())); + if (ourBounds != null) { + return (ourBounds.isSameKind(typeBounds) && ourBounds.isAssignableFrom(typeBounds.getBounds())); + } + else if (upUntilUnresolvable) { + return typeBounds.isAssignableFrom(this); + } + else { + return false; + } } // In the form is assignable to X... @@ -376,7 +402,7 @@ public class ResolvableType implements Serializable { } matchedBefore.put(this.type, other.type); for (int i = 0; i < ourGenerics.length; i++) { - if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], true, matchedBefore)) { + if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], true, matchedBefore, upUntilUnresolvable)) { return false; } }