From d4192b9d355a2d4b0be959e076c255d8b5f01bcf Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 28 Aug 2020 18:51:27 +0200 Subject: [PATCH] Expose record-style accessor methods for instance fields as bean properties Closes gh-24391 --- .../beans/CachedIntrospectionResults.java | 84 ++++++++++++++----- .../GenericTypeAwarePropertyDescriptor.java | 8 +- ...nnotationControllerHandlerMethodTests.java | 19 ++++- 3 files changed, 83 insertions(+), 28 deletions(-) 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 7d94fe28bd..7b7a67d91c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java @@ -20,7 +20,10 @@ import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -92,6 +95,8 @@ public final class CachedIntrospectionResults { */ public static final String IGNORE_BEANINFO_PROPERTY_NAME = "spring.beaninfo.ignore"; + private static final PropertyDescriptor[] EMPTY_PROPERTY_DESCRIPTOR_ARRAY = {}; + private static final boolean shouldIntrospectorIgnoreBeaninfoClasses = SpringProperties.getFlag(IGNORE_BEANINFO_PROPERTY_NAME); @@ -253,7 +258,7 @@ public final class CachedIntrospectionResults { private final BeanInfo beanInfo; /** PropertyDescriptor objects keyed by property name String. */ - private final Map propertyDescriptorCache; + private final Map propertyDescriptors; /** TypeDescriptor objects keyed by PropertyDescriptor. */ private final ConcurrentMap typeDescriptorCache; @@ -274,7 +279,9 @@ public final class CachedIntrospectionResults { if (logger.isTraceEnabled()) { logger.trace("Caching PropertyDescriptors for class [" + beanClass.getName() + "]"); } - this.propertyDescriptorCache = new LinkedHashMap<>(); + this.propertyDescriptors = new LinkedHashMap<>(); + + Set readMethodNames = new HashSet<>(); // This call is slow so we do it once. PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors(); @@ -291,17 +298,26 @@ public final class CachedIntrospectionResults { "; editor [" + pd.getPropertyEditorClass().getName() + "]" : "")); } 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, // in particular for Java 8 default methods... Class currClass = beanClass; while (currClass != null && currClass != Object.class) { - introspectInterfaces(beanClass, currClass); + introspectInterfaces(beanClass, currClass, readMethodNames); 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<>(); } 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 readMethodNames) + throws IntrospectionException { + for (Class ifc : currClass.getInterfaces()) { if (!ClassUtils.isJavaLanguageInterface(ifc)) { for (PropertyDescriptor pd : getBeanInfo(ifc).getPropertyDescriptors()) { - PropertyDescriptor existingPd = this.propertyDescriptorCache.get(pd.getName()); + PropertyDescriptor existingPd = this.propertyDescriptors.get(pd.getName()); if (existingPd == null || (existingPd.getReadMethod() == null && pd.getReadMethod() != null)) { // GenericTypeAwarePropertyDescriptor leniently resolves a set* write method // against a declared read method, so we prefer read method descriptors here. 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 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() { return this.beanInfo; @@ -338,27 +388,19 @@ public final class CachedIntrospectionResults { @Nullable PropertyDescriptor getPropertyDescriptor(String name) { - PropertyDescriptor pd = this.propertyDescriptorCache.get(name); + PropertyDescriptor pd = this.propertyDescriptors.get(name); if (pd == null && StringUtils.hasLength(name)) { // Same lenient fallback checking as in Property... - pd = this.propertyDescriptorCache.get(StringUtils.uncapitalize(name)); + pd = this.propertyDescriptors.get(StringUtils.uncapitalize(name)); if (pd == null) { - pd = this.propertyDescriptorCache.get(StringUtils.capitalize(name)); + pd = this.propertyDescriptors.get(StringUtils.capitalize(name)); } } - return (pd == null || pd instanceof GenericTypeAwarePropertyDescriptor ? pd : - buildGenericTypeAwarePropertyDescriptor(getBeanClass(), pd)); + return pd; } PropertyDescriptor[] getPropertyDescriptors() { - PropertyDescriptor[] pds = new PropertyDescriptor[this.propertyDescriptorCache.size()]; - int i = 0; - for (PropertyDescriptor pd : this.propertyDescriptorCache.values()) { - pds[i] = (pd instanceof GenericTypeAwarePropertyDescriptor ? pd : - buildGenericTypeAwarePropertyDescriptor(getBeanClass(), pd)); - i++; - } - return pds; + return this.propertyDescriptors.values().toArray(EMPTY_PROPERTY_DESCRIPTOR_ARRAY); } private PropertyDescriptor buildGenericTypeAwarePropertyDescriptor(Class beanClass, PropertyDescriptor 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 edcb0cd794..603f5aae15 100644 --- a/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java @@ -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"); * you may not use this file except in compliance with the License. @@ -60,12 +60,13 @@ final class GenericTypeAwarePropertyDescriptor extends PropertyDescriptor { @Nullable private Class propertyType; + @Nullable private final Class propertyEditorClass; public GenericTypeAwarePropertyDescriptor(Class beanClass, String propertyName, - @Nullable Method readMethod, @Nullable Method writeMethod, Class propertyEditorClass) - throws IntrospectionException { + @Nullable Method readMethod, @Nullable Method writeMethod, + @Nullable Class propertyEditorClass) throws IntrospectionException { super(propertyName, null, null); this.beanClass = beanClass; @@ -156,6 +157,7 @@ final class GenericTypeAwarePropertyDescriptor extends PropertyDescriptor { } @Override + @Nullable public Class getPropertyEditorClass() { return this.propertyEditorClass; } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java index 579ef85666..b4583b1805 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java @@ -3834,11 +3834,11 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl public static class DataClass { @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"}) public DataClass(String param1, boolean p2, Optional optionalParam) { @@ -3848,9 +3848,21 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl optionalParam.ifPresent(integer -> this.param3 = integer); } + public String param1() { + return param1; + } + + public boolean param2() { + return param2; + } + public void setParam3(int param3) { this.param3 = param3; } + + public int getParam3() { + return param3; + } } @RestController @@ -3876,7 +3888,6 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl @InitBinder public void initBinder(WebDataBinder binder) { - binder.initDirectFieldAccess(); binder.setConversionService(new DefaultFormattingConversionService()); LocalValidatorFactoryBean vf = new LocalValidatorFactoryBean(); vf.afterPropertiesSet();