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
This commit is contained in:
Sam Brannen
2024-08-26 16:07:06 +02:00
parent 6d1294ddad
commit 38a56b3fda
3 changed files with 164 additions and 39 deletions

View File

@@ -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()

View File

@@ -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
}