From bba313c2f51dcf45125f5f4ee787823ad04342f9 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 13 Oct 2022 19:02:45 +0200 Subject: [PATCH] Perform basic property determination without java.beans.Introspector Closes gh-29320 --- .../beans/CachedIntrospectionResults.java | 55 +++----- .../beans/ExtendedBeanInfoFactory.java | 32 ++--- .../beans/PropertyDescriptorUtils.java | 123 +++++++++++++++++- .../beans/StandardBeanInfoFactory.java | 81 ++++++++++++ ...BeanDefinitionPropertiesCodeGenerator.java | 34 +---- .../main/resources/META-INF/spring.factories | 1 - .../beans/ExtendedBeanInfoFactoryTests.java | 89 ------------- .../AnnotationBeanNameGenerator.java | 5 +- .../CommonAnnotationBeanPostProcessor.java | 7 +- .../hint/BindingReflectionHintsRegistrar.java | 25 ++-- .../org/springframework/util/ClassUtils.java | 5 +- .../org/springframework/util/StringUtils.java | 18 +++ 12 files changed, 276 insertions(+), 199 deletions(-) create mode 100644 spring-beans/src/main/java/org/springframework/beans/StandardBeanInfoFactory.java delete mode 100644 spring-beans/src/main/resources/META-INF/spring.factories delete mode 100644 spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoFactoryTests.java 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 bd234eb58f..02f4ec7200 100644 --- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java @@ -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; *

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. * - *

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. + *

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). - *

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. - *

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 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 pds = PropertyDescriptorUtils.determineBasicProperties(beanClass); + return new SimpleBeanInfo() { + @Override + public PropertyDescriptor[] getPropertyDescriptors() { + return pds.toArray(EMPTY_PROPERTY_DESCRIPTOR_ARRAY); + } + }; } diff --git a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java index d7d2b2ecc9..5f41742632 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java @@ -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. + * + *

To be configured via a {@code META-INF/spring.factories} file with the following content: + * {@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.ExtendedBeanInfoFactory} * *

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; - } - } diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java b/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java index aa9909822d..a5d760380b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java @@ -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. + *

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 determineBasicProperties(Class beanClass) throws IntrospectionException { + Map 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; + } + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/StandardBeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/StandardBeanInfoFactory.java new file mode 100644 index 0000000000..7abdade7c4 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/StandardBeanInfoFactory.java @@ -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. + * + *

To be configured via a {@code META-INF/spring.factories} file with the following content: + * {@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.StandardBeanInfoFactory} + * + *

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). + *

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. + *

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; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java index cef034888c..ec13bb3a53 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java @@ -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 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 writeMethods = getWriteMethods(beanInfo); + if (infrastructureType != Object.class) { + Map 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 getWriteMethods(BeanInfo beanInfo) { + private Map getWriteMethods(Class clazz) { Map 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); } diff --git a/spring-beans/src/main/resources/META-INF/spring.factories b/spring-beans/src/main/resources/META-INF/spring.factories deleted file mode 100644 index b623047ec2..0000000000 --- a/spring-beans/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1 +0,0 @@ -org.springframework.beans.BeanInfoFactory=org.springframework.beans.ExtendedBeanInfoFactory \ No newline at end of file diff --git a/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoFactoryTests.java deleted file mode 100644 index c4449bcb5b..0000000000 --- a/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoFactoryTests.java +++ /dev/null @@ -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(); - } - -} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java index 4f2dc199fd..be73ec25d0 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java @@ -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); } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java index 18a6245b64..b7b31fba94 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java @@ -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(); diff --git a/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java b/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java index 134711407d..d56189935a 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java @@ -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 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(); diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index 5c4fb6ed22..ea4618d03d 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -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); } /** diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index 711fc170b7..3c2716be5a 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -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;