diff --git a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java index e35836f1f5a..eb7b34d4732 100644 --- a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java @@ -46,12 +46,12 @@ import org.springframework.lang.Nullable; public abstract class ReflectionUtils { /** - * Pre-built MethodFilter that matches all non-bridge non-synthetic methods + * Pre-built {@link MethodFilter} that matches all non-bridge non-synthetic methods * which are not declared on {@code java.lang.Object}. * @since 3.0.5 */ public static final MethodFilter USER_DECLARED_METHODS = - (method -> !method.isBridge() && !method.isSynthetic()); + (method -> !method.isBridge() && !method.isSynthetic() && (method.getDeclaringClass() != Object.class)); /** * Pre-built FieldFilter that matches all non-static, non-final fields. @@ -353,7 +353,10 @@ public abstract class ReflectionUtils { * @throws IllegalStateException if introspection fails */ public static void doWithMethods(Class clazz, MethodCallback mc, @Nullable MethodFilter mf) { - // Keep backing up the inheritance hierarchy. + if (mf == USER_DECLARED_METHODS && clazz == Object.class) { + // nothing to introspect + return; + } Method[] methods = getDeclaredMethods(clazz, false); for (Method method : methods) { if (mf != null && !mf.matches(method)) { @@ -366,6 +369,7 @@ public abstract class ReflectionUtils { throw new IllegalStateException("Not allowed to access method '" + method.getName() + "': " + ex); } } + // Keep backing up the inheritance hierarchy. if (clazz.getSuperclass() != null && (mf != USER_DECLARED_METHODS || clazz.getSuperclass() != Object.class)) { doWithMethods(clazz.getSuperclass(), mc, mf); } diff --git a/spring-core/src/test/java/org/springframework/util/ReflectionUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ReflectionUtilsTests.java index 8ca69ddc9a0..ef089755251 100644 --- a/spring-core/src/test/java/org/springframework/util/ReflectionUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ReflectionUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.tests.sample.objects.TestObject; +import org.springframework.util.ReflectionUtils.MethodFilter; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -184,27 +185,46 @@ class ReflectionUtilsTests { } @Test - void doWithProtectedMethods() { + void doWithMethodsUsingProtectedFilter() { ListSavingMethodCallback mc = new ListSavingMethodCallback(); ReflectionUtils.doWithMethods(TestObject.class, mc, method -> Modifier.isProtected(method.getModifiers())); - assertThat(mc.getMethodNames().isEmpty()).isFalse(); - assertThat(mc.getMethodNames().contains("clone")).as("Must find protected method on Object").isTrue(); - assertThat(mc.getMethodNames().contains("finalize")).as("Must find protected method on Object").isTrue(); - assertThat(mc.getMethodNames().contains("hashCode")).as("Public, not protected").isFalse(); - assertThat(mc.getMethodNames().contains("absquatulate")).as("Public, not protected").isFalse(); + assertThat(mc.getMethodNames()) + .hasSizeGreaterThanOrEqualTo(2) + .as("Must find protected methods on Object").contains("clone", "finalize") + .as("Public, not protected").doesNotContain("hashCode", "absquatulate"); } @Test - void duplicatesFound() { + void doWithMethodsUsingUserDeclaredMethodsFilterStartingWithObject() { + ListSavingMethodCallback mc = new ListSavingMethodCallback(); + ReflectionUtils.doWithMethods(Object.class, mc, ReflectionUtils.USER_DECLARED_METHODS); + assertThat(mc.getMethodNames()).isEmpty(); + } + + @Test + void doWithMethodsUsingUserDeclaredMethodsFilterStartingWithTestObject() { + ListSavingMethodCallback mc = new ListSavingMethodCallback(); + ReflectionUtils.doWithMethods(TestObject.class, mc, ReflectionUtils.USER_DECLARED_METHODS); + assertThat(mc.getMethodNames()) + .as("user declared methods").contains("absquatulate", "compareTo", "getName", "setName", "getAge", "setAge", "getSpouse", "setSpouse") + .as("methods on Object").doesNotContain("equals", "hashCode", "toString", "clone", "finalize", "getClass", "notify", "notifyAll", "wait"); + } + + @Test + void doWithMethodsUsingUserDeclaredMethodsComposedFilter() { + ListSavingMethodCallback mc = new ListSavingMethodCallback(); + // "q" because both absquatulate() and equals() contain "q" + MethodFilter isSetterMethodOrNameContainsQ = m -> m.getName().startsWith("set") || m.getName().contains("q"); + MethodFilter methodFilter = ReflectionUtils.USER_DECLARED_METHODS.and(isSetterMethodOrNameContainsQ); + ReflectionUtils.doWithMethods(TestObject.class, mc, methodFilter); + assertThat(mc.getMethodNames()).containsExactlyInAnyOrder("setName", "setAge", "setSpouse", "absquatulate"); + } + + @Test + void doWithMethodsFindsDuplicatesInClassHierarchy() { ListSavingMethodCallback mc = new ListSavingMethodCallback(); ReflectionUtils.doWithMethods(TestObjectSubclass.class, mc); - int absquatulateCount = 0; - for (String name : mc.getMethodNames()) { - if (name.equals("absquatulate")) { - ++absquatulateCount; - } - } - assertThat(absquatulateCount).as("Found 2 absquatulates").isEqualTo(2); + assertThat(mc.getMethodNames().stream()).filteredOn("absquatulate"::equals).as("Found 2 absquatulates").hasSize(2); } @Test