All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.aop.aspectj.AspectJExpressionPointcut Maven / Gradle / Ivy

There is a newer version: 6.2.0
Show newest version
/*
 * Copyright 2002-2024 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
 *
 *      https://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.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.weaver.patterns.NamePattern;
import org.aspectj.weaver.reflect.ReflectionWorld.ReflectionWorldException;
import org.aspectj.weaver.reflect.ShadowMatchImpl;
import org.aspectj.weaver.tools.ContextBasedMatcher;
import org.aspectj.weaver.tools.FuzzyBoolean;
import org.aspectj.weaver.tools.JoinPointMatch;
import org.aspectj.weaver.tools.MatchingContext;
import org.aspectj.weaver.tools.PointcutDesignatorHandler;
import org.aspectj.weaver.tools.PointcutExpression;
import org.aspectj.weaver.tools.PointcutParameter;
import org.aspectj.weaver.tools.PointcutParser;
import org.aspectj.weaver.tools.PointcutPrimitive;
import org.aspectj.weaver.tools.ShadowMatch;

import org.springframework.aop.ClassFilter;
import org.springframework.aop.IntroductionAwareMethodMatcher;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.ProxyMethodInvocation;
import org.springframework.aop.framework.autoproxy.ProxyCreationContext;
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
import org.springframework.aop.support.AbstractExpressionPointcut;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * Spring {@link org.springframework.aop.Pointcut} implementation
 * that uses the AspectJ weaver to evaluate a pointcut expression.
 *
 * 

The pointcut expression value is an AspectJ expression. This can * reference other pointcuts and use composition and other operations. * *

Naturally, as this is to be processed by Spring AOP's proxy-based model, * only method execution pointcuts are supported. * * @author Rob Harrop * @author Adrian Colyer * @author Rod Johnson * @author Juergen Hoeller * @author Ramnivas Laddad * @author Dave Syer * @since 2.0 */ @SuppressWarnings("serial") public class AspectJExpressionPointcut extends AbstractExpressionPointcut implements ClassFilter, IntroductionAwareMethodMatcher, BeanFactoryAware { private static final Set SUPPORTED_PRIMITIVES = Set.of( PointcutPrimitive.EXECUTION, PointcutPrimitive.ARGS, PointcutPrimitive.REFERENCE, PointcutPrimitive.THIS, PointcutPrimitive.TARGET, PointcutPrimitive.WITHIN, PointcutPrimitive.AT_ANNOTATION, PointcutPrimitive.AT_WITHIN, PointcutPrimitive.AT_ARGS, PointcutPrimitive.AT_TARGET); private static final Log logger = LogFactory.getLog(AspectJExpressionPointcut.class); @Nullable private Class pointcutDeclarationScope; private String[] pointcutParameterNames = new String[0]; private Class[] pointcutParameterTypes = new Class[0]; @Nullable private BeanFactory beanFactory; @Nullable private transient ClassLoader pointcutClassLoader; @Nullable private transient PointcutExpression pointcutExpression; private transient Map shadowMatchCache = new ConcurrentHashMap<>(32); /** * Create a new default AspectJExpressionPointcut. */ public AspectJExpressionPointcut() { } /** * Create a new AspectJExpressionPointcut with the given settings. * @param declarationScope the declaration scope for the pointcut * @param paramNames the parameter names for the pointcut * @param paramTypes the parameter types for the pointcut */ public AspectJExpressionPointcut(Class declarationScope, String[] paramNames, Class[] paramTypes) { this.pointcutDeclarationScope = declarationScope; if (paramNames.length != paramTypes.length) { throw new IllegalStateException( "Number of pointcut parameter names must match number of pointcut parameter types"); } this.pointcutParameterNames = paramNames; this.pointcutParameterTypes = paramTypes; } /** * Set the declaration scope for the pointcut. */ public void setPointcutDeclarationScope(Class pointcutDeclarationScope) { this.pointcutDeclarationScope = pointcutDeclarationScope; } /** * Set the parameter names for the pointcut. */ public void setParameterNames(String... names) { this.pointcutParameterNames = names; } /** * Set the parameter types for the pointcut. */ public void setParameterTypes(Class... types) { this.pointcutParameterTypes = types; } @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } @Override public ClassFilter getClassFilter() { obtainPointcutExpression(); return this; } @Override public MethodMatcher getMethodMatcher() { obtainPointcutExpression(); return this; } /** * Check whether this pointcut is ready to match, * lazily building the underlying AspectJ pointcut expression. */ private PointcutExpression obtainPointcutExpression() { if (getExpression() == null) { throw new IllegalStateException("Must set property 'expression' before attempting to match"); } if (this.pointcutExpression == null) { this.pointcutClassLoader = determinePointcutClassLoader(); this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader); } return this.pointcutExpression; } /** * Determine the ClassLoader to use for pointcut evaluation. */ @Nullable private ClassLoader determinePointcutClassLoader() { if (this.beanFactory instanceof ConfigurableBeanFactory cbf) { return cbf.getBeanClassLoader(); } if (this.pointcutDeclarationScope != null) { return this.pointcutDeclarationScope.getClassLoader(); } return ClassUtils.getDefaultClassLoader(); } /** * Build the underlying AspectJ pointcut expression. */ private PointcutExpression buildPointcutExpression(@Nullable ClassLoader classLoader) { PointcutParser parser = initializePointcutParser(classLoader); PointcutParameter[] pointcutParameters = new PointcutParameter[this.pointcutParameterNames.length]; for (int i = 0; i < pointcutParameters.length; i++) { pointcutParameters[i] = parser.createPointcutParameter( this.pointcutParameterNames[i], this.pointcutParameterTypes[i]); } return parser.parsePointcutExpression(replaceBooleanOperators(resolveExpression()), this.pointcutDeclarationScope, pointcutParameters); } private String resolveExpression() { String expression = getExpression(); Assert.state(expression != null, "No expression set"); return expression; } /** * Initialize the underlying AspectJ pointcut parser. */ private PointcutParser initializePointcutParser(@Nullable ClassLoader classLoader) { PointcutParser parser = PointcutParser .getPointcutParserSupportingSpecifiedPrimitivesAndUsingSpecifiedClassLoaderForResolution( SUPPORTED_PRIMITIVES, classLoader); parser.registerPointcutDesignatorHandler(new BeanPointcutDesignatorHandler()); return parser; } /** * If a pointcut expression has been specified in XML, the user cannot * write "and" as "&&" (though {@code &&} will work). *

We also allow "and" between two pointcut sub-expressions. *

This method converts back to {@code &&} for the AspectJ pointcut parser. */ private String replaceBooleanOperators(String pcExpr) { String result = StringUtils.replace(pcExpr, " and ", " && "); result = StringUtils.replace(result, " or ", " || "); result = StringUtils.replace(result, " not ", " ! "); return result; } /** * Return the underlying AspectJ pointcut expression. */ public PointcutExpression getPointcutExpression() { return obtainPointcutExpression(); } @Override public boolean matches(Class targetClass) { PointcutExpression pointcutExpression = obtainPointcutExpression(); try { try { return pointcutExpression.couldMatchJoinPointsInType(targetClass); } catch (ReflectionWorldException ex) { logger.debug("PointcutExpression matching rejected target class - trying fallback expression", ex); // Actually this is still a "maybe" - treat the pointcut as dynamic if we don't know enough yet PointcutExpression fallbackExpression = getFallbackPointcutExpression(targetClass); if (fallbackExpression != null) { return fallbackExpression.couldMatchJoinPointsInType(targetClass); } } } catch (Throwable ex) { logger.debug("PointcutExpression matching rejected target class", ex); } return false; } @Override public boolean matches(Method method, Class targetClass, boolean hasIntroductions) { obtainPointcutExpression(); ShadowMatch shadowMatch = getTargetShadowMatch(method, targetClass); // Special handling for this, target, @this, @target, @annotation // in Spring - we can optimize since we know we have exactly this class, // and there will never be matching subclass at runtime. if (shadowMatch.alwaysMatches()) { return true; } else if (shadowMatch.neverMatches()) { return false; } else { // the maybe case if (hasIntroductions) { return true; } // A match test returned maybe - if there are any subtype sensitive variables // involved in the test (this, target, at_this, at_target, at_annotation) then // we say this is not a match as in Spring there will never be a different // runtime subtype. RuntimeTestWalker walker = getRuntimeTestWalker(shadowMatch); return (!walker.testsSubtypeSensitiveVars() || walker.testTargetInstanceOfResidue(targetClass)); } } @Override public boolean matches(Method method, Class targetClass) { return matches(method, targetClass, false); } @Override public boolean isRuntime() { return obtainPointcutExpression().mayNeedDynamicTest(); } @Override public boolean matches(Method method, Class targetClass, Object... args) { obtainPointcutExpression(); ShadowMatch shadowMatch = getTargetShadowMatch(method, targetClass); // Bind Spring AOP proxy to AspectJ "this" and Spring AOP target to AspectJ target, // consistent with return of MethodInvocationProceedingJoinPoint ProxyMethodInvocation pmi = null; Object targetObject = null; Object thisObject = null; try { MethodInvocation curr = ExposeInvocationInterceptor.currentInvocation(); targetObject = curr.getThis(); if (!(curr instanceof ProxyMethodInvocation currPmi)) { throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + curr); } pmi = currPmi; thisObject = pmi.getProxy(); } catch (IllegalStateException ex) { // No current invocation... if (logger.isDebugEnabled()) { logger.debug("Could not access current invocation - matching with limited context: " + ex); } } try { JoinPointMatch joinPointMatch = shadowMatch.matchesJoinPoint(thisObject, targetObject, args); /* * Do a final check to see if any this(TYPE) kind of residue match. For * this purpose, we use the original method's (proxy method's) shadow to * ensure that 'this' is correctly checked against. Without this check, * we get incorrect match on this(TYPE) where TYPE matches the target * type but not 'this' (as would be the case of JDK dynamic proxies). *

See SPR-2979 for the original bug. */ if (pmi != null && thisObject != null) { // there is a current invocation RuntimeTestWalker originalMethodResidueTest = getRuntimeTestWalker(getShadowMatch(method, method)); if (!originalMethodResidueTest.testThisInstanceOfResidue(thisObject.getClass())) { return false; } if (joinPointMatch.matches()) { bindParameters(pmi, joinPointMatch); } } return joinPointMatch.matches(); } catch (Throwable ex) { if (logger.isDebugEnabled()) { logger.debug("Failed to evaluate join point for arguments " + Arrays.toString(args) + " - falling back to non-match", ex); } return false; } } @Nullable protected String getCurrentProxiedBeanName() { return ProxyCreationContext.getCurrentProxiedBeanName(); } /** * Get a new pointcut expression based on a target class's loader rather than the default. */ @Nullable private PointcutExpression getFallbackPointcutExpression(Class targetClass) { try { ClassLoader classLoader = targetClass.getClassLoader(); if (classLoader != null && classLoader != this.pointcutClassLoader) { return buildPointcutExpression(classLoader); } } catch (Throwable ex) { logger.debug("Failed to create fallback PointcutExpression", ex); } return null; } private RuntimeTestWalker getRuntimeTestWalker(ShadowMatch shadowMatch) { if (shadowMatch instanceof DefensiveShadowMatch defensiveShadowMatch) { return new RuntimeTestWalker(defensiveShadowMatch.primary); } return new RuntimeTestWalker(shadowMatch); } private void bindParameters(ProxyMethodInvocation invocation, JoinPointMatch jpm) { // Note: 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. invocation.setUserAttribute(resolveExpression(), jpm); } private ShadowMatch getTargetShadowMatch(Method method, Class targetClass) { Method targetMethod = AopUtils.getMostSpecificMethod(method, targetClass); if (targetMethod.getDeclaringClass().isInterface() && targetMethod.getDeclaringClass() != targetClass && obtainPointcutExpression().getPointcutExpression().contains("." + targetMethod.getName() + "(")) { // Try to build the most specific interface possible for inherited methods to be // considered for sub-interface matches as well, in particular for proxy classes. // Note: AspectJ is only going to take Method.getDeclaringClass() into account. Set> ifcs = ClassUtils.getAllInterfacesForClassAsSet(targetClass); if (ifcs.size() > 1) { try { Class compositeInterface = ClassUtils.createCompositeInterface( ClassUtils.toClassArray(ifcs), targetClass.getClassLoader()); targetMethod = ClassUtils.getMostSpecificMethod(targetMethod, compositeInterface); } catch (IllegalArgumentException ex) { // Implemented interfaces probably expose conflicting method signatures... // Proceed with original target method. } } } return getShadowMatch(targetMethod, method); } private ShadowMatch getShadowMatch(Method targetMethod, Method originalMethod) { // Avoid lock contention for known Methods through concurrent access... ShadowMatch shadowMatch = this.shadowMatchCache.get(targetMethod); if (shadowMatch == null) { synchronized (this.shadowMatchCache) { // Not found - now check again with full lock... PointcutExpression fallbackExpression = null; shadowMatch = this.shadowMatchCache.get(targetMethod); if (shadowMatch == null) { Method methodToMatch = targetMethod; try { try { shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch); } catch (ReflectionWorldException ex) { // Failed to introspect target method, probably because it has been loaded // in a special ClassLoader. Let's try the declaring ClassLoader instead... try { fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass()); if (fallbackExpression != null) { shadowMatch = fallbackExpression.matchesMethodExecution(methodToMatch); } } catch (ReflectionWorldException ex2) { fallbackExpression = null; } } if (targetMethod != originalMethod && (shadowMatch == null || (shadowMatch.neverMatches() && Proxy.isProxyClass(targetMethod.getDeclaringClass())))) { // Fall back to the plain original method in case of no resolvable match or a // negative match on a proxy class (which doesn't carry any annotations on its // redeclared methods). methodToMatch = originalMethod; try { shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch); } catch (ReflectionWorldException ex) { // Could neither introspect the target class nor the proxy class -> // let's try the original method's declaring class before we give up... try { fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass()); if (fallbackExpression != null) { shadowMatch = fallbackExpression.matchesMethodExecution(methodToMatch); } } catch (ReflectionWorldException ex2) { fallbackExpression = null; } } } } catch (Throwable ex) { // Possibly AspectJ 1.8.10 encountering an invalid signature logger.debug("PointcutExpression matching rejected target method", ex); fallbackExpression = null; } if (shadowMatch == null) { shadowMatch = new ShadowMatchImpl(org.aspectj.util.FuzzyBoolean.NO, null, null, null); } else if (shadowMatch.maybeMatches() && fallbackExpression != null) { shadowMatch = new DefensiveShadowMatch(shadowMatch, fallbackExpression.matchesMethodExecution(methodToMatch)); } this.shadowMatchCache.put(targetMethod, shadowMatch); } } } return shadowMatch; } @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof AspectJExpressionPointcut that && ObjectUtils.nullSafeEquals(getExpression(), that.getExpression()) && ObjectUtils.nullSafeEquals(this.pointcutDeclarationScope, that.pointcutDeclarationScope) && ObjectUtils.nullSafeEquals(this.pointcutParameterNames, that.pointcutParameterNames) && ObjectUtils.nullSafeEquals(this.pointcutParameterTypes, that.pointcutParameterTypes))); } @Override public int hashCode() { int hashCode = ObjectUtils.nullSafeHashCode(getExpression()); hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.pointcutDeclarationScope); hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.pointcutParameterNames); hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.pointcutParameterTypes); return hashCode; } @Override public String toString() { StringBuilder sb = new StringBuilder("AspectJExpressionPointcut: ("); for (int i = 0; i < this.pointcutParameterTypes.length; i++) { sb.append(this.pointcutParameterTypes[i].getName()); sb.append(' '); sb.append(this.pointcutParameterNames[i]); if ((i+1) < this.pointcutParameterTypes.length) { sb.append(", "); } } sb.append(") "); if (getExpression() != null) { sb.append(getExpression()); } else { sb.append(""); } return sb.toString(); } //--------------------------------------------------------------------- // Serialization support //--------------------------------------------------------------------- private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { // Rely on default serialization, just initialize state after deserialization. ois.defaultReadObject(); // Initialize transient fields. // pointcutExpression will be initialized lazily by checkReadyToMatch() this.shadowMatchCache = new ConcurrentHashMap<>(32); } /** * Handler for the Spring-specific {@code bean()} pointcut designator * extension to AspectJ. *

This handler must be added to each pointcut object that needs to * handle the {@code bean()} PCD. Matching context is obtained * automatically by examining a thread local variable and therefore a matching * context need not be set on the pointcut. */ private class BeanPointcutDesignatorHandler implements PointcutDesignatorHandler { private static final String BEAN_DESIGNATOR_NAME = "bean"; @Override public String getDesignatorName() { return BEAN_DESIGNATOR_NAME; } @Override public ContextBasedMatcher parse(String expression) { return new BeanContextMatcher(expression); } } /** * Matcher class for the BeanNamePointcutDesignatorHandler. *

Dynamic match tests for this matcher always return true, * since the matching decision is made at the proxy creation time. * For static match tests, this matcher abstains to allow the overall * pointcut to match even when negation is used with the bean() pointcut. */ private class BeanContextMatcher implements ContextBasedMatcher { private final NamePattern expressionPattern; public BeanContextMatcher(String expression) { this.expressionPattern = new NamePattern(expression); } @Override @SuppressWarnings("rawtypes") @Deprecated public boolean couldMatchJoinPointsInType(Class someClass) { return (contextMatch(someClass) == FuzzyBoolean.YES); } @Override @SuppressWarnings("rawtypes") @Deprecated public boolean couldMatchJoinPointsInType(Class someClass, MatchingContext context) { return (contextMatch(someClass) == FuzzyBoolean.YES); } @Override public boolean matchesDynamically(MatchingContext context) { return true; } @Override public FuzzyBoolean matchesStatically(MatchingContext context) { return contextMatch(null); } @Override public boolean mayNeedDynamicTest() { return false; } private FuzzyBoolean contextMatch(@Nullable Class targetType) { String advisedBeanName = getCurrentProxiedBeanName(); if (advisedBeanName == null) { // no proxy creation in progress // abstain; can't return YES, since that will make pointcut with negation fail return FuzzyBoolean.MAYBE; } if (BeanFactoryUtils.isGeneratedBeanName(advisedBeanName)) { return FuzzyBoolean.NO; } if (targetType != null) { boolean isFactory = FactoryBean.class.isAssignableFrom(targetType); return FuzzyBoolean.fromBoolean( matchesBean(isFactory ? BeanFactory.FACTORY_BEAN_PREFIX + advisedBeanName : advisedBeanName)); } else { return FuzzyBoolean.fromBoolean(matchesBean(advisedBeanName) || matchesBean(BeanFactory.FACTORY_BEAN_PREFIX + advisedBeanName)); } } private boolean matchesBean(String advisedBeanName) { return BeanFactoryAnnotationUtils.isQualifierMatch( this.expressionPattern::matches, advisedBeanName, beanFactory); } } private static class DefensiveShadowMatch implements ShadowMatch { private final ShadowMatch primary; private final ShadowMatch other; public DefensiveShadowMatch(ShadowMatch primary, ShadowMatch other) { this.primary = primary; this.other = other; } @Override public boolean alwaysMatches() { return this.primary.alwaysMatches(); } @Override public boolean maybeMatches() { return this.primary.maybeMatches(); } @Override public boolean neverMatches() { return this.primary.neverMatches(); } @Override public JoinPointMatch matchesJoinPoint(Object thisObject, Object targetObject, Object[] args) { try { return this.primary.matchesJoinPoint(thisObject, targetObject, args); } catch (ReflectionWorldException ex) { return this.other.matchesJoinPoint(thisObject, targetObject, args); } } @Override public void setMatchingContext(MatchingContext aMatchContext) { this.primary.setMatchingContext(aMatchContext); this.other.setMatchingContext(aMatchContext); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy