diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java index ec2a7c70c0..70081a97b6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -799,7 +799,7 @@ public abstract class BeanUtils { if (sourcePd != null) { Method readMethod = sourcePd.getReadMethod(); if (readMethod != null) { - if (isAssignable(writeMethod, readMethod)) { + if (isAssignable(writeMethod, readMethod, sourcePd, targetPd)) { try { ReflectionUtils.makeAccessible(readMethod); Object value = readMethod.invoke(source); @@ -817,7 +817,9 @@ public abstract class BeanUtils { } } - private static boolean isAssignable(Method writeMethod, Method readMethod) { + private static boolean isAssignable(Method writeMethod, Method readMethod, + PropertyDescriptor sourcePd, PropertyDescriptor targetPd) { + Type paramType = writeMethod.getGenericParameterTypes()[0]; if (paramType instanceof Class clazz) { return ClassUtils.isAssignable(clazz, readMethod.getReturnType()); @@ -826,8 +828,8 @@ public abstract class BeanUtils { return true; } else { - ResolvableType sourceType = ResolvableType.forMethodReturnType(readMethod); - ResolvableType targetType = ResolvableType.forMethodParameter(writeMethod, 0); + ResolvableType sourceType = ((GenericTypeAwarePropertyDescriptor) sourcePd).getReadMethodType(); + ResolvableType targetType = ((GenericTypeAwarePropertyDescriptor) targetPd).getWriteMethodType(); // Ignore generic types in assignable check if either ResolvableType has unresolvable generics. return (sourceType.hasUnresolvableGenerics() || targetType.hasUnresolvableGenerics() ? ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType()) : diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java index f7744476a7..330d20c471 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java @@ -20,9 +20,9 @@ import java.beans.PropertyDescriptor; import java.lang.reflect.Method; import org.springframework.core.ResolvableType; -import org.springframework.core.convert.Property; import org.springframework.core.convert.TypeDescriptor; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; /** @@ -183,23 +183,15 @@ public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements throw new InvalidPropertyException(getRootClass(), getNestedPath() + propertyName, "No property '" + propertyName + "' found"); } - TypeDescriptor td = cachedIntrospectionResults.getTypeDescriptor(pd); - if (td == null) { - td = cachedIntrospectionResults.addTypeDescriptor(pd, new TypeDescriptor(property(pd))); - } + TypeDescriptor td = ((GenericTypeAwarePropertyDescriptor) pd).getTypeDescriptor(); return convertForProperty(propertyName, null, value, td); } - private Property property(PropertyDescriptor pd) { - GenericTypeAwarePropertyDescriptor gpd = (GenericTypeAwarePropertyDescriptor) pd; - return new Property(gpd.getBeanClass(), gpd.getReadMethod(), gpd.getWriteMethod(), gpd.getName()); - } - @Override @Nullable protected BeanPropertyHandler getLocalPropertyHandler(String propertyName) { PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(propertyName); - return (pd != null ? new BeanPropertyHandler(pd) : null); + return (pd != null ? new BeanPropertyHandler((GenericTypeAwarePropertyDescriptor) pd) : null); } @Override @@ -234,58 +226,55 @@ public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements private class BeanPropertyHandler extends PropertyHandler { - private final PropertyDescriptor pd; + private final GenericTypeAwarePropertyDescriptor pd; - private final TypeDescriptor typeDescriptor; - - public BeanPropertyHandler(PropertyDescriptor pd) { + public BeanPropertyHandler(GenericTypeAwarePropertyDescriptor pd) { super(pd.getPropertyType(), pd.getReadMethod() != null, pd.getWriteMethod() != null); this.pd = pd; - this.typeDescriptor = new TypeDescriptor(property(pd)); } @Override public TypeDescriptor toTypeDescriptor() { - return this.typeDescriptor; + return this.pd.getTypeDescriptor(); } @Override public ResolvableType getResolvableType() { - return this.typeDescriptor.getResolvableType(); + return this.pd.getReadMethodType(); } @Override public TypeDescriptor getMapValueType(int nestingLevel) { return new TypeDescriptor( - this.typeDescriptor.getResolvableType().getNested(nestingLevel).asMap().getGeneric(1), - null, this.typeDescriptor.getAnnotations()); + this.pd.getReadMethodType().getNested(nestingLevel).asMap().getGeneric(1), + null, this.pd.getTypeDescriptor().getAnnotations()); } @Override public TypeDescriptor getCollectionType(int nestingLevel) { return new TypeDescriptor( - this.typeDescriptor.getResolvableType().getNested(nestingLevel).asCollection().getGeneric(), - null, this.typeDescriptor.getAnnotations()); + this.pd.getReadMethodType().getNested(nestingLevel).asCollection().getGeneric(), + null, this.pd.getTypeDescriptor().getAnnotations()); } @Override @Nullable public TypeDescriptor nested(int level) { - return TypeDescriptor.nested(property(this.pd), level); + return this.pd.getTypeDescriptor().nested(level); } @Override @Nullable public Object getValue() throws Exception { Method readMethod = this.pd.getReadMethod(); + Assert.state(readMethod != null, "No read method available"); ReflectionUtils.makeAccessible(readMethod); return readMethod.invoke(getWrappedInstance(), (Object[]) null); } @Override public void setValue(@Nullable Object value) throws Exception { - Method writeMethod = (this.pd instanceof GenericTypeAwarePropertyDescriptor typeAwarePd ? - typeAwarePd.getWriteMethodForActualAccess() : this.pd.getWriteMethod()); + Method writeMethod = this.pd.getWriteMethodForActualAccess(); ReflectionUtils.makeAccessible(writeMethod); writeMethod.invoke(getWrappedInstance(), value); } diff --git a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java index e7b8d426e9..96d9f94548 100644 --- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -35,7 +35,6 @@ import java.util.concurrent.ConcurrentMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; @@ -235,9 +234,6 @@ public final class CachedIntrospectionResults { /** PropertyDescriptor objects keyed by property name String. */ private final Map propertyDescriptors; - /** TypeDescriptor objects keyed by PropertyDescriptor. */ - private final ConcurrentMap typeDescriptorCache; - /** * Create a new CachedIntrospectionResults instance for the given class. @@ -300,8 +296,6 @@ public final class CachedIntrospectionResults { // - accessor method directly referring to instance field of same name // - same convention for component accessors of Java 15 record classes introspectPlainAccessors(beanClass, readMethodNames); - - this.typeDescriptorCache = new ConcurrentReferenceHashMap<>(); } catch (IntrospectionException ex) { throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", ex); @@ -410,14 +404,4 @@ public final class CachedIntrospectionResults { } } - TypeDescriptor addTypeDescriptor(PropertyDescriptor pd, TypeDescriptor td) { - TypeDescriptor existing = this.typeDescriptorCache.putIfAbsent(pd, td); - return (existing != null ? existing : td); - } - - @Nullable - TypeDescriptor getTypeDescriptor(PropertyDescriptor pd) { - return this.typeDescriptorCache.get(pd); - } - } diff --git a/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java index b7d4c18a43..7922f0864d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java @@ -26,8 +26,10 @@ import java.util.Set; import org.apache.commons.logging.LogFactory; import org.springframework.core.BridgeMethodResolver; -import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.Property; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -57,6 +59,15 @@ final class GenericTypeAwarePropertyDescriptor extends PropertyDescriptor { @Nullable private MethodParameter writeMethodParameter; + @Nullable + private volatile ResolvableType writeMethodType; + + @Nullable + private ResolvableType readMethodType; + + @Nullable + private volatile TypeDescriptor typeDescriptor; + @Nullable private Class propertyType; @@ -107,7 +118,8 @@ final class GenericTypeAwarePropertyDescriptor extends PropertyDescriptor { } if (this.readMethod != null) { - this.propertyType = GenericTypeResolver.resolveReturnType(this.readMethod, this.beanClass); + this.readMethodType = ResolvableType.forMethodReturnType(this.readMethod, this.beanClass); + this.propertyType = this.readMethodType.resolve(this.readMethod.getReturnType()); } else if (this.writeMethodParameter != null) { this.propertyType = this.writeMethodParameter.getParameterType(); @@ -150,6 +162,30 @@ final class GenericTypeAwarePropertyDescriptor extends PropertyDescriptor { return this.writeMethodParameter; } + public ResolvableType getWriteMethodType() { + ResolvableType writeMethodType = this.writeMethodType; + if (writeMethodType == null) { + writeMethodType = ResolvableType.forMethodParameter(getWriteMethodParameter()); + this.writeMethodType = writeMethodType; + } + return writeMethodType; + } + + public ResolvableType getReadMethodType() { + Assert.state(this.readMethodType != null, "No read method available"); + return this.readMethodType; + } + + public TypeDescriptor getTypeDescriptor() { + TypeDescriptor typeDescriptor = this.typeDescriptor; + if (typeDescriptor == null) { + Property property = new Property(getBeanClass(), getReadMethod(), getWriteMethod(), getName()); + typeDescriptor = new TypeDescriptor(property); + this.typeDescriptor = typeDescriptor; + } + return typeDescriptor; + } + @Override @Nullable public Class getPropertyType() { diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index f02d0ea0a2..c5cb84c5c8 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -170,6 +170,33 @@ public class TypeDescriptor implements Serializable { return this.resolvableType.getSource(); } + + /** + * Create a type descriptor for a nested type declared within this descriptor. + * @param nestingLevel the nesting level of the collection/array element or + * map key/value declaration within the property + * @return the nested type descriptor at the specified nesting level, or + * {@code null} if it could not be obtained + * @since 6.1 + */ + @Nullable + public TypeDescriptor nested(int nestingLevel) { + ResolvableType nested = this.resolvableType; + for (int i = 0; i < nestingLevel; i++) { + if (Object.class == nested.getType()) { + // Could be a collection type but we don't know about its element type, + // so let's just assume there is an element type of type Object... + } + else { + nested = nested.getNested(2); + } + } + if (nested == ResolvableType.NONE) { + return null; + } + return getRelatedIfResolvable(nested); + } + /** * Narrows this {@link TypeDescriptor} by setting its type to the class of the * provided value. @@ -335,9 +362,9 @@ public class TypeDescriptor implements Serializable { return new TypeDescriptor(getResolvableType().getComponentType(), null, getAnnotations()); } if (Stream.class.isAssignableFrom(getType())) { - return getRelatedIfResolvable(this, getResolvableType().as(Stream.class).getGeneric(0)); + return getRelatedIfResolvable(getResolvableType().as(Stream.class).getGeneric(0)); } - return getRelatedIfResolvable(this, getResolvableType().asCollection().getGeneric(0)); + return getRelatedIfResolvable(getResolvableType().asCollection().getGeneric(0)); } /** @@ -380,7 +407,7 @@ public class TypeDescriptor implements Serializable { @Nullable public TypeDescriptor getMapKeyTypeDescriptor() { Assert.state(isMap(), "Not a [java.util.Map]"); - return getRelatedIfResolvable(this, getResolvableType().asMap().getGeneric(0)); + return getRelatedIfResolvable(getResolvableType().asMap().getGeneric(0)); } /** @@ -417,7 +444,7 @@ public class TypeDescriptor implements Serializable { @Nullable public TypeDescriptor getMapValueTypeDescriptor() { Assert.state(isMap(), "Not a [java.util.Map]"); - return getRelatedIfResolvable(this, getResolvableType().asMap().getGeneric(1)); + return getRelatedIfResolvable(getResolvableType().asMap().getGeneric(1)); } /** @@ -442,6 +469,14 @@ public class TypeDescriptor implements Serializable { return narrow(mapValue, getMapValueTypeDescriptor()); } + @Nullable + private TypeDescriptor getRelatedIfResolvable(ResolvableType type) { + if (type.resolve() == null) { + return null; + } + return new TypeDescriptor(type, null, getAnnotations()); + } + @Nullable private TypeDescriptor narrow(@Nullable Object value, @Nullable TypeDescriptor typeDescriptor) { if (typeDescriptor != null) { @@ -645,7 +680,7 @@ public class TypeDescriptor implements Serializable { throw new IllegalArgumentException("MethodParameter nesting level must be 1: " + "use the nestingLevel parameter to specify the desired nestingLevel for nested type traversal"); } - return nested(new TypeDescriptor(methodParameter), nestingLevel); + return new TypeDescriptor(methodParameter).nested(nestingLevel); } /** @@ -671,7 +706,7 @@ public class TypeDescriptor implements Serializable { */ @Nullable public static TypeDescriptor nested(Field field, int nestingLevel) { - return nested(new TypeDescriptor(field), nestingLevel); + return new TypeDescriptor(field).nested(nestingLevel); } /** @@ -697,33 +732,7 @@ public class TypeDescriptor implements Serializable { */ @Nullable public static TypeDescriptor nested(Property property, int nestingLevel) { - return nested(new TypeDescriptor(property), nestingLevel); - } - - @Nullable - private static TypeDescriptor nested(TypeDescriptor typeDescriptor, int nestingLevel) { - ResolvableType nested = typeDescriptor.resolvableType; - for (int i = 0; i < nestingLevel; i++) { - if (Object.class == nested.getType()) { - // Could be a collection type but we don't know about its element type, - // so let's just assume there is an element type of type Object... - } - else { - nested = nested.getNested(2); - } - } - if (nested == ResolvableType.NONE) { - return null; - } - return getRelatedIfResolvable(typeDescriptor, nested); - } - - @Nullable - private static TypeDescriptor getRelatedIfResolvable(TypeDescriptor source, ResolvableType type) { - if (type.resolve() == null) { - return null; - } - return new TypeDescriptor(type, null, source.getAnnotations()); + return new TypeDescriptor(property).nested(nestingLevel); }