001: /*
002: * Copyright 2002-2007 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.springframework.aop.aspectj;
018:
019: import java.lang.reflect.Method;
020: import java.util.HashMap;
021: import java.util.HashSet;
022: import java.util.Map;
023: import java.util.Set;
024:
025: import org.aopalliance.intercept.MethodInvocation;
026: import org.apache.commons.logging.Log;
027: import org.apache.commons.logging.LogFactory;
028: import org.aspectj.weaver.BCException;
029: import org.aspectj.weaver.reflect.ReflectionWorld;
030: import org.aspectj.weaver.tools.JoinPointMatch;
031: import org.aspectj.weaver.tools.PointcutExpression;
032: import org.aspectj.weaver.tools.PointcutParameter;
033: import org.aspectj.weaver.tools.PointcutParser;
034: import org.aspectj.weaver.tools.PointcutPrimitive;
035: import org.aspectj.weaver.tools.ShadowMatch;
036:
037: import org.springframework.aop.ClassFilter;
038: import org.springframework.aop.IntroductionAwareMethodMatcher;
039: import org.springframework.aop.MethodMatcher;
040: import org.springframework.aop.ProxyMethodInvocation;
041: import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
042: import org.springframework.aop.support.AbstractExpressionPointcut;
043: import org.springframework.aop.support.AopUtils;
044: import org.springframework.util.ObjectUtils;
045: import org.springframework.util.StringUtils;
046:
047: /**
048: * Spring {@link org.springframework.aop.Pointcut} implementation
049: * that uses the AspectJ weaver to evaluate a pointcut expression.
050: *
051: * <p>The pointcut expression value is an AspectJ expression. This can
052: * reference other pointcuts and use composition and other operations.
053: *
054: * <p>Naturally, as this is to be processed by Spring AOP's proxy-based model,
055: * only method execution pointcuts are supported.
056: *
057: * @author Rob Harrop
058: * @author Adrian Colyer
059: * @author Rod Johnson
060: * @author Juergen Hoeller
061: * @author Ramnivas Laddad
062: * @since 2.0
063: */
064: public class AspectJExpressionPointcut extends
065: AbstractExpressionPointcut implements ClassFilter,
066: IntroductionAwareMethodMatcher {
067:
068: private static final Set DEFAULT_SUPPORTED_PRIMITIVES = new HashSet();
069:
070: static {
071: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION);
072: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.ARGS);
073: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.REFERENCE);
074: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.THIS);
075: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.TARGET);
076: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.WITHIN);
077: DEFAULT_SUPPORTED_PRIMITIVES
078: .add(PointcutPrimitive.AT_ANNOTATION);
079: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_WITHIN);
080: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_ARGS);
081: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_TARGET);
082: }
083:
084: private static final Log logger = LogFactory
085: .getLog(AspectJExpressionPointcut.class);
086:
087: private final Map shadowMapCache = new HashMap();
088:
089: private PointcutParser pointcutParser;
090:
091: private Class pointcutDeclarationScope;
092:
093: private String[] pointcutParameterNames = new String[0];
094:
095: private Class[] pointcutParameterTypes = new Class[0];
096:
097: private PointcutExpression pointcutExpression;
098:
099: /**
100: * Create a new default AspectJExpressionPointcut.
101: */
102: public AspectJExpressionPointcut() {
103: this (DEFAULT_SUPPORTED_PRIMITIVES);
104: }
105:
106: /**
107: * Create a new AspectJExpressionPointcut with the given supported primitives.
108: * @param supportedPrimitives Set of {@link org.aspectj.weaver.tools.PointcutPrimitive}
109: * instances
110: */
111: public AspectJExpressionPointcut(Set supportedPrimitives) {
112: this .pointcutParser = PointcutParser
113: .getPointcutParserSupportingSpecifiedPrimitivesAndUsingContextClassloaderForResolution(supportedPrimitives);
114: }
115:
116: /**
117: * Create a new AspectJExpressionPointcut with the given settings.
118: * @param declarationScope the declaration scope for the pointcut
119: * @param paramNames the parameter names for the pointcut
120: * @param paramTypes the parameter types for the pointcut
121: */
122: public AspectJExpressionPointcut(Class declarationScope,
123: String[] paramNames, Class[] paramTypes) {
124: this (DEFAULT_SUPPORTED_PRIMITIVES);
125: this .pointcutDeclarationScope = declarationScope;
126: if (paramNames.length != paramTypes.length) {
127: throw new IllegalStateException(
128: "Number of pointcut parameter names must match number of pointcut parameter types");
129: }
130: this .pointcutParameterNames = paramNames;
131: this .pointcutParameterTypes = paramTypes;
132: }
133:
134: /**
135: * Set the declaration scope for the pointcut.
136: */
137: public void setPointcutDeclarationScope(
138: Class pointcutDeclarationScope) {
139: this .pointcutDeclarationScope = pointcutDeclarationScope;
140: }
141:
142: /**
143: * Set the parameter names for the pointcut.
144: */
145: public void setParameterNames(String[] names) {
146: this .pointcutParameterNames = names;
147: }
148:
149: /**
150: * Set the parameter types for the pointcut.
151: */
152: public void setParameterTypes(Class[] types) {
153: this .pointcutParameterTypes = types;
154: }
155:
156: public ClassFilter getClassFilter() {
157: checkReadyToMatch();
158: return this ;
159: }
160:
161: public MethodMatcher getMethodMatcher() {
162: checkReadyToMatch();
163: return this ;
164: }
165:
166: /**
167: * Check whether this pointcut is ready to match,
168: * lazily building the underlying AspectJ pointcut expression.
169: */
170: private void checkReadyToMatch() {
171: if (getExpression() == null) {
172: throw new IllegalStateException(
173: "Must set property 'expression' before attempting to match");
174: }
175: if (this .pointcutExpression == null) {
176: this .pointcutExpression = buildPointcutExpression();
177: }
178: }
179:
180: /**
181: * Build the underlying AspectJ pointcut expression.
182: */
183: private PointcutExpression buildPointcutExpression() {
184: PointcutParameter[] pointcutParameters = new PointcutParameter[this .pointcutParameterNames.length];
185: for (int i = 0; i < pointcutParameters.length; i++) {
186: pointcutParameters[i] = this .pointcutParser
187: .createPointcutParameter(
188: this .pointcutParameterNames[i],
189: this .pointcutParameterTypes[i]);
190: }
191: return this .pointcutParser.parsePointcutExpression(
192: replaceBooleanOperators(getExpression()),
193: this .pointcutDeclarationScope, pointcutParameters);
194: }
195:
196: /**
197: * If a pointcut expression has been specified in XML, the user cannot
198: * write <code>and</code> as "&&" (though && will work).
199: * We also allow <code>and</code> between two pointcut sub-expressions.
200: * <p>This method converts back to <code>&&</code> for the AspectJ pointcut parser.
201: */
202: private String replaceBooleanOperators(String pcExpr) {
203: pcExpr = StringUtils.replace(pcExpr, " and ", " && ");
204: pcExpr = StringUtils.replace(pcExpr, " or ", " || ");
205: pcExpr = StringUtils.replace(pcExpr, " not ", " ! ");
206: return pcExpr;
207: }
208:
209: /**
210: * Return the underlying AspectJ pointcut expression.
211: */
212: public PointcutExpression getPointcutExpression() {
213: checkReadyToMatch();
214: return this .pointcutExpression;
215: }
216:
217: public boolean matches(Class targetClass) {
218: checkReadyToMatch();
219: try {
220: return this .pointcutExpression
221: .couldMatchJoinPointsInType(targetClass);
222: } catch (BCException ex) {
223: logger
224: .debug(
225: "PointcutExpression matching rejected target class",
226: ex);
227: return false;
228: }
229: }
230:
231: public boolean matches(Method method, Class targetClass,
232: boolean beanHasIntroductions) {
233: checkReadyToMatch();
234: Method targetMethod = AopUtils.getMostSpecificMethod(method,
235: targetClass);
236: ShadowMatch shadowMatch = null;
237: try {
238: shadowMatch = getShadowMatch(targetMethod, method);
239: } catch (ReflectionWorld.ReflectionWorldException ex) {
240: // Could neither introspect the target class nor the proxy class ->
241: // let's simply consider this method as non-matching.
242: return false;
243: }
244:
245: // Special handling for this, target, @this, @target, @annotation
246: // in Spring - we can optimize since we know we have exactly this class,
247: // and there will never be matching subclass at runtime.
248: if (shadowMatch.alwaysMatches()) {
249: return true;
250: } else if (shadowMatch.neverMatches()) {
251: return false;
252: } else {
253: // the maybe case
254: return (beanHasIntroductions || matchesIgnoringSubtypes(shadowMatch));
255: }
256: }
257:
258: public boolean matches(Method method, Class targetClass) {
259: return matches(method, targetClass, false);
260: }
261:
262: public boolean isRuntime() {
263: checkReadyToMatch();
264: return this .pointcutExpression.mayNeedDynamicTest();
265: }
266:
267: public boolean matches(Method method, Class targetClass,
268: Object[] args) {
269: checkReadyToMatch();
270: ShadowMatch shadowMatch = null;
271: ShadowMatch originalShadowMatch = null;
272: try {
273: shadowMatch = getShadowMatch(AopUtils
274: .getMostSpecificMethod(method, targetClass), method);
275: originalShadowMatch = getShadowMatch(method, method);
276: } catch (ReflectionWorld.ReflectionWorldException ex) {
277: // Could neither introspect the target class nor the proxy class ->
278: // let's simply consider this method as non-matching.
279: return false;
280: }
281:
282: // Bind Spring AOP proxy to AspectJ "this" and Spring AOP target to AspectJ target,
283: // consistent with return of MethodInvocationProceedingJoinPoint
284: ProxyMethodInvocation pmi = null;
285: Object targetObject = null;
286: Object this Object = null;
287: try {
288: MethodInvocation mi = ExposeInvocationInterceptor
289: .currentInvocation();
290: targetObject = mi.getThis();
291: if (!(mi instanceof ProxyMethodInvocation)) {
292: throw new IllegalStateException(
293: "MethodInvocation is not a Spring ProxyMethodInvocation: "
294: + mi);
295: }
296: pmi = (ProxyMethodInvocation) mi;
297: this Object = pmi.getProxy();
298: } catch (IllegalStateException ex) {
299: // No current invocation...
300: // TODO: Should we really proceed here?
301: logger
302: .debug("Couldn't access current invocation - matching with limited context: "
303: + ex);
304: }
305:
306: JoinPointMatch joinPointMatch = shadowMatch.matchesJoinPoint(
307: this Object, targetObject, args);
308:
309: /*
310: * Do a final check to see if any this(TYPE) kind of residue match. For
311: * this purpose, we use the original method's (proxy method's) shadow to
312: * ensure that 'this' is correctly checked against. Without this check,
313: * we get incorrect match on this(TYPE) where TYPE matches the target
314: * type but not 'this' (as would be the case of JDK dynamic proxies).
315: *
316: * See SPR-2979 for the original bug.
317: */
318: RuntimeTestWalker originalMethodResidueTest = new RuntimeTestWalker(
319: originalShadowMatch);
320: if (!originalMethodResidueTest
321: .testThisInstanceOfResidue(this Object)) {
322: return false;
323: }
324:
325: if (joinPointMatch.matches() && pmi != null) {
326: bindParameters(pmi, joinPointMatch);
327: }
328: return joinPointMatch.matches();
329: }
330:
331: /**
332: * A match test returned maybe - if there are any subtype sensitive variables
333: * involved in the test (this, target, at_this, at_target, at_annotation) then
334: * we say this is not a match as in Spring there will never be a different
335: * runtime subtype.
336: */
337: private boolean matchesIgnoringSubtypes(ShadowMatch shadowMatch) {
338: return !(new RuntimeTestWalker(shadowMatch)
339: .testsSubtypeSensitiveVars());
340: }
341:
342: private void bindParameters(ProxyMethodInvocation invocation,
343: JoinPointMatch jpm) {
344: // Note: Can't use JoinPointMatch.getClass().getName() as the key, since
345: // Spring AOP does all the matching at a join point, and then all the invocations
346: // under this scenario, if we just use JoinPointMatch as the key, then
347: // 'last man wins' which is not what we want at all.
348: // Using the expression is guaranteed to be safe, since 2 identical expressions
349: // are guaranteed to bind in exactly the same way.
350: invocation.setUserAttribute(getExpression(), jpm);
351: }
352:
353: private ShadowMatch getShadowMatch(Method targetMethod,
354: Method originalMethod) {
355: synchronized (this .shadowMapCache) {
356: ShadowMatch shadowMatch = (ShadowMatch) this .shadowMapCache
357: .get(targetMethod);
358: if (shadowMatch == null) {
359: try {
360: shadowMatch = this .pointcutExpression
361: .matchesMethodExecution(targetMethod);
362: } catch (ReflectionWorld.ReflectionWorldException ex) {
363: // Failed to introspect target method, probably because it has been loaded
364: // in a special ClassLoader. Let's try the original method instead...
365: if (targetMethod == originalMethod) {
366: throw ex;
367: }
368: shadowMatch = this .pointcutExpression
369: .matchesMethodExecution(originalMethod);
370: }
371: this .shadowMapCache.put(targetMethod, shadowMatch);
372: }
373: return shadowMatch;
374: }
375: }
376:
377: public boolean equals(Object other) {
378: if (this == other) {
379: return true;
380: }
381: if (!(other instanceof AspectJExpressionPointcut)) {
382: return false;
383: }
384: AspectJExpressionPointcut otherPc = (AspectJExpressionPointcut) other;
385: return ObjectUtils.nullSafeEquals(this .getExpression(), otherPc
386: .getExpression())
387: && ObjectUtils.nullSafeEquals(
388: this .pointcutDeclarationScope,
389: otherPc.pointcutDeclarationScope)
390: && ObjectUtils.nullSafeEquals(
391: this .pointcutParameterNames,
392: otherPc.pointcutParameterNames)
393: && ObjectUtils.nullSafeEquals(
394: this .pointcutParameterTypes,
395: otherPc.pointcutParameterTypes);
396: }
397:
398: public int hashCode() {
399: int hashCode = ObjectUtils.nullSafeHashCode(this
400: .getExpression());
401: hashCode = 31
402: * hashCode
403: + ObjectUtils
404: .nullSafeHashCode(this .pointcutDeclarationScope);
405: hashCode = 31
406: * hashCode
407: + ObjectUtils
408: .nullSafeHashCode(this .pointcutParameterNames);
409: hashCode = 31
410: * hashCode
411: + ObjectUtils
412: .nullSafeHashCode(this .pointcutParameterTypes);
413: return hashCode;
414: }
415:
416: public String toString() {
417: StringBuffer sb = new StringBuffer();
418: sb.append("AspectJExpressionPointcut: ");
419: if (this .pointcutParameterNames != null
420: && this .pointcutParameterTypes != null) {
421: sb.append("(");
422: for (int i = 0; i < this .pointcutParameterTypes.length; i++) {
423: sb.append(this .pointcutParameterTypes[i].getName());
424: sb.append(" ");
425: sb.append(this .pointcutParameterNames[i]);
426: if ((i + 1) < this .pointcutParameterTypes.length) {
427: sb.append(", ");
428: }
429: }
430: sb.append(")");
431: }
432: sb.append(" ");
433: if (getExpression() != null) {
434: sb.append(getExpression());
435: } else {
436: sb.append("<pointcut expression not set>");
437: }
438: return sb.toString();
439: }
440:
441: }
|