Skip to content

Commit 8e9f9fb

Browse files
committed
WW-4800 Executes aspects when chaining AOPed actions
1 parent 2fb431d commit 8e9f9fb

File tree

10 files changed

+204
-52
lines changed

10 files changed

+204
-52
lines changed

core/src/main/java/com/opensymphony/xwork2/interceptor/ChainingInterceptor.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,11 @@ private void copyStack(ActionInvocation invocation, CompoundRoot root) {
163163
for (Object object : list) {
164164
if (shouldCopy(object)) {
165165
Object action = invocation.getAction();
166+
Class<?> editable = null;
166167
if(ProxyUtil.isSpringAopProxy(action)) {
167-
action = ProxyUtil.getSpringUltimateTargetObject(action);
168+
editable = ProxyUtil.springUltimateTargetClass(action);
168169
}
169-
reflectionProvider.copy(object, action, ctxMap, prepareExcludes(), includes);
170+
reflectionProvider.copy(object, action, ctxMap, prepareExcludes(), includes, editable);
170171
}
171172
}
172173
}

core/src/main/java/com/opensymphony/xwork2/ognl/OgnlReflectionProvider.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,12 @@ public PropertyDescriptor getPropertyDescriptor(Class targetClass,
7070

7171
public void copy(Object from, Object to, Map<String, Object> context,
7272
Collection<String> exclusions, Collection<String> inclusions) {
73-
ognlUtil.copy(from, to, context, exclusions, inclusions);
73+
copy(from, to, context, exclusions, inclusions, null);
74+
}
75+
76+
public void copy(Object from, Object to, Map<String, Object> context,
77+
Collection<String> exclusions, Collection<String> inclusions, Class<?> editable) {
78+
ognlUtil.copy(from, to, context, exclusions, inclusions, editable);
7479
}
7580

7681
public Object getRealTarget(String property, Map<String, Object> context, Object root)

core/src/main/java/com/opensymphony/xwork2/ognl/OgnlUtil.java

+24-1
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,24 @@ private void checkSimpleMethod(Object tree, Map<String, Object> context) throws
450450
* note if exclusions AND inclusions are supplied and not null nothing will get copied.
451451
*/
452452
public void copy(final Object from, final Object to, final Map<String, Object> context, Collection<String> exclusions, Collection<String> inclusions) {
453+
copy(from, to, context, exclusions, inclusions, null);
454+
}
455+
456+
/**
457+
* Copies the properties in the object "from" and sets them in the object "to"
458+
* only setting properties defined in the given "editable" class (or interface)
459+
* using specified type converter, or {@link com.opensymphony.xwork2.conversion.impl.XWorkConverter} if none
460+
* is specified.
461+
*
462+
* @param from the source object
463+
* @param to the target object
464+
* @param context the action context we're running under
465+
* @param exclusions collection of method names to excluded from copying ( can be null)
466+
* @param inclusions collection of method names to included copying (can be null)
467+
* note if exclusions AND inclusions are supplied and not null nothing will get copied.
468+
* @param editable the class (or interface) to restrict property setting to
469+
*/
470+
public void copy(final Object from, final Object to, final Map<String, Object> context, Collection<String> exclusions, Collection<String> inclusions, Class<?> editable) {
453471
if (from == null || to == null) {
454472
LOG.warn("Attempting to copy from or to a null source. This is illegal and is bein skipped. This may be due to an error in an OGNL expression, action chaining, or some other event.");
455473
return;
@@ -466,7 +484,12 @@ public void copy(final Object from, final Object to, final Map<String, Object> c
466484

467485
try {
468486
fromPds = getPropertyDescriptors(from);
469-
toPds = getPropertyDescriptors(to);
487+
if (editable != null) {
488+
toPds = getPropertyDescriptors(editable);
489+
}
490+
else {
491+
toPds = getPropertyDescriptors(to);
492+
}
470493
} catch (IntrospectionException e) {
471494
LOG.error("An error occurred", e);
472495
return;

core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java

+41-20
Original file line numberDiff line numberDiff line change
@@ -29,31 +29,32 @@
2929
public class ProxyUtil {
3030
private static final String SPRING_ADVISED_CLASS_NAME = "org.springframework.aop.framework.Advised";
3131
private static final String SPRING_SPRINGPROXY_CLASS_NAME = "org.springframework.aop.SpringProxy";
32+
private static final String SPRING_SINGLETONTARGETSOURCE_CLASS_NAME = "org.springframework.aop.target.SingletonTargetSource";
33+
private static final String SPRING_TARGETCLASSAWARE_CLASS_NAME = "org.springframework.aop.TargetClassAware";
3234

3335
/**
34-
* Get the ultimate <em>target</em> object of the supplied {@code candidate}
35-
* object, unwrapping not only a top-level proxy but also any number of
36-
* nested proxies.
37-
* <p>If the supplied {@code candidate} is a Spring proxy, the ultimate target of all
38-
* nested proxies will be returned; otherwise, the {@code candidate}
39-
* will be returned <em>as is</em>.
40-
* @param candidate the instance to check (potentially a Spring AOP proxy;
41-
* never {@code null})
42-
* @return the target object or the {@code candidate} (never {@code null})
43-
* @throws IllegalStateException if an error occurs while unwrapping a proxy
36+
* Determine the ultimate target class of the given spring bean instance, traversing
37+
* not only a top-level spring proxy but any number of nested spring proxies as well &mdash;
38+
* as long as possible without side effects, that is, just for singleton targets.
39+
* @param candidate the instance to check (might be a spring AOP proxy)
40+
* @return the ultimate target class (or the plain class of the given
41+
* object as fallback; never {@code null})
4442
*/
45-
public static <T> T getSpringUltimateTargetObject(Object candidate) {
46-
try {
47-
if (isSpringAopProxy(candidate) && implementsInterface(candidate.getClass(), SPRING_ADVISED_CLASS_NAME)) {
48-
Object targetSource = MethodUtils.invokeMethod(candidate, "getTargetSource");
49-
Object target = MethodUtils.invokeMethod(targetSource, "getTarget");
50-
return getSpringUltimateTargetObject(target);
43+
public static Class<?> springUltimateTargetClass(Object candidate) {
44+
Object current = candidate;
45+
Class<?> result = null;
46+
while (null != current && implementsInterface(current.getClass(), SPRING_TARGETCLASSAWARE_CLASS_NAME)) {
47+
try {
48+
result = (Class<?>) MethodUtils.invokeMethod(current, "getTargetClass");
49+
} catch (Throwable ignored) {
5150
}
51+
current = getSingletonTarget(current);
5252
}
53-
catch (Throwable ex) {
54-
throw new IllegalStateException("Failed to unwrap proxied object", ex);
53+
if (result == null) {
54+
Class<?> clazz = candidate.getClass();
55+
result = (isCglibProxyClass(clazz) ? clazz.getSuperclass() : candidate.getClass());
5556
}
56-
return (T) candidate;
57+
return result;
5758
}
5859

5960
/**
@@ -66,6 +67,26 @@ public static boolean isSpringAopProxy(Object object) {
6667
|| isCglibProxyClass(clazz)));
6768
}
6869

70+
/**
71+
* Obtain the singleton target object behind the given spring proxy, if any.
72+
* @param candidate the (potential) spring proxy to check
73+
* @return the singleton target object, or {@code null} in any other case
74+
* (not a spring proxy, not an existing singleton target)
75+
*/
76+
private static Object getSingletonTarget(Object candidate) {
77+
try {
78+
if (implementsInterface(candidate.getClass(), SPRING_ADVISED_CLASS_NAME)) {
79+
Object targetSource = MethodUtils.invokeMethod(candidate, "getTargetSource");
80+
if (implementsInterface(targetSource.getClass(), SPRING_SINGLETONTARGETSOURCE_CLASS_NAME)) {
81+
return MethodUtils.invokeMethod(targetSource, "getTarget");
82+
}
83+
}
84+
} catch (Throwable ignored) {
85+
}
86+
87+
return null;
88+
}
89+
6990
/**
7091
* Check whether the specified class is a CGLIB-generated class.
7192
* @param clazz the class to check
@@ -81,7 +102,7 @@ private static boolean isCglibProxyClass(Class<?> clazz) {
81102
*/
82103
private static boolean implementsInterface(Class<?> clazz, String ifaceClassName) {
83104
try {
84-
Class ifaceClass = ClassLoaderUtil.loadClass(ifaceClassName, ProxyUtil.class);
105+
Class<?> ifaceClass = ClassLoaderUtil.loadClass(ifaceClassName, ProxyUtil.class);
85106
return ifaceClass.isAssignableFrom(clazz);
86107
} catch (ClassNotFoundException e) {
87108
return false;

core/src/main/java/com/opensymphony/xwork2/util/reflection/ReflectionProvider.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,23 @@ public interface ReflectionProvider {
7272
* note if exclusions AND inclusions are supplied and not null nothing will get copied.
7373
*/
7474
void copy(Object from, Object to, Map<String, Object> context, Collection<String> exclusions, Collection<String> inclusions);
75-
75+
76+
/**
77+
* Copies the properties in the object "from" and sets them in the object "to"
78+
* only setting properties defined in the given "editable" class (or interface)
79+
* using specified type converter, or {@link com.opensymphony.xwork2.conversion.impl.XWorkConverter} if none
80+
* is specified.
81+
*
82+
* @param from the source object
83+
* @param to the target object
84+
* @param context the action context we're running under
85+
* @param exclusions collection of method names to excluded from copying ( can be null)
86+
* @param inclusions collection of method names to included copying (can be null)
87+
* note if exclusions AND inclusions are supplied and not null nothing will get copied.
88+
* @param editable the class (or interface) to restrict property setting to
89+
*/
90+
void copy(Object from, Object to, Map<String, Object> context, Collection<String> exclusions, Collection<String> inclusions, Class<?> editable);
91+
7692
/**
7793
* Looks for the real target with the specified property given a root Object which may be a
7894
* CompoundRoot.

core/src/test/java/com/opensymphony/xwork2/ognl/OgnlUtilTest.java

+26
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,32 @@ public void testIncudeExcludes() {
209209

210210
}
211211

212+
public void testCopyEditable() {
213+
Foo foo1 = new Foo();
214+
Foo foo2 = new Foo();
215+
216+
Map<String, Object> context = ognlUtil.createDefaultContext(foo1);
217+
218+
Calendar cal = Calendar.getInstance();
219+
cal.clear();
220+
cal.set(Calendar.MONTH, Calendar.MAY);
221+
cal.set(Calendar.DAY_OF_MONTH, 29);
222+
cal.set(Calendar.YEAR, 2017);
223+
224+
foo1.setTitle("blah");
225+
foo1.setNumber(1);
226+
foo1.setPoints(new long[]{1, 2, 3});
227+
foo1.setBirthday(cal.getTime());
228+
foo1.setUseful(false);
229+
230+
ognlUtil.copy(foo1, foo2, context, null, null, Bar.class);
231+
232+
assertEquals(foo1.getTitle(), foo2.getTitle());
233+
assertEquals(0, foo2.getNumber());
234+
assertNull(foo2.getPoints());
235+
assertNull(foo2.getBirthday());
236+
}
237+
212238

213239
public void testCopyUnevenObjects() {
214240
Foo foo = new Foo();

core/src/test/java/com/opensymphony/xwork2/spring/ActionsFromSpringTest.java

+13-7
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,20 @@ public void testChainingProxiedActions() throws Exception {
8484

8585
proxy.execute();
8686

87-
TestSubBean chaintoAOPedAction = (TestSubBean) appContext.getBean("pointcutted-test-sub-bean");
88-
TestSubBean aspectState = (TestSubBean) appContext.getBean("aspected-test-sub-bean");
87+
// check if AOP works
88+
TestAspect aspectState = (TestAspect) appContext.getBean("test-aspect");
89+
// chainedAction.actionMethodName sets name then chainedAction.getCount sets count
90+
// then chaintoAction.setCount sets count2 then chainedAction.getName sets name again
91+
// then chaintoAction.actionMethodName sets issueId of the aspect object.
92+
assertEquals("setName(WW-4105)-setCount(1)-setCount2(1)-setName(WW-4105)-setIssueId(WW-4105)-", aspectState.log);
93+
assertEquals(aspectState.getName(), aspectState.getIssueId());
94+
assertEquals("WW-4105", aspectState.getIssueId());
95+
assertEquals(aspectState.getCount(), aspectState.getCount2());
96+
assertEquals(1, aspectState.getCount());
8997

90-
assertEquals(1, chaintoAOPedAction.getCount()); //check if chain
98+
// check if chain works
99+
TestSubBean chaintoAOPedAction = (TestSubBean) appContext.getBean("pointcutted-test-sub-bean");
100+
assertEquals(1, chaintoAOPedAction.getCount());
91101
assertEquals("WW-4105", chaintoAOPedAction.getName());
92-
assertNotNull(aspectState.getIssueId()); //and AOP proxied actions
93-
assertNotNull(aspectState.getName());
94-
assertEquals(aspectState.getName(), aspectState.getIssueId());
95-
assertEquals("WW-4105", aspectState.getIssueId()); //work together without any problem
96102
}
97103
}

core/src/test/java/com/opensymphony/xwork2/spring/SpringProxyUtilTest.java

+16-16
Original file line numberDiff line numberDiff line change
@@ -53,33 +53,33 @@ public void testIsSpringAopProxy() throws Exception {
5353
Object pointcuttedTestSubBean = appContext.getBean("pointcutted-test-sub-bean");
5454
assertTrue(ProxyUtil.isSpringAopProxy(pointcuttedTestSubBean));
5555

56-
Object aspectedTestSubBean = appContext.getBean("aspected-test-sub-bean");
57-
assertFalse(ProxyUtil.isSpringAopProxy(aspectedTestSubBean));
56+
Object testAspect = appContext.getBean("test-aspect");
57+
assertFalse(ProxyUtil.isSpringAopProxy(testAspect));
5858
}
5959

60-
public void testGetSpringUltimateTargetObject() throws Exception {
60+
public void testSpringUltimateTargetClass() throws Exception {
6161
Object simpleAction = appContext.getBean("simple-action");
62-
Object simpleActionUltimateTargetObject = ProxyUtil.getSpringUltimateTargetObject(simpleAction);
63-
assertEquals(simpleAction, simpleActionUltimateTargetObject);
62+
Class<?> simpleActionUltimateTargetClass = ProxyUtil.springUltimateTargetClass(simpleAction);
63+
assertEquals(SimpleAction.class, simpleActionUltimateTargetClass);
6464

6565
Object proxiedAction = appContext.getBean("proxied-action");
66-
Object proxiedActionUltimateTargetObject = ProxyUtil.getSpringUltimateTargetObject(proxiedAction);
67-
assertEquals(SimpleAction.class, proxiedActionUltimateTargetObject.getClass());
66+
Class<?> proxiedActionUltimateTargetClass = ProxyUtil.springUltimateTargetClass(proxiedAction);
67+
assertEquals(SimpleAction.class, proxiedActionUltimateTargetClass);
6868

6969
Object autoProxiedAction = appContext.getBean("auto-proxied-action");
70-
Object autoProxiedActionUltimateTargetObject = ProxyUtil.getSpringUltimateTargetObject(autoProxiedAction);
71-
assertEquals(SimpleAction.class, autoProxiedActionUltimateTargetObject.getClass());
70+
Class<?> autoProxiedActionUltimateTargetClass = ProxyUtil.springUltimateTargetClass(autoProxiedAction);
71+
assertEquals(SimpleAction.class, autoProxiedActionUltimateTargetClass);
7272

7373
Object pointcuttedTestBean = appContext.getBean("pointcutted-test-bean");
74-
Object pointcuttedTestBeanUltimateTargetObject = ProxyUtil.getSpringUltimateTargetObject(pointcuttedTestBean);
75-
assertEquals(TestBean.class, pointcuttedTestBeanUltimateTargetObject.getClass());
74+
Class<?> pointcuttedTestBeanUltimateTargetClass = ProxyUtil.springUltimateTargetClass(pointcuttedTestBean);
75+
assertEquals(TestBean.class, pointcuttedTestBeanUltimateTargetClass);
7676

7777
Object pointcuttedTestSubBean = appContext.getBean("pointcutted-test-sub-bean");
78-
Object pointcuttedTestSubBeanUltimateTargetObject = ProxyUtil.getSpringUltimateTargetObject(pointcuttedTestSubBean);
79-
assertEquals(TestSubBean.class, pointcuttedTestSubBeanUltimateTargetObject.getClass());
78+
Class<?> pointcuttedTestSubBeanUltimateTargetClass = ProxyUtil.springUltimateTargetClass(pointcuttedTestSubBean);
79+
assertEquals(TestSubBean.class, pointcuttedTestSubBeanUltimateTargetClass);
8080

81-
Object aspectedTestSubBean = appContext.getBean("aspected-test-sub-bean");
82-
Object aspectedTestSubBeanUltimateTargetObject = ProxyUtil.getSpringUltimateTargetObject(aspectedTestSubBean);
83-
assertEquals(aspectedTestSubBean, aspectedTestSubBeanUltimateTargetObject);
81+
Object testAspect = appContext.getBean("test-aspect");
82+
Class<?> testAspectUltimateTargetClass = ProxyUtil.springUltimateTargetClass(testAspect);
83+
assertEquals(TestAspect.class, testAspectUltimateTargetClass);
8484
}
8585
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.opensymphony.xwork2.spring;
2+
3+
public class TestAspect {
4+
protected String log = "";
5+
6+
private String issueId;
7+
private int count;
8+
private String name;
9+
private int count2;
10+
11+
String getIssueId() {
12+
return issueId;
13+
}
14+
15+
public void setIssueId(String issueId) {
16+
log = log + "setIssueId(" + issueId + ")-";
17+
this.issueId = issueId;
18+
}
19+
20+
public int getCount() {
21+
return count;
22+
}
23+
24+
public void setCount(int count) {
25+
log = log + "setCount(" + count + ")-";
26+
this.count = count;
27+
}
28+
29+
public String getName() {
30+
return name;
31+
}
32+
33+
public void setName(String name) {
34+
log = log + "setName(" + name + ")-";
35+
this.name = name;
36+
}
37+
38+
int getCount2() {
39+
return count2;
40+
}
41+
42+
public void setCount2(int count2) {
43+
log = log + "setCount2(" + count2 + ")-";
44+
this.count2 = count2;
45+
}
46+
}

core/src/test/resources/com/opensymphony/xwork2/spring/actionContext-spring.xml

+12-4
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,25 @@
5151
<bean id="pointcutted-test-sub-bean" class="com.opensymphony.xwork2.TestSubBean">
5252
<property name="issueId"><value>WW-4105</value></property>
5353
</bean>
54-
<bean id="aspected-test-sub-bean" class="com.opensymphony.xwork2.TestSubBean" />
54+
<bean id="test-aspect" class="com.opensymphony.xwork2.spring.TestAspect" />
5555
<aop:config>
56-
<aop:aspect id="myAspect" ref="aspected-test-sub-bean">
56+
<aop:aspect id="myAspect" ref="test-aspect">
5757
<aop:pointcut id="testBeanGetName"
5858
expression="execution(String com.opensymphony.xwork2.TestBean.getName()) and bean(pointcutted-test-bean)" />
5959
<aop:after-returning pointcut-ref="testBeanGetName"
60-
method="setIssueId" returning="issueId" />
60+
method="setName" returning="name" />
61+
<aop:pointcut id="testBeanGetCount"
62+
expression="execution(int com.opensymphony.xwork2.TestBean.getCount()) and bean(pointcutted-test-bean)" />
63+
<aop:after-returning pointcut-ref="testBeanGetCount"
64+
method="setCount" returning="count" />
6165
<aop:pointcut id="testSubBeanGetIssueId"
6266
expression="execution(String com.opensymphony.xwork2.TestSubBean.getIssueId()) and bean(pointcutted-test-sub-bean)" />
6367
<aop:after-returning pointcut-ref="testSubBeanGetIssueId"
64-
method="setName" returning="name" />
68+
method="setIssueId" returning="issueId" />
69+
<aop:pointcut id="testBeanSetCount"
70+
expression="execution(void com.opensymphony.xwork2.TestBean.setCount(int)) and args(count2) and bean(pointcutted-test-sub-bean)" />
71+
<aop:before pointcut-ref="testBeanSetCount"
72+
method="setCount2" arg-names="count2"/>
6573
</aop:aspect>
6674
</aop:config>
6775
</beans>

0 commit comments

Comments
 (0)