org.springframework.aop.aspectj.AbstractAspectJAdvice Maven / Gradle / Ivy
/*
* Copyright 2002-2007 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.Modifier;
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.JdkVersion;
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.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();
private static final boolean ASM_AVAILABLE =
ClassUtils.isPresent("org.objectweb.asm.ClassReader", AbstractAspectJAdvice.class.getClassLoader());
/**
* Lazily instantiate joinpoint for the current invocation.
* Requires MethodInvocation to be bound with ExposeInvocationInterceptor.
* 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 = (ProxyMethodInvocation) mi;
JoinPoint jp = (JoinPoint) pmi.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 argumentBindings = null;
private boolean argumentsIntrospected = false;
// The actual type is java.lang.reflect.Type,
// but for JDK 1.4 compatibility we use Object as the static type.
private Object 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;
}
public int getOrder() {
return this.aspectInstanceFactory.getOrder();
}
public void setAspectName(String name) {
this.aspectName = name;
}
public String getAspectName() {
return this.aspectName;
}
/**
* Sets the declaration order 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.
*
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");
}
}
}
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);
}
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 Object 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);
}
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.
*
If the first argument is of type JoinPoint or ProceedingJoinPoint then we
* pass a JoinPoint in that position (ProceedingJoinPoint for around advice).
*
If the first argument is of type JoinPoint.StaticPart
* then we pass a JoinPoint.StaticPart
in that position.
*
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])) {
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) || candidateParameterType.equals(ProceedingJoinPoint.class)) {
this.joinPointArgumentIndex = 0;
return true;
}
else {
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.
*
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();
if (ASM_AVAILABLE) {
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();
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],new Integer(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 = (Integer) this.argumentBindings.get(this.returningName);
this.discoveredReturningType = this.aspectJAdviceMethod.getParameterTypes()[index.intValue()];
if (JdkVersion.isAtLeastJava15()) {
this.discoveredReturningGenericType =
this.aspectJAdviceMethod.getGenericParameterTypes()[index.intValue()];
}
}
}
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 = (Integer) this.argumentBindings.get(this.throwingName);
this.discoveredThrowingType = this.aspectJAdviceMethod.getParameterTypes()[index.intValue()];
}
}
// 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[index] = this.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 (int i = 0; i < parameterBindings.length; i++) {
PointcutParameter parameter = parameterBindings[i];
String name = parameter.getName();
Integer index = (Integer) this.argumentBindings.get(name);
adviceInvocationArgs[index.intValue()] = parameter.getBinding();
numBound++;
}
}
// binding from returning clause
if (this.returningName != null) {
Integer index = (Integer) this.argumentBindings.get(this.returningName);
adviceInvocationArgs[index.intValue()] = returnValue;
numBound++;
}
// binding from thrown exception
if (this.throwingName != null) {
Integer index = (Integer) this.argumentBindings.get(this.throwingName);
adviceInvocationArgs[index.intValue()] = 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 ex) throws 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[] args) throws Throwable {
Object[] actualArgs = args;
if (this.aspectJAdviceMethod.getParameterTypes().length == 0) {
actualArgs = null;
}
try {
if (!Modifier.isPublic(this.aspectJAdviceMethod.getModifiers()) ||
!Modifier.isPublic(this.aspectJAdviceMethod.getDeclaringClass().getModifiers())) {
this.aspectJAdviceMethod.setAccessible(true);
}
// 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((ProxyMethodInvocation) mi);
}
// 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 (JoinPointMatch) pmi.getUserAttribute(this.pointcut.getExpression());
}
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);
}
}
}