Expose record-style accessor methods for instance fields as bean properties
Closes gh-24391
This commit is contained in:
parent
1fe2ea5a87
commit
d4192b9d35
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in New Issue