Open Source Repository

Home /spring/spring-aop-3.0.5 | Repository Home



org/springframework/aop/aspectj/AbstractAspectJAdvice.java
/*
 * Copyright 2002-2010 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.aop.aspectj;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;

import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInvocation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.weaver.tools.JoinPointMatch;
import org.aspectj.weaver.tools.PointcutParameter;

import org.springframework.aop.AopInvocationException;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
import org.springframework.aop.ProxyMethodInvocation;
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
import org.springframework.aop.support.ComposablePointcut;
import org.springframework.aop.support.MethodMatchers;
import org.springframework.aop.support.StaticMethodMatcher;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.PrioritizedParameterNameDiscoverer;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

/**
 * Base class for AOP Alliance {@link org.aopalliance.aop.Advice} classes
 * wrapping an AspectJ aspect or an AspectJ-annotated advice method.
 *
 @author Rod Johnson
 @author Adrian Colyer
 @author Juergen Hoeller
 @author Ramnivas Laddad
 @since 2.0
 */
public abstract class AbstractAspectJAdvice implements Advice, AspectJPrecedenceInformation {

  /**
   * Key used in ReflectiveMethodInvocation userAtributes map for the current joinpoint.
   */
  protected static final String JOIN_POINT_KEY = JoinPoint.class.getName();


  /**
   * Lazily instantiate joinpoint for the current invocation.
   * Requires MethodInvocation to be bound with ExposeInvocationInterceptor.
   <p>Do not use if access is available to the current ReflectiveMethodInvocation
   * (in an around advice).
   @return current AspectJ joinpoint, or through an exception if we're not in a
   * Spring AOP invocation.
   */
  public static JoinPoint currentJoinPoint() {
    MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation();
    if (!(mi instanceof ProxyMethodInvocation)) {
      throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
    }
    ProxyMethodInvocation pmi = (ProxyMethodInvocationmi;
    JoinPoint jp = (JoinPointpmi.getUserAttribute(JOIN_POINT_KEY);
    if (jp == null) {
      jp = new MethodInvocationProceedingJoinPoint(pmi);
      pmi.setUserAttribute(JOIN_POINT_KEY, jp);
    }
    return jp;
  }


  protected final Method aspectJAdviceMethod;

  /** The total number of arguments we have to populate on advice dispatch */
  private final int adviceInvocationArgumentCount;

  private final AspectJExpressionPointcut pointcut;

  private final AspectInstanceFactory aspectInstanceFactory;

  /**
   * The name of the aspect (ref bean) in which this advice was defined (used
   * when determining advice precedence so that we can determine
   * whether two pieces of advice come from the same aspect).
   */
  private String aspectName;

  /**
   * The order of declaration of this advice within the aspect.
   */
  private int declarationOrder;

  /**
   * This will be non-null if the creator of this advice object knows the argument names
   * and sets them explicitly
   */
  private String[] argumentNames = null;

  /** Non-null if after throwing advice binds the thrown value */
  private String throwingName = null;

  /** Non-null if after returning advice binds the return value */
  private String returningName = null;

  private Class discoveredReturningType = Object.class;

  private Class discoveredThrowingType = Object.class;

  /**
   * Index for thisJoinPoint argument (currently only
   * supported at index 0 if present at all)
   */
  private int joinPointArgumentIndex = -1;

  /**
   * Index for thisJoinPointStaticPart argument (currently only
   * supported at index 0 if present at all)
   */
  private int joinPointStaticPartArgumentIndex = -1;

  private Map<String, Integer> argumentBindings = null;

  private boolean argumentsIntrospected = false;

  private Type discoveredReturningGenericType;
  // Note: Unlike return type, no such generic information is needed for the throwing type,
  // since Java doesn't allow exception types to be parameterized.


  /**
   * Create a new AbstractAspectJAdvice for the given advice method.
   @param aspectJAdviceMethod the AspectJ-style advice method
   @param pointcut the AspectJ expression pointcut
   @param aspectInstanceFactory the factory for aspect instances
   */
  public AbstractAspectJAdvice(
      Method aspectJAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aspectInstanceFactory) {

    Assert.notNull(aspectJAdviceMethod, "Advice method must not be null");
    this.aspectJAdviceMethod = aspectJAdviceMethod;
    this.adviceInvocationArgumentCount = this.aspectJAdviceMethod.getParameterTypes().length;
    this.pointcut = pointcut;
    this.aspectInstanceFactory = aspectInstanceFactory;
  }


  /**
   * Return the AspectJ-style advice method.
   */
  public final Method getAspectJAdviceMethod() {
    return this.aspectJAdviceMethod;
  }

  /**
   * Return the AspectJ expression pointcut.
   */
  public final AspectJExpressionPointcut getPointcut() {
    calculateArgumentBindings();
    return this.pointcut;
  }

  /**
   * Build a 'safe' pointcut that excludes the AspectJ advice method itself.
   @return a composable pointcut that builds on the original AspectJ expression pointcut
   @see #getPointcut()
   */
  public final Pointcut buildSafePointcut() {
    Pointcut pc = getPointcut();
    MethodMatcher safeMethodMatcher = MethodMatchers.intersection(
        new AdviceExcludingMethodMatcher(this.aspectJAdviceMethod), pc.getMethodMatcher());
    return new ComposablePointcut(pc.getClassFilter(), safeMethodMatcher);
  }

  /**
   * Return the factory for aspect instances.
   */
  public final AspectInstanceFactory getAspectInstanceFactory() {
    return this.aspectInstanceFactory;
  }

  /**
   * Return the ClassLoader for aspect instances.
   */
  public final ClassLoader getAspectClassLoader() {
    return this.aspectInstanceFactory.getAspectClassLoader();
  }

  public int getOrder() {
    return this.aspectInstanceFactory.getOrder();
  }


  public void setAspectName(String name) {
    this.aspectName = name;
  }
  
  public String getAspectName() {
    return this.aspectName;
  }

  /**
   * Sets the <b>declaration order</b> of this advice within the aspect
   */
  public void setDeclarationOrder(int order) {
    this.declarationOrder = order;
  }

  public int getDeclarationOrder() {
    return this.declarationOrder;
  }

  /**
   * Set by creator of this advice object if the argument names are known.
   <p>This could be for example because they have been explicitly specified in XML,
   * or in an advice annotation.
   @param argNames comma delimited list of arg names
   */
  public void setArgumentNames(String argNames) {
    String[] tokens = StringUtils.commaDelimitedListToStringArray(argNames);
    setArgumentNamesFromStringArray(tokens);
  }

  public void setArgumentNamesFromStringArray(String[] args) {
    this.argumentNames = new String[args.length];
    for (int i = 0; i < args.length; i++) {
      this.argumentNames[i= StringUtils.trimWhitespace(args[i]);
      if (!isVariableName(this.argumentNames[i])) {
        throw new IllegalArgumentException(
            "'argumentNames' property of AbstractAspectJAdvice contains an argument name '" +
            this.argumentNames[i"' that is not a valid Java identifier");
      }
    }
    if (argumentNames != null) {
      if (aspectJAdviceMethod.getParameterTypes().length == argumentNames.length + 1) {
        // May need to add implicit join point arg name...
        Class firstArgType = aspectJAdviceMethod.getParameterTypes()[0];
        if (firstArgType == JoinPoint.class ||
            firstArgType == ProceedingJoinPoint.class ||
            firstArgType == JoinPoint.StaticPart.class) {
          String[] oldNames = argumentNames;
          argumentNames = new String[oldNames.length + 1];
          argumentNames[0"THIS_JOIN_POINT";
          System.arraycopy(oldNames, 0, argumentNames, 1, oldNames.length);
        }
      }
    }
  }

  public void setReturningName(String name) {
    throw new UnsupportedOperationException("Only afterReturning advice can be used to bind a return value");
  }

  /** 
   * We need to hold the returning name at this level for argument binding calculations,
   * this method allows the afterReturning advice subclass to set the name.
   */
  protected void setReturningNameNoCheck(String name) {
    // name could be a variable or a type...
    if (isVariableName(name)) {
      this.returningName = name;
    }
    else {
      // assume a type
      try {
        this.discoveredReturningType = ClassUtils.forName(name, getAspectClassLoader());
      }
      catch (Throwable ex) {
        throw new IllegalArgumentException("Returning name '" + name  +
            "' is neither a valid argument name nor the fully-qualified name of a Java type on the classpath. " +
            "Root cause: " + ex);
      }
    }
  }

  protected Class getDiscoveredReturningType() {
    return this.discoveredReturningType;
  }

  protected Type getDiscoveredReturningGenericType() {
    return this.discoveredReturningGenericType;
  }

  public void setThrowingName(String name) {
    throw new UnsupportedOperationException("Only afterThrowing advice can be used to bind a thrown exception");
  }

  /** 
   * We need to hold the throwing name at this level for argument binding calculations,
   * this method allows the afterThrowing advice subclass to set the name.
   */
  protected void setThrowingNameNoCheck(String name) {
    // name could be a variable or a type...
    if (isVariableName(name)) {
      this.throwingName = name;
    }
    else {
      // assume a type
      try {
        this.discoveredThrowingType = ClassUtils.forName(name, getAspectClassLoader());
      }
      catch (Throwable ex) {
        throw new IllegalArgumentException("Throwing name '" + name  +
            "' is neither a valid argument name nor the fully-qualified name of a Java type on the classpath. " +
            "Root cause: " + ex);
      }
    }
  }

  protected Class getDiscoveredThrowingType() {
    return this.discoveredThrowingType;
  }

  private boolean isVariableName(String name) {
    char[] chars = name.toCharArray();
    if (!Character.isJavaIdentifierStart(chars[0])) {
      return false;
    }
    for (int i = 1; i < chars.length; i++) {
      if (!Character.isJavaIdentifierPart(chars[i])) {
        return false;
      }
    }
    return true;
  }


  /**
   * Do as much work as we can as part of the set-up so that argument binding
   * on subsequent advice invocations can be as fast as possible.
   <p>If the first argument is of type JoinPoint or ProceedingJoinPoint then we
   * pass a JoinPoint in that position (ProceedingJoinPoint for around advice).
   <p>If the first argument is of type <code>JoinPoint.StaticPart</code>
   * then we pass a <code>JoinPoint.StaticPart</code> in that position.
   <p>Remaining arguments have to be bound by pointcut evaluation at
   * a given join point. We will get back a map from argument name to
   * value. We need to calculate which advice parameter needs to be bound
   * to which argument name. There are multiple strategies for determining
   * this binding, which are arranged in a ChainOfResponsibility.
   */
  public synchronized final void calculateArgumentBindings() {
    // The simple case... nothing to bind.
    if (this.argumentsIntrospected || this.adviceInvocationArgumentCount == 0) {
      return;
    }

    int numUnboundArgs = this.adviceInvocationArgumentCount;
    Class[] parameterTypes = this.aspectJAdviceMethod.getParameterTypes();
    if (maybeBindJoinPoint(parameterTypes[0]) || maybeBindProceedingJoinPoint(parameterTypes[0])) {
      numUnboundArgs--;
    
    else if (maybeBindJoinPointStaticPart(parameterTypes[0])) {
      numUnboundArgs--;
    }
      
    if (numUnboundArgs > 0) {
      // need to bind arguments by name as returned from the pointcut match
      bindArgumentsByName(numUnboundArgs);
    }

    this.argumentsIntrospected = true;
  }

  private boolean maybeBindJoinPoint(Class candidateParameterType) {
    if (candidateParameterType.equals(JoinPoint.class)) {
      this.joinPointArgumentIndex = 0;
      return true;
    }
    else {
      return false;
    }
  }

  private boolean maybeBindProceedingJoinPoint(Class candidateParameterType) {
    if (candidateParameterType.equals(ProceedingJoinPoint.class)) {
      if (!supportsProceedingJoinPoint()) {
        throw new IllegalArgumentException("ProceedingJoinPoint is only supported for around advice");
      }
      this.joinPointArgumentIndex = 0;
      return true;
    }
    else {
      return false;
    }    
  }

  protected boolean supportsProceedingJoinPoint() {
    return false;
  }

  private boolean maybeBindJoinPointStaticPart(Class candidateParameterType) {
    if (candidateParameterType.equals(JoinPoint.StaticPart.class)) {
      this.joinPointStaticPartArgumentIndex = 0;
      return true;
    
    else {
      return false;
    }
  }

  private void bindArgumentsByName(int numArgumentsExpectingToBind) {
    if (this.argumentNames == null) {
      this.argumentNames = createParameterNameDiscoverer().getParameterNames(this.aspectJAdviceMethod);
    }
    if (this.argumentNames != null) {
      // We have been able to determine the arg names.
      bindExplicitArguments(numArgumentsExpectingToBind);
    
    else {
      throw new IllegalStateException("Advice method [" this.aspectJAdviceMethod.getName() "] " +
          "requires " + numArgumentsExpectingToBind + " arguments to be bound by name, but " +
          "the argument names were not specified and could not be discovered.");
    }
  }

  /**
   * Create a ParameterNameDiscoverer to be used for argument binding.
   <p>The default implementation creates a {@link PrioritizedParameterNameDiscoverer}
   * containing a {@link LocalVariableTableParameterNameDiscoverer} and an
   {@link AspectJAdviceParameterNameDiscoverer}.
   */
  protected ParameterNameDiscoverer createParameterNameDiscoverer() {
    // We need to discover them, or if that fails, guess,
    // and if we can't guess with 100% accuracy, fail.
    PrioritizedParameterNameDiscoverer discoverer = new PrioritizedParameterNameDiscoverer();
    discoverer.addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
    AspectJAdviceParameterNameDiscoverer adviceParameterNameDiscoverer =
        new AspectJAdviceParameterNameDiscoverer(this.pointcut.getExpression());
    adviceParameterNameDiscoverer.setReturningName(this.returningName);
    adviceParameterNameDiscoverer.setThrowingName(this.throwingName);
    // Last in chain, so if we're called and we fail, that's bad...
    adviceParameterNameDiscoverer.setRaiseExceptions(true);
    discoverer.addDiscoverer(adviceParameterNameDiscoverer);
    return discoverer;
  }

  private void bindExplicitArguments(int numArgumentsLeftToBind) {
    this.argumentBindings = new HashMap<String, Integer>();

    int numExpectedArgumentNames = this.aspectJAdviceMethod.getParameterTypes().length;
    if (this.argumentNames.length != numExpectedArgumentNames) {
      throw new IllegalStateException("Expecting to find " + numExpectedArgumentNames
          " arguments to bind by name in advice, but actually found " +
          this.argumentNames.length + " arguments.");
    }

    // So we match in number...
    int argumentIndexOffset = this.adviceInvocationArgumentCount - numArgumentsLeftToBind;
    for (int i = argumentIndexOffset; i < this.argumentNames.length; i++) {
      this.argumentBindings.put(this.argumentNames[i], i);
    }

    // Check that returning and throwing were in the argument names list if
    // specified, and find the discovered argument types.
    if (this.returningName != null) {
      if (!this.argumentBindings.containsKey(this.returningName)) {
        throw new IllegalStateException("Returning argument name '" 
            this.returningName + "' was not bound in advice arguments");
      
      else {
        Integer index = this.argumentBindings.get(this.returningName);
        this.discoveredReturningType = this.aspectJAdviceMethod.getParameterTypes()[index];
        this.discoveredReturningGenericType = this.aspectJAdviceMethod.getGenericParameterTypes()[index];
      }
    }
    if (this.throwingName != null) {
      if (!this.argumentBindings.containsKey(this.throwingName)) {
        throw new IllegalStateException("Throwing argument name '" 
            this.throwingName + "' was not bound in advice arguments");
      
      else {
        Integer index = this.argumentBindings.get(this.throwingName);
        this.discoveredThrowingType = this.aspectJAdviceMethod.getParameterTypes()[index];
      }
    }

    // configure the pointcut expression accordingly.
    configurePointcutParameters(argumentIndexOffset);
  }

  /**
   * All parameters from argumentIndexOffset onwards are candidates for
   * pointcut parameters - but returning and throwing vars are handled differently
   * and must be removed from the list if present.
   */
  private void configurePointcutParameters(int argumentIndexOffset) {
    int numParametersToRemove = argumentIndexOffset;
    if (this.returningName != null) {
      numParametersToRemove++;
    }
    if (this.throwingName != null) {
      numParametersToRemove++;
    }
    String[] pointcutParameterNames = new String[this.argumentNames.length - numParametersToRemove];
    Class[] pointcutParameterTypes = new Class[pointcutParameterNames.length];
    Class[] methodParameterTypes = this.aspectJAdviceMethod.getParameterTypes();

    int index = 0;
    for (int i = 0; i < this.argumentNames.length; i++) {
      if (i < argumentIndexOffset) {
        continue;
      }
      if (this.argumentNames[i].equals(this.returningName||
        this.argumentNames[i].equals(this.throwingName)) {
        continue;
      }
      pointcutParameterNames[indexthis.argumentNames[i];
      pointcutParameterTypes[index= methodParameterTypes[i];
      index++;
    }
    
    this.pointcut.setParameterNames(pointcutParameterNames);
    this.pointcut.setParameterTypes(pointcutParameterTypes);
  }

  /**
   * Take the arguments at the method execution join point and output a set of arguments
   * to the advice method
   @param jp the current JoinPoint
   @param jpMatch the join point match that matched this execution join point
   @param returnValue the return value from the method execution (may be null)
   @param ex the exception thrown by the method execution (may be null)
   @return the empty array if there are no arguments
   */
  protected Object[] argBinding(JoinPoint jp, JoinPointMatch jpMatch, Object returnValue, Throwable ex) {
    calculateArgumentBindings();

    // AMC start
    Object[] adviceInvocationArgs = new Object[this.adviceInvocationArgumentCount];
    int numBound = 0;

    if (this.joinPointArgumentIndex != -1) {
      adviceInvocationArgs[this.joinPointArgumentIndex= jp;
      numBound++;
    
    else if (this.joinPointStaticPartArgumentIndex != -1) {
      adviceInvocationArgs[this.joinPointStaticPartArgumentIndex= jp.getStaticPart();
      numBound++;
    }

    if (!CollectionUtils.isEmpty(this.argumentBindings)) {
      // binding from pointcut match
      if (jpMatch != null) {
        PointcutParameter[] parameterBindings = jpMatch.getParameterBindings();
        for (PointcutParameter parameter : parameterBindings) {
          String name = parameter.getName();
          Integer index = this.argumentBindings.get(name);
          adviceInvocationArgs[index= parameter.getBinding();
          numBound++;
        }
      }
      // binding from returning clause
      if (this.returningName != null) {
        Integer index = this.argumentBindings.get(this.returningName);
        adviceInvocationArgs[index= returnValue;
        numBound++;
      }
      // binding from thrown exception
      if (this.throwingName != null) {
        Integer index = this.argumentBindings.get(this.throwingName);
        adviceInvocationArgs[index= ex;
        numBound++;
      }
    }

    if (numBound != this.adviceInvocationArgumentCount) {
      throw new IllegalStateException("Required to bind " this.adviceInvocationArgumentCount
          " arguments, but only bound " + numBound + " (JoinPointMatch " 
          (jpMatch == null "was NOT" "WAS"
          " bound in invocation)");
    }

    return adviceInvocationArgs;
  }


  /**
   * Invoke the advice method.
   @param jpMatch the JoinPointMatch that matched this execution join point
   @param returnValue the return value from the method execution (may be null)
   @param ex the exception thrown by the method execution (may be null)
   @return the invocation result
   @throws Throwable in case of invocation failure
   */
  protected Object invokeAdviceMethod(JoinPointMatch jpMatch, Object returnValue, Throwable exthrows Throwable {
    return invokeAdviceMethodWithGivenArgs(argBinding(getJoinPoint(), jpMatch, returnValue, ex));
  }

  // As above, but in this case we are given the join point.
  protected Object invokeAdviceMethod(JoinPoint jp, JoinPointMatch jpMatch, Object returnValue, Throwable t)
      throws Throwable {

    return invokeAdviceMethodWithGivenArgs(argBinding(jp, jpMatch, returnValue, t));
  }

  protected Object invokeAdviceMethodWithGivenArgs(Object[] argsthrows Throwable {
    Object[] actualArgs = args;
    if (this.aspectJAdviceMethod.getParameterTypes().length == 0) {
      actualArgs = null;
    }
    try {
      ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
      // TODO AopUtils.invokeJoinpointUsingReflection
      return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
    }
    catch (IllegalArgumentException ex) {
      throw new AopInvocationException("Mismatch on arguments to advice method [" +
          this.aspectJAdviceMethod + "]; pointcut expression [" +
          this.pointcut.getPointcutExpression() "]", ex);
    }
    catch (InvocationTargetException ex) {
      throw ex.getTargetException();
    }
  }

  /**
   * Overridden in around advice to return proceeding join point.
   */
  protected JoinPoint getJoinPoint() {
    return currentJoinPoint();
  }

  /**
   * Get the current join point match at the join point we are being dispatched on.
   */
  protected JoinPointMatch getJoinPointMatch() {
    MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation();
    if (!(mi instanceof ProxyMethodInvocation)) {
      throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
    }
    return getJoinPointMatch((ProxyMethodInvocationmi);
  }

  // Note: We can't use JoinPointMatch.getClass().getName() as the key, since
  // Spring AOP does all the matching at a join point, and then all the invocations.
  // Under this scenario, if we just use JoinPointMatch as the key, then
  // 'last man wins' which is not what we want at all.
  // Using the expression is guaranteed to be safe, since 2 identical expressions
  // are guaranteed to bind in exactly the same way.
  protected JoinPointMatch getJoinPointMatch(ProxyMethodInvocation pmi) {
    return (JoinPointMatchpmi.getUserAttribute(this.pointcut.getExpression());
  }


  @Override
  public String toString() {
    return getClass().getName() ": advice method [" this.aspectJAdviceMethod + "]; " +
        "aspect name '" this.aspectName + "'";
  }


  /**
   * MethodMatcher that excludes the specified advice method.
   @see AbstractAspectJAdvice#buildSafePointcut()
   */
  private static class AdviceExcludingMethodMatcher extends StaticMethodMatcher {

    private final Method adviceMethod;

    public AdviceExcludingMethodMatcher(Method adviceMethod) {
      this.adviceMethod = adviceMethod;
    }

    public boolean matches(Method method, Class targetClass) {
      return !this.adviceMethod.equals(method);
    }
  }

}