diff --git a/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java b/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java index 9f905cbda47..f1ceac4a7cc 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java +++ b/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -36,6 +36,7 @@ import org.springframework.util.ReflectionUtils; * * @author Juergen Hoeller * @author Rossen Stoyanchev + * @author Sam Brannen * @since 4.2.3 */ public final class MethodIntrospector { @@ -75,6 +76,7 @@ public final class MethodIntrospector { if (result != null) { Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); if (bridgedMethod == specificMethod || bridgedMethod == method || + bridgedMethod.equals(specificMethod) || bridgedMethod.equals(method) || metadataLookup.inspect(bridgedMethod) == null) { methodMap.put(specificMethod, result); } 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 062a386c397..003da863f3b 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -305,7 +305,7 @@ public abstract class ClassUtils { } catch (ClassNotFoundException ex) { int lastDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR); - int previousDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR, lastDotIndex -1); + int previousDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR, lastDotIndex - 1); if (lastDotIndex != -1 && previousDotIndex != -1 && Character.isUpperCase(name.charAt(previousDotIndex + 1))) { String nestedClassName = name.substring(0, lastDotIndex) + NESTED_CLASS_SEPARATOR + name.substring(lastDotIndex + 1); diff --git a/spring-core/src/test/java/a/ClassHavingNestedClass.java b/spring-core/src/test/java/a/ClassHavingNestedClass.java index b18d8b05878..2170cdcd860 100644 --- a/spring-core/src/test/java/a/ClassHavingNestedClass.java +++ b/spring-core/src/test/java/a/ClassHavingNestedClass.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,6 +19,9 @@ package a; /** * Test class for {@code org.springframework.util.ClassUtilsTests}. * + *

The use case for this test class requires that the package name is a single + * character (i.e., length of 1). + * * @author Johnny Lim */ public class ClassHavingNestedClass { diff --git a/spring-core/src/test/java/org/springframework/core/MethodIntrospectorTests.java b/spring-core/src/test/java/org/springframework/core/MethodIntrospectorTests.java new file mode 100644 index 00000000000..f11f8eb8659 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/MethodIntrospectorTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2024 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.core; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodIntrospector.MetadataLookup; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.TYPE_HIERARCHY; + +/** + * Tests for {@link MethodIntrospector}. + * + * @author Sam Brannen + * @since 5.3.34 + */ +class MethodIntrospectorTests { + + @Test // gh-32586 + void selectMethodsAndClearDeclaredMethodsCacheBetweenInvocations() { + Class targetType = ActualController.class; + + // Preconditions for this use case. + assertThat(targetType).isPublic(); + assertThat(targetType.getSuperclass()).isPackagePrivate(); + + MetadataLookup metadataLookup = (MetadataLookup) method -> { + if (MergedAnnotations.from(method, TYPE_HIERARCHY).isPresent(Mapped.class)) { + return method.getName(); + } + return null; + }; + + // Start with a clean slate. + ReflectionUtils.clearCache(); + + // Round #1 + Map methods = MethodIntrospector.selectMethods(targetType, metadataLookup); + assertThat(methods.values()).containsExactlyInAnyOrder("update", "delete"); + + // Simulate ConfigurableApplicationContext#refresh() which clears the + // ReflectionUtils#declaredMethodsCache but NOT the BridgeMethodResolver#cache. + // As a consequence, ReflectionUtils.getDeclaredMethods(...) will return a + // new set of methods that are logically equivalent to but not identical + // to (in terms of object identity) any bridged methods cached in the + // BridgeMethodResolver cache. + ReflectionUtils.clearCache(); + + // Round #2 + methods = MethodIntrospector.selectMethods(targetType, metadataLookup); + assertThat(methods.values()).containsExactlyInAnyOrder("update", "delete"); + } + + + @Retention(RetentionPolicy.RUNTIME) + @interface Mapped { + } + + interface Controller { + + void unmappedMethod(); + + @Mapped + void update(); + + @Mapped + void delete(); + } + + // Must NOT be public. + abstract static class AbstractController implements Controller { + + @Override + public void unmappedMethod() { + } + + @Override + public void delete() { + } + } + + // MUST be public. + public static class ActualController extends AbstractController { + + @Override + public void update() { + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java index b2a40a8ffd4..dd834e75dfa 100644 --- a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Set; import java.util.function.Supplier; +import a.ClassHavingNestedClass; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -86,8 +87,11 @@ class ClassUtilsTests { void forNameWithNestedType() throws ClassNotFoundException { assertThat(ClassUtils.forName("org.springframework.util.ClassUtilsTests$NestedClass", classLoader)).isEqualTo(NestedClass.class); assertThat(ClassUtils.forName("org.springframework.util.ClassUtilsTests.NestedClass", classLoader)).isEqualTo(NestedClass.class); - assertThat(ClassUtils.forName("a.ClassHavingNestedClass$NestedClass", classLoader)).isEqualTo(a.ClassHavingNestedClass.NestedClass.class); - assertThat(ClassUtils.forName("a.ClassHavingNestedClass.NestedClass", classLoader)).isEqualTo(a.ClassHavingNestedClass.NestedClass.class); + + // Precondition: package name must have length == 1. + assertThat(ClassHavingNestedClass.class.getPackageName().length()).isEqualTo(1); + assertThat(ClassUtils.forName("a.ClassHavingNestedClass$NestedClass", classLoader)).isEqualTo(ClassHavingNestedClass.NestedClass.class); + assertThat(ClassUtils.forName("a.ClassHavingNestedClass.NestedClass", classLoader)).isEqualTo(ClassHavingNestedClass.NestedClass.class); } @Test