diff --git a/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java b/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java index e17d334022..f63738405e 100644 --- a/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java +++ b/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -22,6 +22,7 @@ import java.lang.reflect.Method; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.aop.support.AopUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -289,22 +290,14 @@ public abstract class ReflectionTestUtils { /** * Invoke the setter method with the given {@code name} on the supplied * target object with the supplied {@code value}. - *
This method traverses the class hierarchy in search of the desired - * method. In addition, an attempt will be made to make non-{@code public} - * methods accessible, thus allowing one to invoke {@code protected}, - * {@code private}, and package-private setter methods. - *
In addition, this method supports JavaBean-style property - * names. For example, if you wish to set the {@code name} property on the - * target object, you may pass either "name" or - * "setName" as the method name. + *
This method delegates to + * {@link #invokeSetterMethod(Object, String, Object, Class)}, supplying + * {@code null} for the parameter type. * @param target the target object on which to invoke the specified setter * method * @param name the name of the setter method to invoke or the corresponding * property name * @param value the value to provide to the setter method - * @see ReflectionUtils#findMethod(Class, String, Class[]) - * @see ReflectionUtils#makeAccessible(Method) - * @see ReflectionUtils#invokeMethod(Method, Object, Object[]) */ public static void invokeSetterMethod(Object target, String name, Object value) { invokeSetterMethod(target, name, value, null); @@ -317,19 +310,24 @@ public abstract class ReflectionTestUtils { * method. In addition, an attempt will be made to make non-{@code public} * methods accessible, thus allowing one to invoke {@code protected}, * {@code private}, and package-private setter methods. - *
In addition, this method supports JavaBean-style property - * names. For example, if you wish to set the {@code name} property on the - * target object, you may pass either "name" or - * "setName" as the method name. + *
This method also supports JavaBean-style property names. For + * example, if you wish to set the {@code name} property on the target object, + * you may pass either {@code "name"} or {@code "setName"} as the method name. + *
As of Spring Framework 6.2, if the supplied target object is a CGLIB + * proxy which does not intercept the setter method, the proxy will be + * {@linkplain AopTestUtils#getUltimateTargetObject unwrapped} allowing the + * setter method to be invoked directly on the ultimate target of the proxy. * @param target the target object on which to invoke the specified setter * method * @param name the name of the setter method to invoke or the corresponding * property name * @param value the value to provide to the setter method * @param type the formal parameter type declared by the setter method + * (may be {@code null} to indicate any type) * @see ReflectionUtils#findMethod(Class, String, Class[]) * @see ReflectionUtils#makeAccessible(Method) * @see ReflectionUtils#invokeMethod(Method, Object, Object[]) + * @see AopTestUtils#getUltimateTargetObject(Object) */ public static void invokeSetterMethod(Object target, String name, @Nullable Object value, @Nullable Class> type) { Assert.notNull(target, "Target object must not be null"); @@ -357,6 +355,14 @@ public abstract class ReflectionTestUtils { safeToString(target), value)); } + if (springAopPresent) { + // If the target is a CGLIB proxy which does not intercept the method, invoke the + // method on the ultimate target. + if (isCglibProxyThatDoesNotInterceptMethod(target, method)) { + target = AopTestUtils.getUltimateTargetObject(target); + } + } + ReflectionUtils.makeAccessible(method); ReflectionUtils.invokeMethod(method, target, value); } @@ -368,10 +374,13 @@ public abstract class ReflectionTestUtils { * method. In addition, an attempt will be made to make non-{@code public} * methods accessible, thus allowing one to invoke {@code protected}, * {@code private}, and package-private getter methods. - *
In addition, this method supports JavaBean-style property - * names. For example, if you wish to get the {@code name} property on the - * target object, you may pass either "name" or - * "getName" as the method name. + *
This method also supports JavaBean-style property names. For + * example, if you wish to get the {@code name} property on the target object, + * you may pass either {@code "name"} or {@code "getName"} as the method name. + *
As of Spring Framework 6.2, if the supplied target object is a CGLIB
+ * proxy which does not intercept the getter method, the proxy will be
+ * {@linkplain AopTestUtils#getUltimateTargetObject unwrapped} allowing the
+ * getter method to be invoked directly on the ultimate target of the proxy.
* @param target the target object on which to invoke the specified getter
* method
* @param name the name of the getter method to invoke or the corresponding
@@ -380,6 +389,7 @@ public abstract class ReflectionTestUtils {
* @see ReflectionUtils#findMethod(Class, String, Class[])
* @see ReflectionUtils#makeAccessible(Method)
* @see ReflectionUtils#invokeMethod(Method, Object, Object[])
+ * @see AopTestUtils#getUltimateTargetObject(Object)
*/
@Nullable
public static Object invokeGetterMethod(Object target, String name) {
@@ -400,6 +410,14 @@ public abstract class ReflectionTestUtils {
"Could not find getter method '%s' on %s", getterMethodName, safeToString(target)));
}
+ if (springAopPresent) {
+ // If the target is a CGLIB proxy which does not intercept the method, invoke the
+ // method on the ultimate target.
+ if (isCglibProxyThatDoesNotInterceptMethod(target, method)) {
+ target = AopTestUtils.getUltimateTargetObject(target);
+ }
+ }
+
if (logger.isDebugEnabled()) {
logger.debug(String.format("Invoking getter method '%s' on %s", getterMethodName, safeToString(target)));
}
@@ -418,10 +436,6 @@ public abstract class ReflectionTestUtils {
* @return the invocation result, if any
* @see #invokeMethod(Class, String, Object...)
* @see #invokeMethod(Object, Class, String, Object...)
- * @see MethodInvoker
- * @see ReflectionUtils#makeAccessible(Method)
- * @see ReflectionUtils#invokeMethod(Method, Object, Object[])
- * @see ReflectionUtils#handleReflectionException(Exception)
*/
@Nullable
public static As of Spring Framework 6.2, if the supplied target object is a CGLIB
+ * proxy which does not intercept the method, the proxy will be
+ * {@linkplain AopTestUtils#getUltimateTargetObject unwrapped} allowing the
+ * method to be invoked directly on the ultimate target of the proxy.
* @param targetObject the target object on which to invoke the method; may
* be {@code null} if the method is static
* @param targetClass the target class on which to invoke the method; may
@@ -471,8 +485,8 @@ public abstract class ReflectionTestUtils {
* @see #invokeMethod(Class, String, Object...)
* @see MethodInvoker
* @see ReflectionUtils#makeAccessible(Method)
- * @see ReflectionUtils#invokeMethod(Method, Object, Object[])
* @see ReflectionUtils#handleReflectionException(Exception)
+ * @see AopTestUtils#getUltimateTargetObject(Object)
*/
@SuppressWarnings("unchecked")
@Nullable
@@ -493,6 +507,15 @@ public abstract class ReflectionTestUtils {
methodInvoker.setArguments(args);
methodInvoker.prepare();
+ if (targetObject != null && springAopPresent) {
+ // If the target is a CGLIB proxy which does not intercept the method, invoke the
+ // method on the ultimate target.
+ if (isCglibProxyThatDoesNotInterceptMethod(targetObject, methodInvoker.getPreparedMethod())) {
+ targetObject = AopTestUtils.getUltimateTargetObject(targetObject);
+ methodInvoker.setTargetObject(targetObject);
+ }
+ }
+
if (logger.isDebugEnabled()) {
logger.debug(String.format("Invoking method '%s' on %s or %s with arguments %s", name,
safeToString(targetObject), safeToString(targetClass), ObjectUtils.nullSafeToString(args)));
@@ -520,4 +543,13 @@ public abstract class ReflectionTestUtils {
return String.format("target class [%s]", (clazz != null ? clazz.getName() : null));
}
+ /**
+ * Determine if the supplied target object is a CBLIB proxy that does not intercept the
+ * supplied method.
+ * @since 6.2
+ */
+ private static boolean isCglibProxyThatDoesNotInterceptMethod(Object target, Method method) {
+ return (AopUtils.isCglibProxy(target) && !method.getDeclaringClass().equals(target.getClass()));
+ }
+
}
diff --git a/spring-test/src/test/java/org/springframework/test/util/ReflectionTestUtilsTests.java b/spring-test/src/test/java/org/springframework/test/util/ReflectionTestUtilsTests.java
index b8b95cc93b..5ba8bb36ab 100644
--- a/spring-test/src/test/java/org/springframework/test/util/ReflectionTestUtilsTests.java
+++ b/spring-test/src/test/java/org/springframework/test/util/ReflectionTestUtilsTests.java
@@ -32,6 +32,7 @@ import org.springframework.test.util.subpackage.StaticMethods;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.assertj.core.api.SoftAssertions.assertSoftly;
import static org.springframework.test.util.ReflectionTestUtils.getField;
import static org.springframework.test.util.ReflectionTestUtils.invokeGetterMethod;
import static org.springframework.test.util.ReflectionTestUtils.invokeMethod;
@@ -48,7 +49,7 @@ class ReflectionTestUtilsTests {
private static final Float PI = (float) 22 / 7;
- private final Person person = new PersonEntity();
+ private final PersonEntity person = new PersonEntity();
private final Component component = new Component();
@@ -156,7 +157,7 @@ class ReflectionTestUtilsTests {
assertThat(person.getAge()).as("age (private field)").isEqualTo(42);
assertThat(person.getEyeColor()).as("eye color (package private field)").isEqualTo("blue");
assertThat(person.likesPets()).as("'likes pets' flag (package private boolean field)").isTrue();
- assertThat(person.getFavoriteNumber()).as("'favorite number' (package field)").isEqualTo(PI);
+ assertThat(person.getFavoriteNumber()).as("'favorite number' (private field)").isEqualTo(PI);
}
private static void assertSetFieldAndGetFieldBehaviorForProxy(Person proxy, Person target) {
@@ -168,7 +169,7 @@ class ReflectionTestUtilsTests {
assertThat(target.getAge()).as("age (private field)").isEqualTo(42);
assertThat(target.getEyeColor()).as("eye color (package private field)").isEqualTo("blue");
assertThat(target.likesPets()).as("'likes pets' flag (package private boolean field)").isTrue();
- assertThat(target.getFavoriteNumber()).as("'favorite number' (package field)").isEqualTo(PI);
+ assertThat(target.getFavoriteNumber()).as("'favorite number' (private field)").isEqualTo(PI);
}
@Test
@@ -188,7 +189,7 @@ class ReflectionTestUtilsTests {
assertThat(person.getName()).as("name (protected field)").isNull();
assertThat(person.getEyeColor()).as("eye color (package private field)").isNull();
- assertThat(person.getFavoriteNumber()).as("'favorite number' (package field)").isNull();
+ assertThat(person.getFavoriteNumber()).as("'favorite number' (private field)").isNull();
}
@Test
@@ -295,6 +296,22 @@ class ReflectionTestUtilsTests {
assertThat(invokeGetterMethod(person, "favoriteNumber")).isEqualTo(PI);
}
+ @Test // gh-33429
+ void invokingPrivateGetterMethodViaCglibProxyInvokesMethodOnUltimateTarget() {
+ this.person.setPrivateEye("I");
+ ProxyFactory pf = new ProxyFactory(this.person);
+ pf.setProxyTargetClass(true);
+ PersonEntity proxy = (PersonEntity) pf.getProxy();
+ assertThat(AopUtils.isCglibProxy(proxy)).as("Proxy is a CGLIB proxy").isTrue();
+
+ assertSoftly(softly -> {
+ softly.assertThat(getField(this.person, "privateEye")).as("'privateEye' (private field in target)").isEqualTo("I");
+ softly.assertThat(getField(proxy, "privateEye")).as("'privateEye' (private field in proxy)").isEqualTo("I");
+ softly.assertThat(invokeGetterMethod(this.person, "privateEye")).as("'privateEye' (getter on target)").isEqualTo("I");
+ softly.assertThat(invokeGetterMethod(proxy, "privateEye")).as("'privateEye' (getter on proxy)").isEqualTo("I");
+ });
+ }
+
@Test
void invokeSetterMethodWithNullValuesForNonPrimitives() {
invokeSetterMethod(person, "name", null, String.class);
@@ -306,6 +323,41 @@ class ReflectionTestUtilsTests {
assertThat(person.getFavoriteNumber()).as("'favorite number' (protected method for a Number)").isNull();
}
+ @Test // gh-33429
+ void invokingPrivateSetterMethodViaCglibProxyInvokesMethodOnUltimateTarget() {
+ ProxyFactory pf = new ProxyFactory(this.person);
+ pf.setProxyTargetClass(true);
+ PersonEntity proxy = (PersonEntity) pf.getProxy();
+ assertThat(AopUtils.isCglibProxy(proxy)).as("Proxy is a CGLIB proxy").isTrue();
+
+ // Set reflectively
+ invokeSetterMethod(proxy, "favoriteNumber", PI, Number.class);
+
+ assertSoftly(softly -> {
+ softly.assertThat(getField(proxy, "favoriteNumber")).as("'favorite number' (private field)").isEqualTo(PI);
+ softly.assertThat(proxy.getFavoriteNumber()).as("'favorite number' (getter on proxy)").isEqualTo(PI);
+ softly.assertThat(this.person.getFavoriteNumber()).as("'favorite number' (getter on target)").isEqualTo(PI);
+ });
+ }
+
+ @Test // gh-33429
+ void invokingFinalSetterMethodViaCglibProxyInvokesMethodOnUltimateTarget() {
+ ProxyFactory pf = new ProxyFactory(this.person);
+ pf.setProxyTargetClass(true);
+ PersonEntity proxy = (PersonEntity) pf.getProxy();
+ assertThat(AopUtils.isCglibProxy(proxy)).as("Proxy is a CGLIB proxy").isTrue();
+ assertThat(proxy.getPuzzle()).as("puzzle").isNull();
+
+ // Set reflectively
+ invokeSetterMethod(proxy, "puzzle", "enigma", String.class);
+
+ assertSoftly(softly -> {
+ softly.assertThat(getField(proxy, "puzzle")).as("'puzzle' (private field)").isEqualTo("enigma");
+ softly.assertThat(proxy.getPuzzle()).as("'puzzle' (getter on proxy)").isEqualTo("enigma");
+ softly.assertThat(this.person.getPuzzle()).as("'puzzle' (getter on target)").isEqualTo("enigma");
+ });
+ }
+
@Test
void invokeSetterMethodWithNullValueForPrimitiveLong() {
assertThatIllegalArgumentException().isThrownBy(() -> invokeSetterMethod(person, "id", null, long.class));
@@ -362,6 +414,23 @@ class ReflectionTestUtilsTests {
assertThat(component.getText()).as("text").isNull();
}
+ @Test // gh-33429
+ void invokingPrivateMethodViaCglibProxyInvokesMethodOnUltimateTarget() {
+ ProxyFactory pf = new ProxyFactory(this.person);
+ pf.setProxyTargetClass(true);
+ PersonEntity proxy = (PersonEntity) pf.getProxy();
+ assertThat(AopUtils.isCglibProxy(proxy)).as("Proxy is a CGLIB proxy").isTrue();
+
+ // Set reflectively
+ invokeMethod(proxy, "setFavoriteNumber", PI);
+
+ assertSoftly(softly -> {
+ softly.assertThat(getField(proxy, "favoriteNumber")).as("'favorite number' (private field)").isEqualTo(PI);
+ softly.assertThat(proxy.getFavoriteNumber()).as("'favorite number' (getter on proxy)").isEqualTo(PI);
+ softly.assertThat(this.person.getFavoriteNumber()).as("'favorite number' (getter on target)").isEqualTo(PI);
+ });
+ }
+
@Test
void invokeInitMethodBeforeAutowiring() {
assertThatIllegalStateException()
diff --git a/spring-test/src/test/java/org/springframework/test/util/subpackage/PersonEntity.java b/spring-test/src/test/java/org/springframework/test/util/subpackage/PersonEntity.java
index 45b841983e..e247e92d85 100644
--- a/spring-test/src/test/java/org/springframework/test/util/subpackage/PersonEntity.java
+++ b/spring-test/src/test/java/org/springframework/test/util/subpackage/PersonEntity.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2016 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.
@@ -37,6 +37,10 @@ public class PersonEntity extends PersistentEntity implements Person {
private Number favoriteNumber;
+ private String puzzle;
+
+ private String privateEye;
+
@Override
public String getName() {
@@ -44,7 +48,7 @@ public class PersonEntity extends PersistentEntity implements Person {
}
@SuppressWarnings("unused")
- private void setName(final String name) {
+ private void setName(String name) {
this.name = name;
}
@@ -53,7 +57,7 @@ public class PersonEntity extends PersistentEntity implements Person {
return this.age;
}
- protected void setAge(final int age) {
+ protected void setAge(int age) {
this.age = age;
}
@@ -62,7 +66,7 @@ public class PersonEntity extends PersistentEntity implements Person {
return this.eyeColor;
}
- void setEyeColor(final String eyeColor) {
+ void setEyeColor(String eyeColor) {
this.eyeColor = eyeColor;
}
@@ -71,7 +75,7 @@ public class PersonEntity extends PersistentEntity implements Person {
return this.likesPets;
}
- protected void setLikesPets(final boolean likesPets) {
+ protected void setLikesPets(boolean likesPets) {
this.likesPets = likesPets;
}
@@ -80,10 +84,28 @@ public class PersonEntity extends PersistentEntity implements Person {
return this.favoriteNumber;
}
- protected void setFavoriteNumber(Number favoriteNumber) {
+ @SuppressWarnings("unused")
+ private void setFavoriteNumber(Number favoriteNumber) {
this.favoriteNumber = favoriteNumber;
}
+ public String getPuzzle() {
+ return this.puzzle;
+ }
+
+ public final void setPuzzle(String puzzle) {
+ this.puzzle = puzzle;
+ }
+
+ @SuppressWarnings("unused")
+ private String getPrivateEye() {
+ return this.privateEye;
+ }
+
+ public void setPrivateEye(String privateEye) {
+ this.privateEye = privateEye;
+ }
+
@Override
public String toString() {
// @formatter:off
@@ -94,6 +116,8 @@ public class PersonEntity extends PersistentEntity implements Person {
.append("eyeColor", this.eyeColor)
.append("likesPets", this.likesPets)
.append("favoriteNumber", this.favoriteNumber)
+ .append("puzzle", this.puzzle)
+ .append("privateEye", this.privateEye)
.toString();
// @formatter:on
}