Expose record-style accessor methods for instance fields as bean properties

Closes gh-24391
This commit is contained in:
Juergen Hoeller 2020-08-28 18:51:27 +02:00
parent 1fe2ea5a87
commit d4192b9d35
3 changed files with 83 additions and 28 deletions

View File

@ -20,7 +20,10 @@ import java.beans.BeanInfo;
import java.beans.IntrospectionException; import java.beans.IntrospectionException;
import java.beans.Introspector; import java.beans.Introspector;
import java.beans.PropertyDescriptor; import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -92,6 +95,8 @@ public final class CachedIntrospectionResults {
*/ */
public static final String IGNORE_BEANINFO_PROPERTY_NAME = "spring.beaninfo.ignore"; public static final String IGNORE_BEANINFO_PROPERTY_NAME = "spring.beaninfo.ignore";
private static final PropertyDescriptor[] EMPTY_PROPERTY_DESCRIPTOR_ARRAY = {};
private static final boolean shouldIntrospectorIgnoreBeaninfoClasses = private static final boolean shouldIntrospectorIgnoreBeaninfoClasses =
SpringProperties.getFlag(IGNORE_BEANINFO_PROPERTY_NAME); SpringProperties.getFlag(IGNORE_BEANINFO_PROPERTY_NAME);
@ -253,7 +258,7 @@ public final class CachedIntrospectionResults {
private final BeanInfo beanInfo; private final BeanInfo beanInfo;
/** PropertyDescriptor objects keyed by property name String. */ /** PropertyDescriptor objects keyed by property name String. */
private final Map<String, PropertyDescriptor> propertyDescriptorCache; private final Map<String, PropertyDescriptor> propertyDescriptors;
/** TypeDescriptor objects keyed by PropertyDescriptor. */ /** TypeDescriptor objects keyed by PropertyDescriptor. */
private final ConcurrentMap<PropertyDescriptor, TypeDescriptor> typeDescriptorCache; private final ConcurrentMap<PropertyDescriptor, TypeDescriptor> typeDescriptorCache;
@ -274,7 +279,9 @@ public final class CachedIntrospectionResults {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("Caching PropertyDescriptors for class [" + beanClass.getName() + "]"); logger.trace("Caching PropertyDescriptors for class [" + beanClass.getName() + "]");
} }
this.propertyDescriptorCache = new LinkedHashMap<>(); this.propertyDescriptors = new LinkedHashMap<>();
Set<String> readMethodNames = new HashSet<>();
// This call is slow so we do it once. // This call is slow so we do it once.
PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors(); PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors();
@ -291,17 +298,26 @@ public final class CachedIntrospectionResults {
"; editor [" + pd.getPropertyEditorClass().getName() + "]" : "")); "; editor [" + pd.getPropertyEditorClass().getName() + "]" : ""));
} }
pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd); pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
this.propertyDescriptorCache.put(pd.getName(), pd); this.propertyDescriptors.put(pd.getName(), pd);
Method readMethod = pd.getReadMethod();
if (readMethod != null) {
readMethodNames.add(readMethod.getName());
}
} }
// Explicitly check implemented interfaces for setter/getter methods as well, // Explicitly check implemented interfaces for setter/getter methods as well,
// in particular for Java 8 default methods... // in particular for Java 8 default methods...
Class<?> currClass = beanClass; Class<?> currClass = beanClass;
while (currClass != null && currClass != Object.class) { while (currClass != null && currClass != Object.class) {
introspectInterfaces(beanClass, currClass); introspectInterfaces(beanClass, currClass, readMethodNames);
currClass = currClass.getSuperclass(); currClass = currClass.getSuperclass();
} }
// Check for record-style accessors without prefix: e.g. "lastName()"
// - 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<>(); this.typeDescriptorCache = new ConcurrentReferenceHashMap<>();
} }
catch (IntrospectionException ex) { catch (IntrospectionException ex) {
@ -309,24 +325,58 @@ public final class CachedIntrospectionResults {
} }
} }
private void introspectInterfaces(Class<?> beanClass, Class<?> currClass) throws IntrospectionException { private void introspectInterfaces(Class<?> beanClass, Class<?> currClass, Set<String> readMethodNames)
throws IntrospectionException {
for (Class<?> ifc : currClass.getInterfaces()) { for (Class<?> ifc : currClass.getInterfaces()) {
if (!ClassUtils.isJavaLanguageInterface(ifc)) { if (!ClassUtils.isJavaLanguageInterface(ifc)) {
for (PropertyDescriptor pd : getBeanInfo(ifc).getPropertyDescriptors()) { for (PropertyDescriptor pd : getBeanInfo(ifc).getPropertyDescriptors()) {
PropertyDescriptor existingPd = this.propertyDescriptorCache.get(pd.getName()); PropertyDescriptor existingPd = this.propertyDescriptors.get(pd.getName());
if (existingPd == null || if (existingPd == null ||
(existingPd.getReadMethod() == null && pd.getReadMethod() != null)) { (existingPd.getReadMethod() == null && pd.getReadMethod() != null)) {
// GenericTypeAwarePropertyDescriptor leniently resolves a set* write method // GenericTypeAwarePropertyDescriptor leniently resolves a set* write method
// against a declared read method, so we prefer read method descriptors here. // against a declared read method, so we prefer read method descriptors here.
pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd); pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
this.propertyDescriptorCache.put(pd.getName(), pd); this.propertyDescriptors.put(pd.getName(), pd);
Method readMethod = pd.getReadMethod();
if (readMethod != null) {
readMethodNames.add(readMethod.getName());
}
} }
} }
introspectInterfaces(ifc, ifc); introspectInterfaces(ifc, ifc, readMethodNames);
} }
} }
} }
private void introspectPlainAccessors(Class<?> beanClass, Set<String> readMethodNames)
throws IntrospectionException {
for (Method method : beanClass.getMethods()) {
if (!this.propertyDescriptors.containsKey(method.getName()) &&
!readMethodNames.contains((method.getName())) && isPlainAccessor(method)) {
this.propertyDescriptors.put(method.getName(),
new GenericTypeAwarePropertyDescriptor(beanClass, method.getName(), method, null, null));
readMethodNames.add(method.getName());
}
}
}
private boolean isPlainAccessor(Method method) {
if (method.getParameterCount() > 0 || method.getReturnType() == void.class ||
method.getDeclaringClass() == Object.class || Modifier.isStatic(method.getModifiers())) {
return false;
}
try {
// Accessor method referring to instance field of same name?
method.getDeclaringClass().getDeclaredField(method.getName());
return true;
}
catch (Exception ex) {
return false;
}
}
BeanInfo getBeanInfo() { BeanInfo getBeanInfo() {
return this.beanInfo; return this.beanInfo;
@ -338,27 +388,19 @@ public final class CachedIntrospectionResults {
@Nullable @Nullable
PropertyDescriptor getPropertyDescriptor(String name) { PropertyDescriptor getPropertyDescriptor(String name) {
PropertyDescriptor pd = this.propertyDescriptorCache.get(name); PropertyDescriptor pd = this.propertyDescriptors.get(name);
if (pd == null && StringUtils.hasLength(name)) { if (pd == null && StringUtils.hasLength(name)) {
// Same lenient fallback checking as in Property... // Same lenient fallback checking as in Property...
pd = this.propertyDescriptorCache.get(StringUtils.uncapitalize(name)); pd = this.propertyDescriptors.get(StringUtils.uncapitalize(name));
if (pd == null) { if (pd == null) {
pd = this.propertyDescriptorCache.get(StringUtils.capitalize(name)); pd = this.propertyDescriptors.get(StringUtils.capitalize(name));
} }
} }
return (pd == null || pd instanceof GenericTypeAwarePropertyDescriptor ? pd : return pd;
buildGenericTypeAwarePropertyDescriptor(getBeanClass(), pd));
} }
PropertyDescriptor[] getPropertyDescriptors() { PropertyDescriptor[] getPropertyDescriptors() {
PropertyDescriptor[] pds = new PropertyDescriptor[this.propertyDescriptorCache.size()]; return this.propertyDescriptors.values().toArray(EMPTY_PROPERTY_DESCRIPTOR_ARRAY);
int i = 0;
for (PropertyDescriptor pd : this.propertyDescriptorCache.values()) {
pds[i] = (pd instanceof GenericTypeAwarePropertyDescriptor ? pd :
buildGenericTypeAwarePropertyDescriptor(getBeanClass(), pd));
i++;
}
return pds;
} }
private PropertyDescriptor buildGenericTypeAwarePropertyDescriptor(Class<?> beanClass, PropertyDescriptor pd) { private PropertyDescriptor buildGenericTypeAwarePropertyDescriptor(Class<?> beanClass, PropertyDescriptor pd) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -60,12 +60,13 @@ final class GenericTypeAwarePropertyDescriptor extends PropertyDescriptor {
@Nullable @Nullable
private Class<?> propertyType; private Class<?> propertyType;
@Nullable
private final Class<?> propertyEditorClass; private final Class<?> propertyEditorClass;
public GenericTypeAwarePropertyDescriptor(Class<?> beanClass, String propertyName, public GenericTypeAwarePropertyDescriptor(Class<?> beanClass, String propertyName,
@Nullable Method readMethod, @Nullable Method writeMethod, Class<?> propertyEditorClass) @Nullable Method readMethod, @Nullable Method writeMethod,
throws IntrospectionException { @Nullable Class<?> propertyEditorClass) throws IntrospectionException {
super(propertyName, null, null); super(propertyName, null, null);
this.beanClass = beanClass; this.beanClass = beanClass;
@ -156,6 +157,7 @@ final class GenericTypeAwarePropertyDescriptor extends PropertyDescriptor {
} }
@Override @Override
@Nullable
public Class<?> getPropertyEditorClass() { public Class<?> getPropertyEditorClass() {
return this.propertyEditorClass; return this.propertyEditorClass;
} }

View File

@ -3834,11 +3834,11 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
public static class DataClass { public static class DataClass {
@NotNull @NotNull
public final String param1; private final String param1;
public final boolean param2; private final boolean param2;
public int param3; private int param3;
@ConstructorProperties({"param1", "param2", "optionalParam"}) @ConstructorProperties({"param1", "param2", "optionalParam"})
public DataClass(String param1, boolean p2, Optional<Integer> optionalParam) { public DataClass(String param1, boolean p2, Optional<Integer> optionalParam) {
@ -3848,9 +3848,21 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
optionalParam.ifPresent(integer -> this.param3 = integer); optionalParam.ifPresent(integer -> this.param3 = integer);
} }
public String param1() {
return param1;
}
public boolean param2() {
return param2;
}
public void setParam3(int param3) { public void setParam3(int param3) {
this.param3 = param3; this.param3 = param3;
} }
public int getParam3() {
return param3;
}
} }
@RestController @RestController
@ -3876,7 +3888,6 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
@InitBinder @InitBinder
public void initBinder(WebDataBinder binder) { public void initBinder(WebDataBinder binder) {
binder.initDirectFieldAccess();
binder.setConversionService(new DefaultFormattingConversionService()); binder.setConversionService(new DefaultFormattingConversionService());
LocalValidatorFactoryBean vf = new LocalValidatorFactoryBean(); LocalValidatorFactoryBean vf = new LocalValidatorFactoryBean();
vf.afterPropertiesSet(); vf.afterPropertiesSet();