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
new file mode 100644
index 000000000000..f49b30551c08
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/util/AopTestUtils.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2002-2015 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;
+
+import org.springframework.aop.framework.Advised;
+import org.springframework.aop.support.AopUtils;
+
+/**
+ * {@code AopTestUtils} is a collection of AOP-related utility methods for
+ * use in unit and integration testing scenarios.
+ *
+ *
For Spring's core AOP utilities, see
+ * {@link org.springframework.aop.support.AopUtils AopUtils} and
+ * {@link org.springframework.aop.framework.AopProxyUtils AopProxyUtils}.
+ *
+ * @author Sam Brannen
+ * @since 4.2
+ * @see org.springframework.aop.support.AopUtils
+ * @see org.springframework.aop.framework.AopProxyUtils
+ */
+public class AopTestUtils {
+
+ /**
+ * Get the target object of the supplied {@code candidate} object.
+ *
If the supplied {@code candidate} is a Spring
+ * {@linkplain AopUtils#isAopProxy proxy}, the target of the proxy will
+ * be returned; otherwise, the {@code candidate} will be returned
+ * as is.
+ *
+ * @param candidate the instance to check (potentially a Spring AOP proxy)
+ * @return the target object or the {@code candidate}; never {@code null}
+ * @throws IllegalStateException if an error occurs while unwrapping a proxy
+ * @see Advised#getTargetSource()
+ * @see #getUltimateTargetObject
+ */
+ @SuppressWarnings("unchecked")
+ public static T getTargetObject(Object candidate) {
+ try {
+ if (AopUtils.isAopProxy(candidate) && (candidate instanceof Advised)) {
+ return (T) ((Advised) candidate).getTargetSource().getTarget();
+ }
+ }
+ catch (Exception e) {
+ throw new IllegalStateException("Failed to unwrap proxied object.", e);
+ }
+
+ // else
+ return (T) candidate;
+ }
+
+ /**
+ * Get the ultimate target object of the supplied {@code candidate}
+ * object, unwrapping not only a top-level proxy but also any number of
+ * nested proxies.
+ * If the supplied {@code candidate} is a Spring
+ * {@linkplain AopUtils#isAopProxy proxy}, the ultimate target of all
+ * nested proxies will be returned; otherwise, the {@code candidate}
+ * will be returned as is.
+ *
+ * @param candidate the instance to check (potentially a Spring AOP proxy)
+ * @return the ultimate target object or the {@code candidate}; never
+ * {@code null}
+ * @throws IllegalStateException if an error occurs while unwrapping a proxy
+ * @see Advised#getTargetSource()
+ * @see org.springframework.aop.framework.AopProxyUtils#ultimateTargetClass
+ */
+ @SuppressWarnings("unchecked")
+ public static T getUltimateTargetObject(Object candidate) {
+ try {
+ if (AopUtils.isAopProxy(candidate) && (candidate instanceof Advised)) {
+ return (T) getUltimateTargetObject(((Advised) candidate).getTargetSource().getTarget());
+ }
+ }
+ catch (Exception e) {
+ throw new IllegalStateException("Failed to unwrap proxied object.", e);
+ }
+
+ // else
+ return (T) candidate;
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/util/AopTestUtilsTests.java b/spring-test/src/test/java/org/springframework/test/util/AopTestUtilsTests.java
new file mode 100644
index 000000000000..21070c5646ac
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/util/AopTestUtilsTests.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2002-2015 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;
+
+import org.junit.Test;
+
+import org.springframework.aop.framework.ProxyFactory;
+import org.springframework.aop.support.AopUtils;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+import static org.springframework.test.util.AopTestUtils.*;
+
+/**
+ * Unit tests for {@link AopTestUtils}.
+ *
+ * @author Sam Brannen
+ * @since 4.2
+ */
+public class AopTestUtilsTests {
+
+ private final FooImpl foo = new FooImpl();
+
+
+ @Test
+ public void getTargetObjectForNonProxiedObject() {
+ Foo target = getTargetObject(foo);
+ assertSame(foo, target);
+ }
+
+ @Test
+ public void getTargetObjectWrappedInSingleJdkDynamicProxy() {
+ Foo target = getTargetObject(jdkProxy(foo));
+ assertSame(foo, target);
+ }
+
+ @Test
+ public void getTargetObjectWrappedInSingleCglibProxy() {
+ Foo target = getTargetObject(cglibProxy(foo));
+ assertSame(foo, target);
+ }
+
+ @Test
+ public void getTargetObjectWrappedInDoubleJdkDynamicProxy() {
+ Foo target = getTargetObject(jdkProxy(jdkProxy(foo)));
+ assertNotSame(foo, target);
+ }
+
+ @Test
+ public void getTargetObjectWrappedInDoubleCglibProxy() {
+ Foo target = getTargetObject(cglibProxy(cglibProxy(foo)));
+ assertNotSame(foo, target);
+ }
+
+ @Test
+ public void getUltimateTargetObjectForNonProxiedObject() {
+ Foo target = getUltimateTargetObject(foo);
+ assertSame(foo, target);
+ }
+
+ @Test
+ public void getUltimateTargetObjectWrappedInSingleJdkDynamicProxy() {
+ Foo target = getUltimateTargetObject(jdkProxy(foo));
+ assertSame(foo, target);
+ }
+
+ @Test
+ public void getUltimateTargetObjectWrappedInSingleCglibProxy() {
+ Foo target = getUltimateTargetObject(cglibProxy(foo));
+ assertSame(foo, target);
+ }
+
+ @Test
+ public void getUltimateTargetObjectWrappedInDoubleJdkDynamicProxy() {
+ Foo target = getUltimateTargetObject(jdkProxy(jdkProxy(foo)));
+ assertSame(foo, target);
+ }
+
+ @Test
+ public void getUltimateTargetObjectWrappedInDoubleCglibProxy() {
+ Foo target = getUltimateTargetObject(cglibProxy(cglibProxy(foo)));
+ assertSame(foo, target);
+ }
+
+ @Test
+ public void getUltimateTargetObjectWrappedInCglibProxyWrappedInJdkDynamicProxy() {
+ Foo target = getUltimateTargetObject(jdkProxy(cglibProxy(foo)));
+ assertSame(foo, target);
+ }
+
+ @Test
+ public void getUltimateTargetObjectWrappedInCglibProxyWrappedInDoubleJdkDynamicProxy() {
+ Foo target = getUltimateTargetObject(jdkProxy(jdkProxy(cglibProxy(foo))));
+ assertSame(foo, target);
+ }
+
+ private Foo jdkProxy(Foo foo) {
+ ProxyFactory pf = new ProxyFactory();
+ pf.setTarget(foo);
+ pf.addInterface(Foo.class);
+ Foo proxy = (Foo) pf.getProxy();
+ assertTrue("Proxy is a JDK dynamic proxy", AopUtils.isJdkDynamicProxy(proxy));
+ assertThat(proxy, instanceOf(Foo.class));
+ return proxy;
+ }
+
+ private Foo cglibProxy(Foo foo) {
+ ProxyFactory pf = new ProxyFactory();
+ pf.setTarget(foo);
+ pf.setProxyTargetClass(true);
+ Foo proxy = (Foo) pf.getProxy();
+ assertTrue("Proxy is a CGLIB proxy", AopUtils.isCglibProxy(proxy));
+ assertThat(proxy, instanceOf(FooImpl.class));
+ return proxy;
+ }
+
+
+ static interface Foo {
+ }
+
+ static class FooImpl implements Foo {
+ }
+
+}