Perform basic property determination without java.beans.Introspector

Closes gh-29320
This commit is contained in:
Juergen Hoeller 2022-10-13 19:02:45 +02:00
parent 113db2fb2f
commit bba313c2f5
12 changed files with 276 additions and 199 deletions

View File

@ -18,12 +18,13 @@ package org.springframework.beans;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.SimpleBeanInfo;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.security.ProtectionDomain;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
@ -36,7 +37,6 @@ import java.util.concurrent.ConcurrentMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.SpringProperties;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.lang.Nullable;
@ -60,14 +60,15 @@ import org.springframework.util.StringUtils;
* <p>Note that for caching to work effectively, some preconditions need to be met:
* Prefer an arrangement where the Spring jars live in the same ClassLoader as the
* application classes, which allows for clean caching along with the application's
* lifecycle in any case. For a web application, consider declaring a local
* {@link org.springframework.web.util.IntrospectorCleanupListener} in {@code web.xml}
* in case of a multi-ClassLoader layout, which will allow for effective caching as well.
* lifecycle in any case.
*
* <p>In case of a non-clean ClassLoader arrangement without a cleanup listener having
* been set up, this class will fall back to a weak-reference-based caching model that
* recreates much-requested entries every time the garbage collector removed them. In
* such a scenario, consider the {@link #IGNORE_BEANINFO_PROPERTY_NAME} system property.
* <p>As of 6.0, Spring's default introspection discovers basic JavaBeans properties
* through an efficient method reflection pass. For full JavaBeans introspection
* including indexed properties and all JDK-supported customizers, configure a
* {@code META-INF/spring.factories} file with the following content:
* {@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.StandardBeanInfoFactory}
* For Spring 5.3 compatible extended introspection including non-void setter methods:
* {@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.ExtendedBeanInfoFactory}
*
* @author Rod Johnson
* @author Juergen Hoeller
@ -78,32 +79,9 @@ import org.springframework.util.StringUtils;
*/
public final class CachedIntrospectionResults {
/**
* System property that instructs Spring to use the {@link Introspector#IGNORE_ALL_BEANINFO}
* mode when calling the JavaBeans {@link Introspector}: "spring.beaninfo.ignore", with a
* value of "true" skipping the search for {@code BeanInfo} classes (typically for scenarios
* where no such classes are being defined for beans in the application in the first place).
* <p>The default is "false", considering all {@code BeanInfo} metadata classes, like for
* standard {@link Introspector#getBeanInfo(Class)} calls. Consider switching this flag to
* "true" if you experience repeated ClassLoader access for non-existing {@code BeanInfo}
* classes, in case such access is expensive on startup or on lazy loading.
* <p>Note that such an effect may also indicate a scenario where caching doesn't work
* effectively: Prefer an arrangement where the Spring jars live in the same ClassLoader
* as the application classes, which allows for clean caching along with the application's
* lifecycle in any case. For a web application, consider declaring a local
* {@link org.springframework.web.util.IntrospectorCleanupListener} in {@code web.xml}
* in case of a multi-ClassLoader layout, which will allow for effective caching as well.
* @see Introspector#getBeanInfo(Class, int)
*/
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);
/** Stores the BeanInfoFactory instances. */
private static final List<BeanInfoFactory> beanInfoFactories = SpringFactoriesLoader.loadFactories(
BeanInfoFactory.class, CachedIntrospectionResults.class.getClassLoader());
@ -241,7 +219,7 @@ public final class CachedIntrospectionResults {
* Retrieve a {@link BeanInfo} descriptor for the given target class.
* @param beanClass the target class to introspect
* @return the resulting {@code BeanInfo} descriptor (never {@code null})
* @throws IntrospectionException from the underlying {@link Introspector}
* @throws IntrospectionException from introspecting the given bean class
*/
private static BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
for (BeanInfoFactory beanInfoFactory : beanInfoFactories) {
@ -250,9 +228,14 @@ public final class CachedIntrospectionResults {
return beanInfo;
}
}
return (shouldIntrospectorIgnoreBeaninfoClasses ?
Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) :
Introspector.getBeanInfo(beanClass));
Collection<PropertyDescriptor> pds = PropertyDescriptorUtils.determineBasicProperties(beanClass);
return new SimpleBeanInfo() {
@Override
public PropertyDescriptor[] getPropertyDescriptors() {
return pds.toArray(EMPTY_PROPERTY_DESCRIPTOR_ARRAY);
}
};
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2022 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.
@ -18,34 +18,35 @@ package org.springframework.beans;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.lang.reflect.Method;
import org.springframework.core.Ordered;
import org.springframework.lang.Nullable;
import org.springframework.lang.NonNull;
/**
* {@link BeanInfoFactory} implementation that evaluates whether bean classes have
* "non-standard" JavaBeans setter methods and are thus candidates for introspection
* by Spring's (package-visible) {@code ExtendedBeanInfo} implementation.
* Extension of {@link StandardBeanInfoFactory} that supports "non-standard"
* JavaBeans setter methods through introspection by Spring's
* (package-visible) {@code ExtendedBeanInfo} implementation.
*
* <p>To be configured via a {@code META-INF/spring.factories} file with the following content:
* {@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.ExtendedBeanInfoFactory}
*
* <p>Ordered at {@link Ordered#LOWEST_PRECEDENCE} to allow other user-defined
* {@link BeanInfoFactory} types to take precedence.
*
* @author Chris Beams
* @author Juergen Hoeller
* @since 3.2
* @see BeanInfoFactory
* @see StandardBeanInfoFactory
* @see CachedIntrospectionResults
*/
public class ExtendedBeanInfoFactory implements BeanInfoFactory, Ordered {
public class ExtendedBeanInfoFactory extends StandardBeanInfoFactory {
/**
* Return an {@link ExtendedBeanInfo} for the given bean class, if applicable.
*/
@Override
@Nullable
@NonNull
public BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
return (supports(beanClass) ? new ExtendedBeanInfo(Introspector.getBeanInfo(beanClass)) : null);
BeanInfo beanInfo = super.getBeanInfo(beanClass);
return (supports(beanClass) ? new ExtendedBeanInfo(beanInfo) : beanInfo);
}
/**
@ -61,9 +62,4 @@ public class ExtendedBeanInfoFactory implements BeanInfoFactory, Ordered {
return false;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 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.
@ -19,10 +19,14 @@ package org.springframework.beans;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Map;
import java.util.TreeMap;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Common delegate methods for Spring's internal {@link PropertyDescriptor} implementations.
@ -32,6 +36,82 @@ import org.springframework.util.ObjectUtils;
*/
abstract class PropertyDescriptorUtils {
/**
* Simple introspection algorithm for basic set/get/is accessor methods,
* building corresponding JavaBeans property descriptors for them.
* <p>This just supports the basic JavaBeans conventions, without indexed
* properties or any customizers, and without other BeanInfo metadata.
* For standard JavaBeans introspection, use the JavaBeans Introspector.
* @param beanClass the target class to introspect
* @return a collection of property descriptors
* @throws IntrospectionException from introspecting the given bean class
* @since 6.0
* @see java.beans.Introspector#getBeanInfo(Class)
*/
public static Collection<PropertyDescriptor> determineBasicProperties(Class<?> beanClass) throws IntrospectionException {
Map<String, PropertyDescriptor> pdMap = new TreeMap<>();
for (Method method : beanClass.getMethods()) {
String methodName = method.getName();
boolean setter;
int nameIndex;
if (methodName.startsWith("set") && method.getParameterCount() == 1) {
setter = true;
nameIndex = 3;
}
else if (methodName.startsWith("get") && method.getParameterCount() == 0 && method.getReturnType() != Void.TYPE) {
setter = false;
nameIndex = 3;
}
else if (methodName.startsWith("is") && method.getParameterCount() == 0 && method.getReturnType() == boolean.class) {
setter = false;
nameIndex = 2;
}
else {
continue;
}
String propertyName = StringUtils.uncapitalizeAsProperty(methodName.substring(nameIndex));
PropertyDescriptor pd = pdMap.get(propertyName);
if (pd != null) {
if (setter) {
if (pd.getWriteMethod() == null ||
pd.getWriteMethod().getParameterTypes()[0].isAssignableFrom(method.getParameterTypes()[0])) {
try {
pd.setWriteMethod(method);
}
catch (IntrospectionException ex) {
// typically a type mismatch -> ignore
}
}
}
else {
if (pd.getReadMethod() == null ||
(pd.getReadMethod().getReturnType() == method.getReturnType() && method.getName().startsWith("is"))) {
try {
pd.setReadMethod(method);
}
catch (IntrospectionException ex) {
// typically a type mismatch -> ignore
}
}
}
}
else {
pd = new BasicPropertyDescriptor(propertyName, beanClass);
if (setter) {
pd.setWriteMethod(method);
}
else {
pd.setReadMethod(method);
}
pdMap.put(propertyName, pd);
}
}
return pdMap.values();
}
/**
* See {@link java.beans.FeatureDescriptor}.
*/
@ -173,4 +253,45 @@ abstract class PropertyDescriptorUtils {
pd.isBound() == otherPd.isBound() && pd.isConstrained() == otherPd.isConstrained());
}
/**
* PropertyDescriptor for {@link #determineBasicProperties(Class)},
* not performing any early type determination for
* {@link #setReadMethod}/{@link #setWriteMethod}.
*/
private static class BasicPropertyDescriptor extends PropertyDescriptor {
@Nullable
private Method readMethod;
@Nullable
private Method writeMethod;
public BasicPropertyDescriptor(String propertyName, Class<?> beanClass) throws IntrospectionException {
super(propertyName, beanClass, null, null);
}
@Override
public void setReadMethod(@Nullable Method readMethod) {
this.readMethod = readMethod;
}
@Override
@Nullable
public Method getReadMethod() {
return this.readMethod;
}
@Override
public void setWriteMethod(@Nullable Method writeMethod) {
this.writeMethod = writeMethod;
}
@Override
@Nullable
public Method getWriteMethod() {
return this.writeMethod;
}
}
}

View File

@ -0,0 +1,81 @@
/*
* Copyright 2002-2022 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import org.springframework.core.Ordered;
import org.springframework.core.SpringProperties;
import org.springframework.lang.NonNull;
/**
* {@link BeanInfoFactory} implementation that performs standard
* {@link java.beans.Introspector} inspection.
*
* <p>To be configured via a {@code META-INF/spring.factories} file with the following content:
* {@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.StandardBeanInfoFactory}
*
* <p>Ordered at {@link Ordered#LOWEST_PRECEDENCE} to allow other user-defined
* {@link BeanInfoFactory} types to take precedence.
*
* @author Juergen Hoeller
* @since 6.0
* @see ExtendedBeanInfoFactory
* @see CachedIntrospectionResults
* @see Introspector#getBeanInfo(Class)
*/
public class StandardBeanInfoFactory implements BeanInfoFactory, Ordered {
/**
* System property that instructs Spring to use the {@link Introspector#IGNORE_ALL_BEANINFO}
* mode when calling the JavaBeans {@link Introspector}: "spring.beaninfo.ignore", with a
* value of "true" skipping the search for {@code BeanInfo} classes (typically for scenarios
* where no such classes are being defined for beans in the application in the first place).
* <p>The default is "false", considering all {@code BeanInfo} metadata classes, like for
* standard {@link Introspector#getBeanInfo(Class)} calls. Consider switching this flag to
* "true" if you experience repeated ClassLoader access for non-existing {@code BeanInfo}
* classes, in case such access is expensive on startup or on lazy loading.
* <p>Note that such an effect may also indicate a scenario where caching doesn't work
* effectively: Prefer an arrangement where the Spring jars live in the same ClassLoader
* as the application classes, which allows for clean caching along with the application's
* lifecycle in any case. For a web application, consider declaring a local
* {@link org.springframework.web.util.IntrospectorCleanupListener} in {@code web.xml}
* in case of a multi-ClassLoader layout, which will allow for effective caching as well.
* @see Introspector#getBeanInfo(Class, int)
*/
public static final String IGNORE_BEANINFO_PROPERTY_NAME = "spring.beaninfo.ignore";
private static final boolean shouldIntrospectorIgnoreBeaninfoClasses =
SpringProperties.getFlag(IGNORE_BEANINFO_PROPERTY_NAME);
@Override
@NonNull
public BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
return (shouldIntrospectorIgnoreBeaninfoClasses ?
Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) :
Introspector.getBeanInfo(beanClass));
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}

View File

@ -16,9 +16,6 @@
package org.springframework.beans.factory.aot;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Arrays;
@ -34,8 +31,7 @@ import java.util.function.Predicate;
import org.springframework.aot.generate.GeneratedMethods;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.beans.BeanInfoFactory;
import org.springframework.beans.ExtendedBeanInfoFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.FactoryBean;
@ -81,8 +77,6 @@ class BeanDefinitionPropertiesCodeGenerator {
private static final String BEAN_DEFINITION_VARIABLE = BeanRegistrationCodeFragments.BEAN_DEFINITION_VARIABLE;
private static final BeanInfoFactory beanInfoFactory = new ExtendedBeanInfoFactory();
private final RuntimeHints hints;
private final Predicate<String> attributeFilter;
@ -185,9 +179,8 @@ class BeanDefinitionPropertiesCodeGenerator {
BEAN_DEFINITION_VARIABLE, propertyValue.getName(), valueCode);
}
Class<?> infrastructureType = getInfrastructureType(beanDefinition);
BeanInfo beanInfo = (infrastructureType != Object.class) ? getBeanInfo(infrastructureType) : null;
if (beanInfo != null) {
Map<String, Method> writeMethods = getWriteMethods(beanInfo);
if (infrastructureType != Object.class) {
Map<String, Method> writeMethods = getWriteMethods(infrastructureType);
for (PropertyValue propertyValue : propertyValues) {
Method writeMethod = writeMethods.get(propertyValue.getName());
if (writeMethod != null) {
@ -208,25 +201,10 @@ class BeanDefinitionPropertiesCodeGenerator {
return ClassUtils.getUserClass(beanDefinition.getResolvableType().toClass());
}
@Nullable
private BeanInfo getBeanInfo(Class<?> beanType) {
try {
BeanInfo beanInfo = beanInfoFactory.getBeanInfo(beanType);
if (beanInfo != null) {
return beanInfo;
}
return Introspector.getBeanInfo(beanType, Introspector.IGNORE_ALL_BEANINFO);
}
catch (IntrospectionException ex) {
return null;
}
}
private Map<String, Method> getWriteMethods(BeanInfo beanInfo) {
private Map<String, Method> getWriteMethods(Class<?> clazz) {
Map<String, Method> writeMethods = new HashMap<>();
for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
writeMethods.put(propertyDescriptor.getName(),
propertyDescriptor.getWriteMethod());
for (PropertyDescriptor propertyDescriptor : BeanUtils.getPropertyDescriptors(clazz)) {
writeMethods.put(propertyDescriptor.getName(), propertyDescriptor.getWriteMethod());
}
return Collections.unmodifiableMap(writeMethods);
}

View File

@ -1 +0,0 @@
org.springframework.beans.BeanInfoFactory=org.springframework.beans.ExtendedBeanInfoFactory

View File

@ -1,89 +0,0 @@
/*
* Copyright 2002-2019 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans;
import java.beans.IntrospectionException;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link ExtendedBeanInfoTests}.
*
* @author Chris Beams
*/
public class ExtendedBeanInfoFactoryTests {
private ExtendedBeanInfoFactory factory = new ExtendedBeanInfoFactory();
@Test
public void shouldNotSupportClassHavingOnlyVoidReturningSetter() throws IntrospectionException {
@SuppressWarnings("unused")
class C {
public void setFoo(String s) { }
}
assertThat(factory.getBeanInfo(C.class)).isNull();
}
@Test
public void shouldSupportClassHavingNonVoidReturningSetter() throws IntrospectionException {
@SuppressWarnings("unused")
class C {
public C setFoo(String s) { return this; }
}
assertThat(factory.getBeanInfo(C.class)).isNotNull();
}
@Test
public void shouldSupportClassHavingNonVoidReturningIndexedSetter() throws IntrospectionException {
@SuppressWarnings("unused")
class C {
public C setFoo(int i, String s) { return this; }
}
assertThat(factory.getBeanInfo(C.class)).isNotNull();
}
@Test
public void shouldNotSupportClassHavingNonPublicNonVoidReturningIndexedSetter() throws IntrospectionException {
@SuppressWarnings("unused")
class C {
void setBar(String s) { }
}
assertThat(factory.getBeanInfo(C.class)).isNull();
}
@Test
public void shouldNotSupportClassHavingNonVoidReturningParameterlessSetter() throws IntrospectionException {
@SuppressWarnings("unused")
class C {
C setBar() { return this; }
}
assertThat(factory.getBeanInfo(C.class)).isNull();
}
@Test
public void shouldNotSupportClassHavingNonVoidReturningMethodNamedSet() throws IntrospectionException {
@SuppressWarnings("unused")
class C {
C set(String s) { return this; }
}
assertThat(factory.getBeanInfo(C.class)).isNull();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@ -16,7 +16,6 @@
package org.springframework.context.annotation;
import java.beans.Introspector;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
@ -167,7 +166,7 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator {
String beanClassName = definition.getBeanClassName();
Assert.state(beanClassName != null, "No bean class name set");
String shortClassName = ClassUtils.getShortName(beanClassName);
return Introspector.decapitalize(shortClassName);
return StringUtils.uncapitalizeAsProperty(shortClassName);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@ -16,7 +16,6 @@
package org.springframework.context.annotation;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.annotation.Annotation;
@ -592,7 +591,7 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
if (this.isDefaultName) {
resourceName = this.member.getName();
if (this.member instanceof Method && resourceName.startsWith("set") && resourceName.length() > 3) {
resourceName = Introspector.decapitalize(resourceName.substring(3));
resourceName = StringUtils.uncapitalizeAsProperty(resourceName.substring(3));
}
}
else if (embeddedValueResolver != null) {
@ -638,7 +637,7 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
if (this.isDefaultName) {
resourceName = this.member.getName();
if (this.member instanceof Method && resourceName.startsWith("set") && resourceName.length() > 3) {
resourceName = Introspector.decapitalize(resourceName.substring(3));
resourceName = StringUtils.uncapitalizeAsProperty(resourceName.substring(3));
}
}
Class<?> resourceType = resource.beanInterface();

View File

@ -16,10 +16,6 @@
package org.springframework.aot.hint;
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.RecordComponent;
import java.lang.reflect.Type;
@ -92,17 +88,14 @@ public class BindingReflectionHintsRegistrar {
typeHint.withMembers(
MemberCategory.DECLARED_FIELDS,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
try {
BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
registerPropertyHints(hints, seen, propertyDescriptor.getWriteMethod(), 0);
registerPropertyHints(hints, seen, propertyDescriptor.getReadMethod(), -1);
for (Method method : clazz.getMethods()) {
String methodName = method.getName();
if (methodName.startsWith("set") && method.getParameterCount() == 1) {
registerPropertyHints(hints, seen, method, 0);
}
}
catch (IntrospectionException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring referenced type [" + clazz.getName() + "]: " + ex.getMessage());
else if ((methodName.startsWith("get") && method.getParameterCount() == 0 && method.getReturnType() != Void.TYPE) ||
(methodName.startsWith("is") && method.getParameterCount() == 0 && method.getReturnType() == boolean.class)) {
registerPropertyHints(hints, seen, method, -1);
}
}
}
@ -125,8 +118,8 @@ public class BindingReflectionHintsRegistrar {
}
private void registerPropertyHints(ReflectionHints hints, Set<Type> seen, @Nullable Method method, int parameterIndex) {
if (method != null && method.getDeclaringClass() != Object.class
&& method.getDeclaringClass() != Enum.class) {
if (method != null && method.getDeclaringClass() != Object.class &&
method.getDeclaringClass() != Enum.class) {
hints.registerMethod(method, ExecutableMode.INVOKE);
MethodParameter methodParameter = MethodParameter.forExecutable(method, parameterIndex);
Type methodParameterType = methodParameter.getGenericParameterType();

View File

@ -16,7 +16,6 @@
package org.springframework.util;
import java.beans.Introspector;
import java.io.Closeable;
import java.io.Externalizable;
import java.io.Serializable;
@ -997,13 +996,13 @@ public abstract class ClassUtils {
* property format. Strips the outer class name in case of a nested class.
* @param clazz the class
* @return the short name rendered in a standard JavaBeans property format
* @see java.beans.Introspector#decapitalize(String)
* @see StringUtils#uncapitalizeAsProperty(String)
*/
public static String getShortNameAsProperty(Class<?> clazz) {
String shortName = getShortName(clazz);
int dotIndex = shortName.lastIndexOf(PACKAGE_SEPARATOR);
shortName = (dotIndex != -1 ? shortName.substring(dotIndex + 1) : shortName);
return Introspector.decapitalize(shortName);
return StringUtils.uncapitalizeAsProperty(shortName);
}
/**

View File

@ -554,6 +554,24 @@ public abstract class StringUtils {
return changeFirstCharacterCase(str, false);
}
/**
* Uncapitalize a {@code String} in JavaBeans property format,
* changing the first letter to lower case as per
* {@link Character#toLowerCase(char)}, unless the initial two
* letters are upper case in direct succession.
* @param str the {@code String} to uncapitalize
* @return the uncapitalized {@code String}
* @since 6.0
* @see java.beans.Introspector#decapitalize(String)
*/
public static String uncapitalizeAsProperty(String str) {
if (!hasLength(str) || (str.length() > 1 && Character.isUpperCase(str.charAt(0)) &&
Character.isUpperCase(str.charAt(1)))) {
return str;
}
return changeFirstCharacterCase(str, false);
}
private static String changeFirstCharacterCase(String str, boolean capitalize) {
if (!hasLength(str)) {
return str;