diff --git a/spring-test/src/main/java/org/springframework/test/util/AopTestUtils.java b/spring-test/src/main/java/org/springframework/test/util/AopTestUtils.java index 4eda8a45ac2..7660ee2cef8 100644 --- a/spring-test/src/main/java/org/springframework/test/util/AopTestUtils.java +++ b/spring-test/src/main/java/org/springframework/test/util/AopTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -32,6 +32,7 @@ import org.springframework.util.Assert; * @since 4.2 * @see org.springframework.aop.support.AopUtils * @see org.springframework.aop.framework.AopProxyUtils + * @see ReflectionTestUtils */ public class AopTestUtils { 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 f40227fb8c8..378f6628482 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-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -60,6 +60,7 @@ import org.springframework.util.StringUtils; * @author Juergen Hoeller * @since 2.5 * @see ReflectionUtils + * @see AopTestUtils */ public class ReflectionTestUtils { @@ -137,6 +138,9 @@ public class ReflectionTestUtils { * Set the {@linkplain Field field} with the given {@code name}/{@code type} * on the provided {@code targetObject}/{@code targetClass} to the supplied * {@code value}. + *
If the supplied {@code targetObject} is a proxy, it will + * be {@linkplain AopTestUtils#getUltimateTargetObject unwrapped} allowing + * the field to be set on the ultimate target of the proxy. *
This method traverses the class hierarchy in search of the desired * field. In addition, an attempt will be made to make non-{@code public} * fields accessible, thus allowing one to set {@code protected}, @@ -153,30 +157,33 @@ public class ReflectionTestUtils { * @see ReflectionUtils#findField(Class, String, Class) * @see ReflectionUtils#makeAccessible(Field) * @see ReflectionUtils#setField(Field, Object, Object) + * @see AopTestUtils#getUltimateTargetObject(Object) * @since 4.2 */ public static void setField(Object targetObject, Class> targetClass, String name, Object value, Class> type) { Assert.isTrue(targetObject != null || targetClass != null, "Either targetObject or targetClass for the field must be specified"); + Object ultimateTarget = (targetObject != null ? AopTestUtils.getUltimateTargetObject(targetObject) : null); + if (targetClass == null) { - targetClass = targetObject.getClass(); + targetClass = ultimateTarget.getClass(); } Field field = ReflectionUtils.findField(targetClass, name, type); if (field == null) { throw new IllegalArgumentException(String.format( "Could not find field '%s' of type [%s] on target object [%s] or target class [%s]", name, type, - targetObject, targetClass)); + ultimateTarget, targetClass)); } if (logger.isDebugEnabled()) { logger.debug(String.format( "Setting field '%s' of type [%s] on target object [%s] or target class [%s] to value [%s]", name, type, - targetObject, targetClass, value)); + ultimateTarget, targetClass, value)); } ReflectionUtils.makeAccessible(field); - ReflectionUtils.setField(field, targetObject, value); + ReflectionUtils.setField(field, ultimateTarget, value); } /** @@ -213,6 +220,9 @@ public class ReflectionTestUtils { /** * Get the value of the {@linkplain Field field} with the given {@code name} * from the provided {@code targetObject}/{@code targetClass}. + *
If the supplied {@code targetObject} is a proxy, it will + * be {@linkplain AopTestUtils#getUltimateTargetObject unwrapped} allowing + * the field to be retrieved from the ultimate target of the proxy. *
This method traverses the class hierarchy in search of the desired * field. In addition, an attempt will be made to make non-{@code public} * fields accessible, thus allowing one to get {@code protected}, @@ -234,23 +244,25 @@ public class ReflectionTestUtils { Assert.isTrue(targetObject != null || targetClass != null, "Either targetObject or targetClass for the field must be specified"); + Object ultimateTarget = (targetObject != null ? AopTestUtils.getUltimateTargetObject(targetObject) : null); + if (targetClass == null) { - targetClass = targetObject.getClass(); + targetClass = ultimateTarget.getClass(); } Field field = ReflectionUtils.findField(targetClass, name); if (field == null) { throw new IllegalArgumentException( String.format("Could not find field '%s' on target object [%s] or target class [%s]", name, - targetObject, targetClass)); + ultimateTarget, targetClass)); } if (logger.isDebugEnabled()) { logger.debug(String.format("Getting field '%s' from target object [%s] or target class [%s]", name, - targetObject, targetClass)); + ultimateTarget, targetClass)); } ReflectionUtils.makeAccessible(field); - return ReflectionUtils.getField(field, targetObject); + return ReflectionUtils.getField(field, ultimateTarget); } /** 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 fa692ed9b12..0216e362f75 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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,9 +22,12 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.AopUtils; import org.springframework.test.util.subpackage.Component; import org.springframework.test.util.subpackage.LegacyEntity; import org.springframework.test.util.subpackage.Person; +import org.springframework.test.util.subpackage.PersonEntity; import org.springframework.test.util.subpackage.StaticFields; import static org.hamcrest.CoreMatchers.*; @@ -41,12 +44,12 @@ public class ReflectionTestUtilsTests { private static final Float PI = new Float((float) 22 / 7); - private final Person person = new Person(); + private final Person person = new PersonEntity(); private final Component component = new Component(); @Rule - public ExpectedException exception = ExpectedException.none(); + public final ExpectedException exception = ExpectedException.none(); @Before @@ -54,7 +57,6 @@ public class ReflectionTestUtilsTests { StaticFields.reset(); } - @Test public void setFieldWithNullTargetObject() throws Exception { exception.expect(IllegalArgumentException.class); @@ -106,6 +108,29 @@ public class ReflectionTestUtilsTests { @Test public void setFieldAndGetFieldForStandardUseCases() throws Exception { + assertSetFieldAndGetFieldBehavior(this.person); + } + + @Test + public void setFieldAndGetFieldViaJdkDynamicProxy() throws Exception { + ProxyFactory pf = new ProxyFactory(this.person); + pf.addInterface(Person.class); + Person proxy = (Person) pf.getProxy(); + assertTrue("Proxy is a JDK dynamic proxy", AopUtils.isJdkDynamicProxy(proxy)); + assertSetFieldAndGetFieldBehaviorForProxy(proxy, this.person); + } + + @Test + public void setFieldAndGetFieldViaCglibProxy() throws Exception { + ProxyFactory pf = new ProxyFactory(this.person); + pf.setProxyTargetClass(true); + Person proxy = (Person) pf.getProxy(); + assertTrue("Proxy is a CGLIB proxy", AopUtils.isCglibProxy(proxy)); + assertSetFieldAndGetFieldBehaviorForProxy(proxy, this.person); + } + + private static void assertSetFieldAndGetFieldBehavior(Person person) { + // Set reflectively setField(person, "id", new Long(99), long.class); setField(person, "name", "Tom"); setField(person, "age", new Integer(42)); @@ -113,19 +138,33 @@ public class ReflectionTestUtilsTests { setField(person, "likesPets", Boolean.TRUE); setField(person, "favoriteNumber", PI, Number.class); + // Get reflectively + assertEquals(new Long(99), getField(person, "id")); + assertEquals("Tom", getField(person, "name")); + assertEquals(new Integer(42), getField(person, "age")); + assertEquals("blue", getField(person, "eyeColor")); + assertEquals(Boolean.TRUE, getField(person, "likesPets")); + assertEquals(PI, getField(person, "favoriteNumber")); + + // Get directly assertEquals("ID (private field in a superclass)", 99, person.getId()); assertEquals("name (protected field)", "Tom", person.getName()); assertEquals("age (private field)", 42, person.getAge()); assertEquals("eye color (package private field)", "blue", person.getEyeColor()); assertEquals("'likes pets' flag (package private boolean field)", true, person.likesPets()); assertEquals("'favorite number' (package field)", PI, person.getFavoriteNumber()); + } - assertEquals(new Long(99), getField(person, "id")); - assertEquals("Tom", getField(person, "name")); - assertEquals(new Integer(42), getField(person, "age")); - assertEquals("blue", getField(person, "eyeColor")); - assertEquals(Boolean.TRUE, getField(person, "likesPets")); - assertEquals(PI, getField(person, "favoriteNumber")); + private static void assertSetFieldAndGetFieldBehaviorForProxy(Person proxy, Person target) { + assertSetFieldAndGetFieldBehavior(proxy); + + // Get directly from Target + assertEquals("ID (private field in a superclass)", 99, target.getId()); + assertEquals("name (protected field)", "Tom", target.getName()); + assertEquals("age (private field)", 42, target.getAge()); + assertEquals("eye color (package private field)", "blue", target.getEyeColor()); + assertEquals("'likes pets' flag (package private boolean field)", true, target.likesPets()); + assertEquals("'favorite number' (package field)", PI, target.getFavoriteNumber()); } @Test diff --git a/spring-test/src/test/java/org/springframework/test/util/subpackage/PersistentEntity.java b/spring-test/src/test/java/org/springframework/test/util/subpackage/PersistentEntity.java index 3ff3d91dfd7..4f34e2e85b0 100644 --- a/spring-test/src/test/java/org/springframework/test/util/subpackage/PersistentEntity.java +++ b/spring-test/src/test/java/org/springframework/test/util/subpackage/PersistentEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2007-2011 the original author or authors. + * Copyright 2007-2016 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,11 +28,12 @@ public abstract class PersistentEntity { private long id; - public final long getId() { + public long getId() { return this.id; } - protected final void setId(long id) { + protected void setId(long id) { this.id = id; } + } diff --git a/spring-test/src/test/java/org/springframework/test/util/subpackage/Person.java b/spring-test/src/test/java/org/springframework/test/util/subpackage/Person.java index b89f56167fd..20177ed098b 100644 --- a/spring-test/src/test/java/org/springframework/test/util/subpackage/Person.java +++ b/spring-test/src/test/java/org/springframework/test/util/subpackage/Person.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 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. @@ -16,85 +16,27 @@ package org.springframework.test.util.subpackage; -import org.springframework.core.style.ToStringCreator; - /** - * Concrete subclass of {@link PersistentEntity} representing a person - * entity; intended for use in unit tests. + * Interface representing a person entity; intended for use in unit tests. + * + *
The introduction of an interface is necessary in order to test support for + * JDK dynamic proxies. * * @author Sam Brannen - * @since 2.5 + * @since 4.3 */ -public class Person extends PersistentEntity { - - protected String name; - - private int age; - - String eyeColor; - - boolean likesPets = false; - - private Number favoriteNumber; - - - public final String getName() { - return this.name; - } - - @SuppressWarnings("unused") - private final void setName(final String name) { - this.name = name; - } - - public final int getAge() { - return this.age; - } - - protected final void setAge(final int age) { - this.age = age; - } - - public final String getEyeColor() { - return this.eyeColor; - } - - final void setEyeColor(final String eyeColor) { - this.eyeColor = eyeColor; - } - - public final boolean likesPets() { - return this.likesPets; - } - - protected final void setLikesPets(final boolean likesPets) { - this.likesPets = likesPets; - } - - public final Number getFavoriteNumber() { - return this.favoriteNumber; - } - - protected final void setFavoriteNumber(Number favoriteNumber) { - this.favoriteNumber = favoriteNumber; - } - - @Override - public String toString() { - return new ToStringCreator(this) +public interface Person { - .append("id", this.getId()) + long getId(); - .append("name", this.name) + String getName(); - .append("age", this.age) + int getAge(); - .append("eyeColor", this.eyeColor) + String getEyeColor(); - .append("likesPets", this.likesPets) + boolean likesPets(); - .append("favoriteNumber", this.favoriteNumber) + Number getFavoriteNumber(); - .toString(); - } } 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 new file mode 100644 index 00000000000..1cdff47bec3 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/util/subpackage/PersonEntity.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.test.util.subpackage; + +import org.springframework.core.style.ToStringCreator; + +/** + * Concrete subclass of {@link PersistentEntity} representing a person + * entity; intended for use in unit tests. + * + * @author Sam Brannen + * @since 2.5 + */ +public class PersonEntity extends PersistentEntity implements Person { + + protected String name; + + private int age; + + String eyeColor; + + boolean likesPets = false; + + private Number favoriteNumber; + + + public String getName() { + return this.name; + } + + @SuppressWarnings("unused") + private void setName(final String name) { + this.name = name; + } + + public int getAge() { + return this.age; + } + + protected void setAge(final int age) { + this.age = age; + } + + public String getEyeColor() { + return this.eyeColor; + } + + void setEyeColor(final String eyeColor) { + this.eyeColor = eyeColor; + } + + public boolean likesPets() { + return this.likesPets; + } + + protected void setLikesPets(final boolean likesPets) { + this.likesPets = likesPets; + } + + public Number getFavoriteNumber() { + return this.favoriteNumber; + } + + protected void setFavoriteNumber(Number favoriteNumber) { + this.favoriteNumber = favoriteNumber; + } + + @Override + public String toString() { + // @formatter:off + return new ToStringCreator(this) + .append("id", this.getId()) + .append("name", this.name) + .append("age", this.age) + .append("eyeColor", this.eyeColor) + .append("likesPets", this.likesPets) + .append("favoriteNumber", this.favoriteNumber) + .toString(); + // @formatter:on + } + +}