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.patterns.NamePattern;
030: import org.aspectj.weaver.reflect.ReflectionWorld;
031: import org.aspectj.weaver.tools.ContextBasedMatcher;
032: import org.aspectj.weaver.tools.FuzzyBoolean;
033: import org.aspectj.weaver.tools.JoinPointMatch;
034: import org.aspectj.weaver.tools.MatchingContext;
035: import org.aspectj.weaver.tools.PointcutDesignatorHandler;
036: import org.aspectj.weaver.tools.PointcutExpression;
037: import org.aspectj.weaver.tools.PointcutParameter;
038: import org.aspectj.weaver.tools.PointcutParser;
039: import org.aspectj.weaver.tools.PointcutPrimitive;
040: import org.aspectj.weaver.tools.ShadowMatch;
041:
042: import org.springframework.aop.ClassFilter;
043: import org.springframework.aop.IntroductionAwareMethodMatcher;
044: import org.springframework.aop.MethodMatcher;
045: import org.springframework.aop.ProxyMethodInvocation;
046: import org.springframework.aop.framework.autoproxy.ProxyCreationContext;
047: import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
048: import org.springframework.aop.support.AbstractExpressionPointcut;
049: import org.springframework.aop.support.AopUtils;
050: import org.springframework.beans.factory.BeanFactoryUtils;
051: import org.springframework.util.ObjectUtils;
052: import org.springframework.util.StringUtils;
053:
054: /**
055: * Spring {@link org.springframework.aop.Pointcut} implementation
056: * that uses the AspectJ weaver to evaluate a pointcut expression.
057: *
058: * <p>The pointcut expression value is an AspectJ expression. This can
059: * reference other pointcuts and use composition and other operations.
060: *
061: * <p>Naturally, as this is to be processed by Spring AOP's proxy-based model,
062: * only method execution pointcuts are supported.
063: *
064: * @author Rob Harrop
065: * @author Adrian Colyer
066: * @author Rod Johnson
067: * @author Juergen Hoeller
068: * @author Ramnivas Laddad
069: * @since 2.0
070: */
071: public class AspectJExpressionPointcut extends
072: AbstractExpressionPointcut implements ClassFilter,
073: IntroductionAwareMethodMatcher {
074:
075: private static final Set DEFAULT_SUPPORTED_PRIMITIVES = new HashSet();
076:
077: static {
078: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION);
079: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.ARGS);
080: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.REFERENCE);
081: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.THIS);
082: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.TARGET);
083: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.WITHIN);
084: DEFAULT_SUPPORTED_PRIMITIVES
085: .add(PointcutPrimitive.AT_ANNOTATION);
086: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_WITHIN);
087: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_ARGS);
088: DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_TARGET);
089: }
090:
091: private static final Log logger = LogFactory
092: .getLog(AspectJExpressionPointcut.class);
093:
094: private final Map shadowMapCache = new HashMap();
095:
096: private PointcutParser pointcutParser;
097:
098: private Class pointcutDeclarationScope;
099:
100: private String[] pointcutParameterNames = new String[0];
101:
102: private Class[] pointcutParameterTypes = new Class[0];
103:
104: private PointcutExpression pointcutExpression;
105:
106: /**
107: * Create a new default AspectJExpressionPointcut.
108: */
109: public AspectJExpressionPointcut() {
110: this (DEFAULT_SUPPORTED_PRIMITIVES);
111: }
112:
113: /**
114: * Create a new AspectJExpressionPointcut with the given supported primitives.
115: * @param supportedPrimitives Set of {@link org.aspectj.weaver.tools.PointcutPrimitive}
116: * instances
117: */
118: public AspectJExpressionPointcut(Set supportedPrimitives) {
119: this .pointcutParser = PointcutParser
120: .getPointcutParserSupportingSpecifiedPrimitivesAndUsingContextClassloaderForResolution(supportedPrimitives);
121: this .pointcutParser
122: .registerPointcutDesignatorHandler(new BeanNamePointcutDesignatorHandler());
123: }
124:
125: /**
126: * Create a new AspectJExpressionPointcut with the given settings.
127: * @param declarationScope the declaration scope for the pointcut
128: * @param paramNames the parameter names for the pointcut
129: * @param paramTypes the parameter types for the pointcut
130: */
131: public AspectJExpressionPointcut(Class declarationScope,
132: String[] paramNames, Class[] paramTypes) {
133: this (DEFAULT_SUPPORTED_PRIMITIVES);
134: this .pointcutDeclarationScope = declarationScope;
135: if (paramNames.length != paramTypes.length) {
136: throw new IllegalStateException(
137: "Number of pointcut parameter names must match number of pointcut parameter types");
138: }
139: this .pointcutParameterNames = paramNames;
140: this .pointcutParameterTypes = paramTypes;
141: }
142:
143: /**
144: * Set the declaration scope for the pointcut.
145: */
146: public void setPointcutDeclarationScope(
147: Class pointcutDeclarationScope) {
148: this .pointcutDeclarationScope = pointcutDeclarationScope;
149: }
150:
151: /**
152: * Set the parameter names for the pointcut.
153: */
154: public void setParameterNames(String[] names) {
155: this .pointcutParameterNames = names;
156: }
157:
158: /**
159: * Set the parameter types for the pointcut.
160: */
161: public void setParameterTypes(Class[] types) {
162: this .pointcutParameterTypes = types;
163: }
164:
165: public ClassFilter getClassFilter() {
166: checkReadyToMatch();
167: return this ;
168: }
169:
170: public MethodMatcher getMethodMatcher() {
171: checkReadyToMatch();
172: return this ;
173: }
174:
175: /**
176: * Check whether this pointcut is ready to match,
177: * lazily building the underlying AspectJ pointcut expression.
178: */
179: private void checkReadyToMatch() {
180: if (getExpression() == null) {
181: throw new IllegalStateException(
182: "Must set property 'expression' before attempting to match");
183: }
184: if (this .pointcutExpression == null) {
185: this .pointcutExpression = buildPointcutExpression();
186: }
187: }
188:
189: /**
190: * Build the underlying AspectJ pointcut expression.
191: */
192: private PointcutExpression buildPointcutExpression() {
193: PointcutParameter[] pointcutParameters = new PointcutParameter[this .pointcutParameterNames.length];
194: for (int i = 0; i < pointcutParameters.length; i++) {
195: pointcutParameters[i] = this .pointcutParser
196: .createPointcutParameter(
197: this .pointcutParameterNames[i],
198: this .pointcutParameterTypes[i]);
199: }
200: return this .pointcutParser.parsePointcutExpression(
201: replaceBooleanOperators(getExpression()),
202: this .pointcutDeclarationScope, pointcutParameters);
203: }
204:
205: /**
206: * If a pointcut expression has been specified in XML, the user cannot
207: * write <code>and</code> as "&&" (though && will work).
208: * We also allow <code>and</code> between two pointcut sub-expressions.
209: * <p>This method converts back to <code>&&</code> for the AspectJ pointcut parser.
210: */
211: private String replaceBooleanOperators(String pcExpr) {
212: pcExpr = StringUtils.replace(pcExpr, " and ", " && ");
213: pcExpr = StringUtils.replace(pcExpr, " or ", " || ");
214: pcExpr = StringUtils.replace(pcExpr, " not ", " ! ");
215: return pcExpr;
216: }
217:
218: /**
219: * Return the underlying AspectJ pointcut expression.
220: */
221: public PointcutExpression getPointcutExpression() {
222: checkReadyToMatch();
223: return this .pointcutExpression;
224: }
225:
226: public boolean matches(Class targetClass) {
227: checkReadyToMatch();
228: try {
229: return this .pointcutExpression
230: .couldMatchJoinPointsInType(targetClass);
231: } catch (BCException ex) {
232: logger
233: .debug(
234: "PointcutExpression matching rejected target class",
235: ex);
236: return false;
237: }
238: }
239:
240: public boolean matches(Method method, Class targetClass,
241: boolean beanHasIntroductions) {
242: checkReadyToMatch();
243: Method targetMethod = AopUtils.getMostSpecificMethod(method,
244: targetClass);
245: ShadowMatch shadowMatch = null;
246: try {
247: shadowMatch = getShadowMatch(targetMethod, method);
248: } catch (ReflectionWorld.ReflectionWorldException ex) {
249: // Could neither introspect the target class nor the proxy class ->
250: // let's simply consider this method as non-matching.
251: return false;
252: }
253:
254: // Special handling for this, target, @this, @target, @annotation
255: // in Spring - we can optimize since we know we have exactly this class,
256: // and there will never be matching subclass at runtime.
257: if (shadowMatch.alwaysMatches()) {
258: return true;
259: } else if (shadowMatch.neverMatches()) {
260: return false;
261: } else {
262: // the maybe case
263: return (beanHasIntroductions
264: || matchesIgnoringSubtypes(shadowMatch) || matchesTarget(
265: shadowMatch, targetClass));
266: }
267: }
268:
269: public boolean matches(Method method, Class targetClass) {
270: return matches(method, targetClass, false);
271: }
272:
273: public boolean isRuntime() {
274: checkReadyToMatch();
275: return this .pointcutExpression.mayNeedDynamicTest();
276: }
277:
278: public boolean matches(Method method, Class targetClass,
279: Object[] args) {
280: checkReadyToMatch();
281: ShadowMatch shadowMatch = null;
282: ShadowMatch originalShadowMatch = null;
283: try {
284: shadowMatch = getShadowMatch(AopUtils
285: .getMostSpecificMethod(method, targetClass), method);
286: originalShadowMatch = getShadowMatch(method, method);
287: } catch (ReflectionWorld.ReflectionWorldException ex) {
288: // Could neither introspect the target class nor the proxy class ->
289: // let's simply consider this method as non-matching.
290: return false;
291: }
292:
293: // Bind Spring AOP proxy to AspectJ "this" and Spring AOP target to AspectJ target,
294: // consistent with return of MethodInvocationProceedingJoinPoint
295: ProxyMethodInvocation pmi = null;
296: Object targetObject = null;
297: Object this Object = null;
298: try {
299: MethodInvocation mi = ExposeInvocationInterceptor
300: .currentInvocation();
301: targetObject = mi.getThis();
302: if (!(mi instanceof ProxyMethodInvocation)) {
303: throw new IllegalStateException(
304: "MethodInvocation is not a Spring ProxyMethodInvocation: "
305: + mi);
306: }
307: pmi = (ProxyMethodInvocation) mi;
308: this Object = pmi.getProxy();
309: } catch (IllegalStateException ex) {
310: // No current invocation...
311: // TODO: Should we really proceed here?
312: logger
313: .debug("Couldn't access current invocation - matching with limited context: "
314: + ex);
315: }
316:
317: JoinPointMatch joinPointMatch = shadowMatch.matchesJoinPoint(
318: this Object, targetObject, args);
319:
320: /*
321: * Do a final check to see if any this(TYPE) kind of residue match. For
322: * this purpose, we use the original method's (proxy method's) shadow to
323: * ensure that 'this' is correctly checked against. Without this check,
324: * we get incorrect match on this(TYPE) where TYPE matches the target
325: * type but not 'this' (as would be the case of JDK dynamic proxies).
326: * <p>See SPR-2979 for the original bug.
327: */
328: if (pmi != null) { // there is a current invocation
329: RuntimeTestWalker originalMethodResidueTest = new RuntimeTestWalker(
330: originalShadowMatch);
331: if (!originalMethodResidueTest
332: .testThisInstanceOfResidue(this Object.getClass())) {
333: return false;
334: }
335: }
336: if (joinPointMatch.matches() && pmi != null) {
337: bindParameters(pmi, joinPointMatch);
338: }
339: return joinPointMatch.matches();
340: }
341:
342: protected String getCurrentProxiedBeanName() {
343: return ProxyCreationContext.getCurrentProxiedBeanName();
344: }
345:
346: /**
347: * A match test returned maybe - if there are any subtype sensitive variables
348: * involved in the test (this, target, at_this, at_target, at_annotation) then
349: * we say this is not a match as in Spring there will never be a different
350: * runtime subtype.
351: */
352: private boolean matchesIgnoringSubtypes(ShadowMatch shadowMatch) {
353: return !(new RuntimeTestWalker(shadowMatch)
354: .testsSubtypeSensitiveVars());
355: }
356:
357: private boolean matchesTarget(ShadowMatch shadowMatch,
358: Class targetClass) {
359: return new RuntimeTestWalker(shadowMatch)
360: .testTargetInstanceOfResidue(targetClass);
361: }
362:
363: private void bindParameters(ProxyMethodInvocation invocation,
364: JoinPointMatch jpm) {
365: // Note: Can't use JoinPointMatch.getClass().getName() as the key, since
366: // Spring AOP does all the matching at a join point, and then all the invocations
367: // under this scenario, if we just use JoinPointMatch as the key, then
368: // 'last man wins' which is not what we want at all.
369: // Using the expression is guaranteed to be safe, since 2 identical expressions
370: // are guaranteed to bind in exactly the same way.
371: invocation.setUserAttribute(getExpression(), jpm);
372: }
373:
374: private ShadowMatch getShadowMatch(Method targetMethod,
375: Method originalMethod) {
376: synchronized (this .shadowMapCache) {
377: ShadowMatch shadowMatch = (ShadowMatch) this .shadowMapCache
378: .get(targetMethod);
379: if (shadowMatch == null) {
380: try {
381: shadowMatch = this .pointcutExpression
382: .matchesMethodExecution(targetMethod);
383: } catch (ReflectionWorld.ReflectionWorldException ex) {
384: // Failed to introspect target method, probably because it has been loaded
385: // in a special ClassLoader. Let's try the original method instead...
386: if (targetMethod == originalMethod) {
387: throw ex;
388: }
389: shadowMatch = this .pointcutExpression
390: .matchesMethodExecution(originalMethod);
391: }
392: this .shadowMapCache.put(targetMethod, shadowMatch);
393: }
394: return shadowMatch;
395: }
396: }
397:
398: public boolean equals(Object other) {
399: if (this == other) {
400: return true;
401: }
402: if (!(other instanceof AspectJExpressionPointcut)) {
403: return false;
404: }
405: AspectJExpressionPointcut otherPc = (AspectJExpressionPointcut) other;
406: return ObjectUtils.nullSafeEquals(this .getExpression(), otherPc
407: .getExpression())
408: && ObjectUtils.nullSafeEquals(
409: this .pointcutDeclarationScope,
410: otherPc.pointcutDeclarationScope)
411: && ObjectUtils.nullSafeEquals(
412: this .pointcutParameterNames,
413: otherPc.pointcutParameterNames)
414: && ObjectUtils.nullSafeEquals(
415: this .pointcutParameterTypes,
416: otherPc.pointcutParameterTypes);
417: }
418:
419: public int hashCode() {
420: int hashCode = ObjectUtils.nullSafeHashCode(this
421: .getExpression());
422: hashCode = 31
423: * hashCode
424: + ObjectUtils
425: .nullSafeHashCode(this .pointcutDeclarationScope);
426: hashCode = 31
427: * hashCode
428: + ObjectUtils
429: .nullSafeHashCode(this .pointcutParameterNames);
430: hashCode = 31
431: * hashCode
432: + ObjectUtils
433: .nullSafeHashCode(this .pointcutParameterTypes);
434: return hashCode;
435: }
436:
437: public String toString() {
438: StringBuffer sb = new StringBuffer();
439: sb.append("AspectJExpressionPointcut: ");
440: if (this .pointcutParameterNames != null
441: && this .pointcutParameterTypes != null) {
442: sb.append("(");
443: for (int i = 0; i < this .pointcutParameterTypes.length; i++) {
444: sb.append(this .pointcutParameterTypes[i].getName());
445: sb.append(" ");
446: sb.append(this .pointcutParameterNames[i]);
447: if ((i + 1) < this .pointcutParameterTypes.length) {
448: sb.append(", ");
449: }
450: }
451: sb.append(")");
452: }
453: sb.append(" ");
454: if (getExpression() != null) {
455: sb.append(getExpression());
456: } else {
457: sb.append("<pointcut expression not set>");
458: }
459: return sb.toString();
460: }
461:
462: /**
463: * Handler for the Spring-specific <code>bean()</code> pointcut designator
464: * extension to AspectJ.
465: * <p>This handler must be added to each pointcut object that needs to
466: * handle the <code>bean()</code> PCD. Matching context is obtained
467: * automatically by examining a thread local variable and therefore a matching
468: * context need not be set on the pointcut.
469: */
470: private class BeanNamePointcutDesignatorHandler implements
471: PointcutDesignatorHandler {
472:
473: private static final String BEAN_DESIGNATOR_NAME = "bean";
474:
475: public String getDesignatorName() {
476: return BEAN_DESIGNATOR_NAME;
477: }
478:
479: public ContextBasedMatcher parse(String expression) {
480: return new BeanNameContextMatcher(expression);
481: }
482: }
483:
484: /**
485: * Matcher class for the BeanNamePointcutDesignatorHandler.
486: *
487: * Static and dynamic match tests for this matcher always return true,
488: * since the matching decision is made at the proxy creation time.
489: */
490: private class BeanNameContextMatcher implements ContextBasedMatcher {
491:
492: private final NamePattern expressionPattern;
493:
494: public BeanNameContextMatcher(String expression) {
495: this .expressionPattern = new NamePattern(expression);
496: }
497:
498: public boolean couldMatchJoinPointsInType(Class someClass) {
499: return true;
500: }
501:
502: public boolean couldMatchJoinPointsInType(Class someClass,
503: MatchingContext context) {
504: return contextMatch();
505: }
506:
507: public boolean matchesDynamically(MatchingContext context) {
508: return true;
509: }
510:
511: public FuzzyBoolean matchesStatically(MatchingContext context) {
512: return FuzzyBoolean.YES;
513: }
514:
515: public boolean mayNeedDynamicTest() {
516: return false;
517: }
518:
519: private boolean contextMatch() {
520: String advisedBeanName = getCurrentProxiedBeanName();
521: return (advisedBeanName != null
522: && !BeanFactoryUtils
523: .isGeneratedBeanName(advisedBeanName) && this.expressionPattern
524: .matches(advisedBeanName));
525: }
526: }
527:
528: }
|