Skip to content

Commit 38a56b3

Browse files
committed
Unwrap CGLIB proxy when invoking non-proxied methods in ReflectionTestUtils
Prior to this commit, when ReflectionTestUtils was used to invoke a method on a CGLIB proxy, the invocation was always performed directly on the proxy. Consequently, if the method was not proxied/intercepted by the CGLIB proxy -- for example, if the method was final or effectively private -- the invoked method could not operate on the state of the target object or interact with other private methods in the target object. With this commit, if the supplied target object is a CGLIB proxy which does not intercept the method, the proxy will be unwrapped allowing the method to be invoked directly on the ultimate target of the proxy. Closes gh-33429
1 parent 6d1294d commit 38a56b3

File tree

3 files changed

+164
-39
lines changed

3 files changed

+164
-39
lines changed

spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@
2222
import org.apache.commons.logging.Log;
2323
import org.apache.commons.logging.LogFactory;
2424

25+
import org.springframework.aop.support.AopUtils;
2526
import org.springframework.lang.Nullable;
2627
import org.springframework.util.Assert;
2728
import org.springframework.util.ClassUtils;
@@ -289,22 +290,14 @@ public static Object getField(@Nullable Object targetObject, @Nullable Class<?>
289290
/**
290291
* Invoke the setter method with the given {@code name} on the supplied
291292
* target object with the supplied {@code value}.
292-
* <p>This method traverses the class hierarchy in search of the desired
293-
* method. In addition, an attempt will be made to make non-{@code public}
294-
* methods <em>accessible</em>, thus allowing one to invoke {@code protected},
295-
* {@code private}, and <em>package-private</em> setter methods.
296-
* <p>In addition, this method supports JavaBean-style <em>property</em>
297-
* names. For example, if you wish to set the {@code name} property on the
298-
* target object, you may pass either &quot;name&quot; or
299-
* &quot;setName&quot; as the method name.
293+
* <p>This method delegates to
294+
* {@link #invokeSetterMethod(Object, String, Object, Class)}, supplying
295+
* {@code null} for the parameter type.
300296
* @param target the target object on which to invoke the specified setter
301297
* method
302298
* @param name the name of the setter method to invoke or the corresponding
303299
* property name
304300
* @param value the value to provide to the setter method
305-
* @see ReflectionUtils#findMethod(Class, String, Class[])
306-
* @see ReflectionUtils#makeAccessible(Method)
307-
* @see ReflectionUtils#invokeMethod(Method, Object, Object[])
308301
*/
309302
public static void invokeSetterMethod(Object target, String name, Object value) {
310303
invokeSetterMethod(target, name, value, null);
@@ -317,19 +310,24 @@ public static void invokeSetterMethod(Object target, String name, Object value)
317310
* method. In addition, an attempt will be made to make non-{@code public}
318311
* methods <em>accessible</em>, thus allowing one to invoke {@code protected},
319312
* {@code private}, and <em>package-private</em> setter methods.
320-
* <p>In addition, this method supports JavaBean-style <em>property</em>
321-
* names. For example, if you wish to set the {@code name} property on the
322-
* target object, you may pass either &quot;name&quot; or
323-
* &quot;setName&quot; as the method name.
313+
* <p>This method also supports JavaBean-style <em>property</em> names. For
314+
* example, if you wish to set the {@code name} property on the target object,
315+
* you may pass either {@code "name"} or {@code "setName"} as the method name.
316+
* <p>As of Spring Framework 6.2, if the supplied target object is a CGLIB
317+
* proxy which does not intercept the setter method, the proxy will be
318+
* {@linkplain AopTestUtils#getUltimateTargetObject unwrapped} allowing the
319+
* setter method to be invoked directly on the ultimate target of the proxy.
324320
* @param target the target object on which to invoke the specified setter
325321
* method
326322
* @param name the name of the setter method to invoke or the corresponding
327323
* property name
328324
* @param value the value to provide to the setter method
329325
* @param type the formal parameter type declared by the setter method
326+
* (may be {@code null} to indicate any type)
330327
* @see ReflectionUtils#findMethod(Class, String, Class[])
331328
* @see ReflectionUtils#makeAccessible(Method)
332329
* @see ReflectionUtils#invokeMethod(Method, Object, Object[])
330+
* @see AopTestUtils#getUltimateTargetObject(Object)
333331
*/
334332
public static void invokeSetterMethod(Object target, String name, @Nullable Object value, @Nullable Class<?> type) {
335333
Assert.notNull(target, "Target object must not be null");
@@ -357,6 +355,14 @@ public static void invokeSetterMethod(Object target, String name, @Nullable Obje
357355
safeToString(target), value));
358356
}
359357

358+
if (springAopPresent) {
359+
// If the target is a CGLIB proxy which does not intercept the method, invoke the
360+
// method on the ultimate target.
361+
if (isCglibProxyThatDoesNotInterceptMethod(target, method)) {
362+
target = AopTestUtils.getUltimateTargetObject(target);
363+
}
364+
}
365+
360366
ReflectionUtils.makeAccessible(method);
361367
ReflectionUtils.invokeMethod(method, target, value);
362368
}
@@ -368,10 +374,13 @@ public static void invokeSetterMethod(Object target, String name, @Nullable Obje
368374
* method. In addition, an attempt will be made to make non-{@code public}
369375
* methods <em>accessible</em>, thus allowing one to invoke {@code protected},
370376
* {@code private}, and <em>package-private</em> getter methods.
371-
* <p>In addition, this method supports JavaBean-style <em>property</em>
372-
* names. For example, if you wish to get the {@code name} property on the
373-
* target object, you may pass either &quot;name&quot; or
374-
* &quot;getName&quot; as the method name.
377+
* <p>This method also supports JavaBean-style <em>property</em> names. For
378+
* example, if you wish to get the {@code name} property on the target object,
379+
* you may pass either {@code "name"} or {@code "getName"} as the method name.
380+
* <p>As of Spring Framework 6.2, if the supplied target object is a CGLIB
381+
* proxy which does not intercept the getter method, the proxy will be
382+
* {@linkplain AopTestUtils#getUltimateTargetObject unwrapped} allowing the
383+
* getter method to be invoked directly on the ultimate target of the proxy.
375384
* @param target the target object on which to invoke the specified getter
376385
* method
377386
* @param name the name of the getter method to invoke or the corresponding
@@ -380,6 +389,7 @@ public static void invokeSetterMethod(Object target, String name, @Nullable Obje
380389
* @see ReflectionUtils#findMethod(Class, String, Class[])
381390
* @see ReflectionUtils#makeAccessible(Method)
382391
* @see ReflectionUtils#invokeMethod(Method, Object, Object[])
392+
* @see AopTestUtils#getUltimateTargetObject(Object)
383393
*/
384394
@Nullable
385395
public static Object invokeGetterMethod(Object target, String name) {
@@ -400,6 +410,14 @@ public static Object invokeGetterMethod(Object target, String name) {
400410
"Could not find getter method '%s' on %s", getterMethodName, safeToString(target)));
401411
}
402412

413+
if (springAopPresent) {
414+
// If the target is a CGLIB proxy which does not intercept the method, invoke the
415+
// method on the ultimate target.
416+
if (isCglibProxyThatDoesNotInterceptMethod(target, method)) {
417+
target = AopTestUtils.getUltimateTargetObject(target);
418+
}
419+
}
420+
403421
if (logger.isDebugEnabled()) {
404422
logger.debug(String.format("Invoking getter method '%s' on %s", getterMethodName, safeToString(target)));
405423
}
@@ -418,10 +436,6 @@ public static Object invokeGetterMethod(Object target, String name) {
418436
* @return the invocation result, if any
419437
* @see #invokeMethod(Class, String, Object...)
420438
* @see #invokeMethod(Object, Class, String, Object...)
421-
* @see MethodInvoker
422-
* @see ReflectionUtils#makeAccessible(Method)
423-
* @see ReflectionUtils#invokeMethod(Method, Object, Object[])
424-
* @see ReflectionUtils#handleReflectionException(Exception)
425439
*/
426440
@Nullable
427441
public static <T> T invokeMethod(Object target, String name, Object... args) {
@@ -441,10 +455,6 @@ public static <T> T invokeMethod(Object target, String name, Object... args) {
441455
* @since 5.2
442456
* @see #invokeMethod(Object, String, Object...)
443457
* @see #invokeMethod(Object, Class, String, Object...)
444-
* @see MethodInvoker
445-
* @see ReflectionUtils#makeAccessible(Method)
446-
* @see ReflectionUtils#invokeMethod(Method, Object, Object[])
447-
* @see ReflectionUtils#handleReflectionException(Exception)
448458
*/
449459
@Nullable
450460
public static <T> T invokeMethod(Class<?> targetClass, String name, Object... args) {
@@ -459,6 +469,10 @@ public static <T> T invokeMethod(Class<?> targetClass, String name, Object... ar
459469
* method. In addition, an attempt will be made to make non-{@code public}
460470
* methods <em>accessible</em>, thus allowing one to invoke {@code protected},
461471
* {@code private}, and <em>package-private</em> methods.
472+
* <p>As of Spring Framework 6.2, if the supplied target object is a CGLIB
473+
* proxy which does not intercept the method, the proxy will be
474+
* {@linkplain AopTestUtils#getUltimateTargetObject unwrapped} allowing the
475+
* method to be invoked directly on the ultimate target of the proxy.
462476
* @param targetObject the target object on which to invoke the method; may
463477
* be {@code null} if the method is static
464478
* @param targetClass the target class on which to invoke the method; may
@@ -471,8 +485,8 @@ public static <T> T invokeMethod(Class<?> targetClass, String name, Object... ar
471485
* @see #invokeMethod(Class, String, Object...)
472486
* @see MethodInvoker
473487
* @see ReflectionUtils#makeAccessible(Method)
474-
* @see ReflectionUtils#invokeMethod(Method, Object, Object[])
475488
* @see ReflectionUtils#handleReflectionException(Exception)
489+
* @see AopTestUtils#getUltimateTargetObject(Object)
476490
*/
477491
@SuppressWarnings("unchecked")
478492
@Nullable
@@ -493,6 +507,15 @@ public static <T> T invokeMethod(@Nullable Object targetObject, @Nullable Class<
493507
methodInvoker.setArguments(args);
494508
methodInvoker.prepare();
495509

510+
if (targetObject != null && springAopPresent) {
511+
// If the target is a CGLIB proxy which does not intercept the method, invoke the
512+
// method on the ultimate target.
513+
if (isCglibProxyThatDoesNotInterceptMethod(targetObject, methodInvoker.getPreparedMethod())) {
514+
targetObject = AopTestUtils.getUltimateTargetObject(targetObject);
515+
methodInvoker.setTargetObject(targetObject);
516+
}
517+
}
518+
496519
if (logger.isDebugEnabled()) {
497520
logger.debug(String.format("Invoking method '%s' on %s or %s with arguments %s", name,
498521
safeToString(targetObject), safeToString(targetClass), ObjectUtils.nullSafeToString(args)));
@@ -520,4 +543,13 @@ private static String safeToString(@Nullable Class<?> clazz) {
520543
return String.format("target class [%s]", (clazz != null ? clazz.getName() : null));
521544
}
522545

546+
/**
547+
* Determine if the supplied target object is a CBLIB proxy that does not intercept the
548+
* supplied method.
549+
* @since 6.2
550+
*/
551+
private static boolean isCglibProxyThatDoesNotInterceptMethod(Object target, Method method) {
552+
return (AopUtils.isCglibProxy(target) && !method.getDeclaringClass().equals(target.getClass()));
553+
}
554+
523555
}

spring-test/src/test/java/org/springframework/test/util/ReflectionTestUtilsTests.java

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import static org.assertj.core.api.Assertions.assertThat;
3333
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
3434
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
35+
import static org.assertj.core.api.SoftAssertions.assertSoftly;
3536
import static org.springframework.test.util.ReflectionTestUtils.getField;
3637
import static org.springframework.test.util.ReflectionTestUtils.invokeGetterMethod;
3738
import static org.springframework.test.util.ReflectionTestUtils.invokeMethod;
@@ -48,7 +49,7 @@ class ReflectionTestUtilsTests {
4849

4950
private static final Float PI = (float) 22 / 7;
5051

51-
private final Person person = new PersonEntity();
52+
private final PersonEntity person = new PersonEntity();
5253

5354
private final Component component = new Component();
5455

@@ -156,7 +157,7 @@ private static void assertSetFieldAndGetFieldBehavior(Person person) {
156157
assertThat(person.getAge()).as("age (private field)").isEqualTo(42);
157158
assertThat(person.getEyeColor()).as("eye color (package private field)").isEqualTo("blue");
158159
assertThat(person.likesPets()).as("'likes pets' flag (package private boolean field)").isTrue();
159-
assertThat(person.getFavoriteNumber()).as("'favorite number' (package field)").isEqualTo(PI);
160+
assertThat(person.getFavoriteNumber()).as("'favorite number' (private field)").isEqualTo(PI);
160161
}
161162

162163
private static void assertSetFieldAndGetFieldBehaviorForProxy(Person proxy, Person target) {
@@ -168,7 +169,7 @@ private static void assertSetFieldAndGetFieldBehaviorForProxy(Person proxy, Pers
168169
assertThat(target.getAge()).as("age (private field)").isEqualTo(42);
169170
assertThat(target.getEyeColor()).as("eye color (package private field)").isEqualTo("blue");
170171
assertThat(target.likesPets()).as("'likes pets' flag (package private boolean field)").isTrue();
171-
assertThat(target.getFavoriteNumber()).as("'favorite number' (package field)").isEqualTo(PI);
172+
assertThat(target.getFavoriteNumber()).as("'favorite number' (private field)").isEqualTo(PI);
172173
}
173174

174175
@Test
@@ -188,7 +189,7 @@ void setFieldWithNullValuesForNonPrimitives() {
188189

189190
assertThat(person.getName()).as("name (protected field)").isNull();
190191
assertThat(person.getEyeColor()).as("eye color (package private field)").isNull();
191-
assertThat(person.getFavoriteNumber()).as("'favorite number' (package field)").isNull();
192+
assertThat(person.getFavoriteNumber()).as("'favorite number' (private field)").isNull();
192193
}
193194

194195
@Test
@@ -295,6 +296,22 @@ void invokeSetterMethodAndInvokeGetterMethodWithJavaBeanPropertyNames() {
295296
assertThat(invokeGetterMethod(person, "favoriteNumber")).isEqualTo(PI);
296297
}
297298

299+
@Test // gh-33429
300+
void invokingPrivateGetterMethodViaCglibProxyInvokesMethodOnUltimateTarget() {
301+
this.person.setPrivateEye("I");
302+
ProxyFactory pf = new ProxyFactory(this.person);
303+
pf.setProxyTargetClass(true);
304+
PersonEntity proxy = (PersonEntity) pf.getProxy();
305+
assertThat(AopUtils.isCglibProxy(proxy)).as("Proxy is a CGLIB proxy").isTrue();
306+
307+
assertSoftly(softly -> {
308+
softly.assertThat(getField(this.person, "privateEye")).as("'privateEye' (private field in target)").isEqualTo("I");
309+
softly.assertThat(getField(proxy, "privateEye")).as("'privateEye' (private field in proxy)").isEqualTo("I");
310+
softly.assertThat(invokeGetterMethod(this.person, "privateEye")).as("'privateEye' (getter on target)").isEqualTo("I");
311+
softly.assertThat(invokeGetterMethod(proxy, "privateEye")).as("'privateEye' (getter on proxy)").isEqualTo("I");
312+
});
313+
}
314+
298315
@Test
299316
void invokeSetterMethodWithNullValuesForNonPrimitives() {
300317
invokeSetterMethod(person, "name", null, String.class);
@@ -306,6 +323,41 @@ void invokeSetterMethodWithNullValuesForNonPrimitives() {
306323
assertThat(person.getFavoriteNumber()).as("'favorite number' (protected method for a Number)").isNull();
307324
}
308325

326+
@Test // gh-33429
327+
void invokingPrivateSetterMethodViaCglibProxyInvokesMethodOnUltimateTarget() {
328+
ProxyFactory pf = new ProxyFactory(this.person);
329+
pf.setProxyTargetClass(true);
330+
PersonEntity proxy = (PersonEntity) pf.getProxy();
331+
assertThat(AopUtils.isCglibProxy(proxy)).as("Proxy is a CGLIB proxy").isTrue();
332+
333+
// Set reflectively
334+
invokeSetterMethod(proxy, "favoriteNumber", PI, Number.class);
335+
336+
assertSoftly(softly -> {
337+
softly.assertThat(getField(proxy, "favoriteNumber")).as("'favorite number' (private field)").isEqualTo(PI);
338+
softly.assertThat(proxy.getFavoriteNumber()).as("'favorite number' (getter on proxy)").isEqualTo(PI);
339+
softly.assertThat(this.person.getFavoriteNumber()).as("'favorite number' (getter on target)").isEqualTo(PI);
340+
});
341+
}
342+
343+
@Test // gh-33429
344+
void invokingFinalSetterMethodViaCglibProxyInvokesMethodOnUltimateTarget() {
345+
ProxyFactory pf = new ProxyFactory(this.person);
346+
pf.setProxyTargetClass(true);
347+
PersonEntity proxy = (PersonEntity) pf.getProxy();
348+
assertThat(AopUtils.isCglibProxy(proxy)).as("Proxy is a CGLIB proxy").isTrue();
349+
assertThat(proxy.getPuzzle()).as("puzzle").isNull();
350+
351+
// Set reflectively
352+
invokeSetterMethod(proxy, "puzzle", "enigma", String.class);
353+
354+
assertSoftly(softly -> {
355+
softly.assertThat(getField(proxy, "puzzle")).as("'puzzle' (private field)").isEqualTo("enigma");
356+
softly.assertThat(proxy.getPuzzle()).as("'puzzle' (getter on proxy)").isEqualTo("enigma");
357+
softly.assertThat(this.person.getPuzzle()).as("'puzzle' (getter on target)").isEqualTo("enigma");
358+
});
359+
}
360+
309361
@Test
310362
void invokeSetterMethodWithNullValueForPrimitiveLong() {
311363
assertThatIllegalArgumentException().isThrownBy(() -> invokeSetterMethod(person, "id", null, long.class));
@@ -362,6 +414,23 @@ void invokeMethodSimulatingLifecycleEvents() {
362414
assertThat(component.getText()).as("text").isNull();
363415
}
364416

417+
@Test // gh-33429
418+
void invokingPrivateMethodViaCglibProxyInvokesMethodOnUltimateTarget() {
419+
ProxyFactory pf = new ProxyFactory(this.person);
420+
pf.setProxyTargetClass(true);
421+
PersonEntity proxy = (PersonEntity) pf.getProxy();
422+
assertThat(AopUtils.isCglibProxy(proxy)).as("Proxy is a CGLIB proxy").isTrue();
423+
424+
// Set reflectively
425+
invokeMethod(proxy, "setFavoriteNumber", PI);
426+
427+
assertSoftly(softly -> {
428+
softly.assertThat(getField(proxy, "favoriteNumber")).as("'favorite number' (private field)").isEqualTo(PI);
429+
softly.assertThat(proxy.getFavoriteNumber()).as("'favorite number' (getter on proxy)").isEqualTo(PI);
430+
softly.assertThat(this.person.getFavoriteNumber()).as("'favorite number' (getter on target)").isEqualTo(PI);
431+
});
432+
}
433+
365434
@Test
366435
void invokeInitMethodBeforeAutowiring() {
367436
assertThatIllegalStateException()

0 commit comments

Comments
 (0)