在上一章节中我们初步了解 Spring AOP,包括 Spring AOP 的基本概念以及使用,本文将对 AOP 核心概念进行解读。
连接点 - Joinpoint
连接点是指程序执行过程中的一些点,比如方法调用,异常处理等。在 Spring AOP 中,仅支持方法级别的连接点。以上是官方说明,通俗地讲就是能够被拦截的地方,每个成员方法都可以称之为连接点。我们还是借用租房的案例做分析。
Rent 接口:
public interface Rent { //租房 public void rent(String address); //买家具 public void buyFurniture(); } 复制代码
该接口的实现类是 Host,即房东类,具体实现如下:
@Component public class Host implements Rent { @Override public void rent(String address) { System.out.println("出租位于"+address+"处的房屋"); } @Override public void buyFurniture() { System.out.println("添置家具"); } } 复制代码
其中 rent()方法即为一个连接点。
接下来我们看看连接点的定义:
public interface JoinPoint { String METHOD_EXECUTION = "method-execution"; String METHOD_CALL = "method-call"; String CONSTRUCTOR_EXECUTION = "constructor-execution"; String CONSTRUCTOR_CALL = "constructor-call"; String FIELD_GET = "field-get"; String FIELD_SET = "field-set"; String STATICINITIALIZATION = "staticinitialization"; String PREINITIALIZATION = "preinitialization"; String INITIALIZATION = "initialization"; String EXCEPTION_HANDLER = "exception-handler"; String SYNCHRONIZATION_LOCK = "lock"; String SYNCHRONIZATION_UNLOCK = "unlock"; String ADVICE_EXECUTION = "adviceexecution"; String toString(); String toShortString(); String toLongString(); //获取代理对象 Object getThis(); /** 返回目标对象。该对象将始终与target切入点指示符匹配的对象相同。除非您特别需要此反射访问,否则应使用 target切入点指示符到达此对象,以获得更好的静态类型和性能。 如果没有目标对象,则返回null。 **/ Object getTarget(); //获取传入目标方法的参数对象 Object[] getArgs(); //获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息 Signature getSignature(); /** 返回与连接点对应的源位置。 如果没有可用的源位置,则返回null。 返回默认构造函数的定义类的SourceLocation。 **/ SourceLocation getSourceLocation(); String getKind(); JoinPoint.StaticPart getStaticPart(); public interface EnclosingStaticPart extends JoinPoint.StaticPart { } //该帮助对象仅包含有关连接点的静态信息。它可以从JoinPoint.getStaticPart()方法中获得,也可以使用特殊形式在建议中单独访问 thisJoinPointStaticPart。 public interface StaticPart { Signature getSignature(); SourceLocation getSourceLocation(); String getKind(); int getId(); String toString(); String toShortString(); String toLongString(); } } 复制代码
JoinPoint 接口中常用 api 有:getSignature()、 getArgs()、 getTarget() 、 getThis() 。但是我们平时使用并不直接使用 JoinPoint 的实现类,中间还有一个接口实现,叫做 ProceedingJoinPoint,其定义如下:
public interface ProceedingJoinPoint extends JoinPoint { void set$AroundClosure(AroundClosure var1); //执行目标方法 Object proceed() throws Throwable; //传入的新的参数去执行目标方法 Object proceed(Object[] var1) throws Throwable; } 复制代码
ProceedingJoinPoint 对象是 JoinPoint 的子接口,该对象只用在@Around 的切面方法中。在该接口中,proceed 方法是核心,该方法用于执行拦截器逻辑。关于拦截器这里说一下,以前置通知拦截器为例,在执行目标方法前,该拦截器首先会执行前置通知逻辑,如果拦截器链中还有其他的拦截器,则继续调用下一个拦截器逻辑。直到拦截器链中没有其他的拦截器后,再去调用目标方法。
proceed() 方法的具体实现在 MethodInvocationProceedingJoinPoint 类中,其定义如下:
public Object proceed() throws Throwable { return this.methodInvocation.invocableClone().proceed(); } public Object proceed(Object[] arguments) throws Throwable { Assert.notNull(arguments, "Argument array passed to proceed cannot be null"); if (arguments.length != this.methodInvocation.getArguments().length) { throw new IllegalArgumentException("Expecting " + this.methodInvocation.getArguments().length + " arguments to proceed, but was passed " + arguments.length + " arguments"); } else { this.methodInvocation.setArguments(arguments); return this.methodInvocation.invocableClone(arguments).proceed(); } } 复制代码
查看代码可知,arguments 参数被传入到 ProxyMethodInvocation 对象中,并调用自身的 proceed()方法,接着我们定位到此处进行查看相关代码:
public MethodInvocation invocableClone(Object... arguments) { if (this.userAttributes == null) { this.userAttributes = new HashMap(); } try { ReflectiveMethodInvocation clone = (ReflectiveMethodInvocation)this.clone(); clone.arguments = arguments; return clone; } catch (CloneNotSupportedException var3) { throw new IllegalStateException("Should be able to clone object of type [" + this.getClass() + "]: " + var3); } } public Object proceed() throws Throwable { if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { return this.invokeJoinpoint(); } else { Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher)interceptorOrInterceptionAdvice; Class<?> targetClass = this.targetClass != null ? this.targetClass : this.method.getDeclaringClass(); return dm.methodMatcher.matches(this.method, targetClass, this.arguments) ? dm.interceptor.invoke(this) : this.proceed(); } else { return ((MethodInterceptor)interceptorOrInterceptionAdvice).invoke(this); } } } 复制代码
如上所示,MethodInvocation 的实现类 ReflectiveMethodInvocation 获取到传入的参数之后,执行 proceed 方法,获取到前置通知拦截器逻辑,然后通过反射进行调用。关于 ReflectiveMethodInvocation 类,又继承自 JoinPoint 接口,所以我们看一下这些定义之间的继承关系:
关于连接点的相关知识,我们先了解到这里。有了这些连接点,我们才能进行一些横切操作,但是在操作之前,我们需要定位选择连接点,怎么选择的呢?这就是切点 Pointcut 要做的事情了,继续往下看。
切点 - Pointcut
在上述定义的接口中的所有方法都可以认为是 JoinPoint,但是有时我们并不希望在所有的方法上都添加 Advice(这个后续会讲到),而 Pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述 )来匹配 JoinPoint,给满足规则的 JoinPoint 添加 Advice。
在上一节中我们基于 XML 和注解实现了 AOP 功能,总结发现切点的定义也分为两种。当基于 XML 文件时,可以通过在配置文件中进行定义,具体如下:
<!--Spring基于Xml的切面--> <aop:config> <!--定义切点函数--> <aop:pointcut id="rentPointCut" expression="execution(* com.msdn.bean.Host.rent())"/> <!-- 定义切面 order 定义优先级,值越小优先级越大--> <aop:aspect ref="proxy" order="0"> <!--前置通知--> <aop:before method="seeHouse" pointcut-ref="rentPointCut" /> <!--环绕通知--> <aop:around method="getMoney" pointcut-ref="rentPointCut" /> <!--后置通知--> <aop:after method="fare" pointcut-ref="rentPointCut" /> </aop:aspect> </aop:config> 复制代码
当基于注解进行配置时,定义切点需要两个步骤:
- 定义一个空方法,无需参数,不能有返回值
- 使用 @Pointcut 标注,填入切点表达式
代码定义如下:
//定义一个切入点表达式,用来确定哪些类需要代理 @Pointcut("execution(* com.msdn.bean.Host.*(..))") public void rentPointCut(){ } 复制代码
这里大家也都看到了,关于切点的定义要么是通过来定义,又或者使用@Pointcut 来定义,并没有其他的地方出现过,但是通过之前在 Spring IoC 自定义标签解析一文可以知道,如果声明了注解,那么就一定会在程序中的某个地方注册了对应的解析器。这里就不从头找起了,我们先查看一下 Pointcut 定义:
package org.springframework.aop; public interface Pointcut { Pointcut TRUE = TruePointcut.INSTANCE; /** 返回一个类型过滤器 */ ClassFilter getClassFilter(); /** 返回一个方法匹配器 */ MethodMatcher getMethodMatcher(); } 复制代码
Pointcut 接口中定义了两个接口,分别用于返回类型过滤器和方法匹配器。用于对定义的切点函数进行解析,关于切点函数的讲解,大家可以阅读Spring AOP : AspectJ Pointcut 切点。下面我们再来看一下类型过滤器和方法匹配器接口的定义:
@FunctionalInterface public interface ClassFilter { ClassFilter TRUE = TrueClassFilter.INSTANCE; boolean matches(Class<?> var1); } public interface MethodMatcher { MethodMatcher TRUE = TrueMethodMatcher.INSTANCE; boolean matches(Method var1, Class<?> var2); boolean isRuntime(); boolean matches(Method var1, Class<?> var2, Object... var3); } 复制代码
上面的两个接口均定义了 matches 方法,我们定义的切点函数就是通过 matches 方法进行解析的,然后选择满足规则的连接点。在 Spring 中提供了一个 AspectJ 表达式切点类 AspectJExpressionPointcut,下面我们来看一下这个类的继承关系:
如上所示,这个类最终实现了 Pointcut、ClassFilter 和 MethodMatcher 接口,其中就包括 matches 方法的实现,具体代码如下:
public boolean matches(Class<?> targetClass) { PointcutExpression pointcutExpression = this.obtainPointcutExpression(); try { try { return pointcutExpression.couldMatchJoinPointsInType(targetClass); } catch (ReflectionWorldException var5) { logger.debug("PointcutExpression matching rejected target class - trying fallback expression", var5); PointcutExpression fallbackExpression = this.getFallbackPointcutExpression(targetClass); if (fallbackExpression != null) { return fallbackExpression.couldMatchJoinPointsInType(targetClass); } } } catch (Throwable var6) { logger.debug("PointcutExpression matching rejected target class", var6); } return false; } 复制代码
通过该方法,对切点函数进行解析,该类也就具备了通过 AspectJ 表达式对连接点进行选择的能力。
通过切点选择出连接点之后,就要进行接下来的处理——通知(Advice)。
增强/通知 - Advice
通知 Advice 即我们定义的横切逻辑,比如我们可以定义一个用于监控方法性能的通知,也可以定义一个事务处理的通知等。如果说切点解决了通知在哪里调用的问题,那么现在还需要考虑了一个问题,即通知在何时被调用?是在目标方法执行前被调用,还是在目标方法执行结束后被调用,还在两者兼备呢?Spring 帮我们解答了这个问题,Spring 中定义了以下几种通知类型:
上面是五种通知的介绍,下面我们来看一下通知的源码,如下:
package org.aopalliance.aop; public interface Advice { } 复制代码
如上,通知接口里好像什么都没定义。不过别慌,我们再去到它的子类接口中一探究竟。
//前置通知 public interface BeforeAdvice extends Advice { } //返回通知 public interface AfterReturningAdvice extends AfterAdvice { void afterReturning(@Nullable Object var1, Method var2, Object[] var3, @Nullable Object var4) throws Throwable; } //后置通知 public interface AfterAdvice extends Advice { } //环绕通知 @FunctionalInterface public interface MethodInterceptor extends Interceptor { Object invoke(MethodInvocation var1) throws Throwable; } //异常通知 public interface ThrowsAdvice extends AfterAdvice { } 复制代码
以上通知的定义很简单,我们找一下它们的具体实现类,这里先看一下前置通知的实现类 AspectJMethodBeforeAdvice。
public class AspectJMethodBeforeAdvice extends AbstractAspectJAdvice implements MethodBeforeAdvice, Serializable { public AspectJMethodBeforeAdvice(Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) { super(aspectJBeforeAdviceMethod, pointcut, aif); } public void before(Method method, Object[] args, @Nullable Object target) throws Throwable { this.invokeAdviceMethod(this.getJoinPointMatch(), (Object)null, (Throwable)null); } public boolean isBeforeAdvice() { return true; } public boolean isAfterAdvice() { return false; } } 复制代码
上面的核心代码是 before()方法,用于执行我们定义的前置通知函数。
由于环绕通知比较重要,所以再来看一下它的具体实现类代码。
public class AspectJAroundAdvice extends AbstractAspectJAdvice implements MethodInterceptor, Serializable { public AspectJAroundAdvice(Method aspectJAroundAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) { super(aspectJAroundAdviceMethod, pointcut, aif); } public boolean isBeforeAdvice() { return false; } public boolean isAfterAdvice() { return false; } protected boolean supportsProceedingJoinPoint() { return true; } public Object invoke(MethodInvocation mi) throws Throwable { if (!(mi instanceof ProxyMethodInvocation)) { throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); } else { ProxyMethodInvocation pmi = (ProxyMethodInvocation)mi; ProceedingJoinPoint pjp = this.lazyGetProceedingJoinPoint(pmi); JoinPointMatch jpm = this.getJoinPointMatch(pmi); return this.invokeAdviceMethod(pjp, jpm, (Object)null, (Throwable)null); } } protected ProceedingJoinPoint lazyGetProceedingJoinPoint(ProxyMethodInvocation rmi) { return new MethodInvocationProceedingJoinPoint(rmi); } } 复制代码
核心代码为 invoke 方法,当执行目标方法时,会首先来到这里,然后再进入到自定义的环绕方法中。
现在我们有了切点 Pointcut 和通知 Advice,由于这两个模块目前还是分离的,我们需要把它们整合在一起。这样切点就可以为通知进行导航,然后由通知逻辑实施精确打击。那怎么整合两个模块呢?答案是,切面
。好的,是时候来介绍切面 Aspect 这个概念了。
切面 - Aspect
切面 Aspect 整合了切点和通知两个模块,切点解决了 where 问题,通知解决了 when 和 how 问题。切面把两者整合起来,就可以解决 对什么方法(where)在何时(when - 前置还是后置,或者环绕)执行什么样的横切逻辑(how)的三连发问题。在 AOP 中,切面只是一个概念,并没有一个具体的接口或类与此对应。
切面类型主要分成了三种:
- 一般切面
- 切点切面
- 引介/引入切面
一般切面,切点切面,引介/引入切面介绍:
public interface Advisor { Advice EMPTY_ADVICE = new Advice() { }; Advice getAdvice(); boolean isPerInstance(); } 复制代码
我们重点看一下 PointcutAdvisor ,关于该接口的定义如下:
public interface PointcutAdvisor extends Advisor { Pointcut getPointcut(); } 复制代码
Advisor 中有一个 getAdvice 方法,用于返回通知。PointcutAdvisor 在 Advisor 基础上,新增了 getPointcut 方法,用于返回切点对象。因此 PointcutAdvisor 的实现类即可以返回切点,也可以返回通知,所以说 PointcutAdvisor 和切面的功能相似。所以说 PointcutAdvisor 和切面的功能相似。不过他们之间还是有一些差异的,比如看下面的配置:
<!--Spring基于Xml的切面--> <aop:config> <!--定义切点函数--> <aop:pointcut id="rentPointCut" expression="execution(* com.msdn.bean.Host.rent())"/> <!-- 定义切面 order 定义优先级,值越小优先级越大--> <aop:aspect ref="proxy" order="0"> <!--前置通知--> <aop:before method="seeHouse" pointcut-ref="rentPointCut" /> <!--环绕通知--> <aop:around method="aroundMethod" pointcut-ref="rentPointCut" /> <!--后置通知--> <aop:after method="fare" pointcut-ref="rentPointCut" /> </aop:aspect> </aop:config> 复制代码
如上,一个切面中配置了一个切点和三个通知,三个通知均引用了同一个切点,即 pointcut-ref="helloPointcut"
。这里在一个切面中,一个切点对应多个通知,是一对多的关系(可以配置多个 pointcut,形成多对多的关系)。而在 PointcutAdvisor 的实现类 AspectJPointcutAdvisor 中,切点和通知是一一对应的关系。
public AspectJPointcutAdvisor(AbstractAspectJAdvice advice) { Assert.notNull(advice, "Advice must not be null"); this.advice = advice; this.pointcut = advice.buildSafePointcut(); } 复制代码
上面的通知最终会被转换成三个 PointcutAdvisor,这里我把在 AbstractAdvisorAutoProxyCreator 源码调试的结果贴在下面:
织入 - Weaving
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有很多个点可以织入:
- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的 。
- 类加载期:切面在目标类加载到 JVM 时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前 增强该目标类的字节码。
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。Spring AOP 就是以这种方式织入切面的。
Spring AOP 既然是在目标对象运行期织入切面的,那它是通过什么方式织入的呢?先来说说以何种方式进行织入,首先还是从 LoadTimeWeaverAwareProcessor 开始,该类是后置处理器 BeanPostProcessor 的一个实现类,我们都知道 BeanPostProcessor 有两个核心方法,用于在 bean 初始化之前和之后被调用。具体是在 bean 对象初始化完成后,Spring通过切点对 bean 类中的方法进行匹配。若匹配成功,则会为该 bean 生成代理对象,并将代理对象返回给容器。容器向后置处理器输入 bean 对象,得到 bean 对象的代理,这样就完成了织入过程。 关于后置处理器的细节,这里就不多说了,大家若有兴趣,可以参考之前写的Spring之BeanFactoryPostProcessor和BeanPostProcessor
实例
结合上述分析的内容,我们对上一章节中的案例进行扩展,修改切面定义。
@Order(0) @Aspect @Component public class JoinPointDemo { //定义一个切入点表达式,用来确定哪些类需要代理 @Pointcut("execution(* com.msdn.bean.Host.*(..))") public void rentPointCut(){ } /** * 前置方法,在目标方法执行前执行 * @param joinPoint 封装了代理方法信息的对象,若用不到则可以忽略不写 */ @Before("rentPointCut()") public void seeHouse(JoinPoint joinPoint){ Signature oo = joinPoint.getSignature(); System.out.println("前置方法准备执行......"); System.out.println("目标方法名为:" + joinPoint.getSignature().getName()); System.out.println("目标方法所属类的简单类名:" + joinPoint.getSignature().getDeclaringType().getSimpleName()); System.out.println("目标方法所属类的类名:" + joinPoint.getSignature().getDeclaringTypeName()); System.out.println("目标方法声明类型:" + Modifier.toString(joinPoint.getSignature().getModifiers())); //获取传入目标方法的参数 Object[] args = joinPoint.getArgs(); for (int i = 0; i < args.length; i++) { System.out.println("第" + (i+1) + "个参数为:" + args[i]); } System.out.println("被代理的对象:" + joinPoint.getTarget()); System.out.println("代理对象自己:" + joinPoint.getThis()); System.out.println("前置方法执行结束......"); } /** * 环绕方法,可自定义目标方法执行的时机 * @param point * @return 该方法需要返回值,返回值视为目标方法的返回值 */ @Around("rentPointCut()") public Object aroundMethod(ProceedingJoinPoint point){ Object result = null; try { System.out.println("目标方法执行前..."); //获取目标方法的参数,判断执行哪个proceed方法 Object[] args = point.getArgs(); if (args.length > 0){ //用新的参数值执行目标方法 result = point.proceed(new Object[]{"上海市黄浦区中华路某某公寓19楼2号"}); }else{ //执行目标方法 result = point.proceed(); } } catch (Throwable e) { //异常通知 System.out.println("执行目标方法异常后..."); throw new RuntimeException(e); } System.out.println("目标方法执行后..."); return result; } @After("rentPointCut()") public void fare(JoinPoint joinPoint){ System.out.println("执行后置方法"); } } 复制代码
配置文件如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <context:component-scan base-package="com.msdn.bean,com.msdn.aop" /> <aop:aspectj-autoproxy /> </beans> 复制代码
测试代码为:
@Test public void aopTest(){ ApplicationContext context = new ClassPathXmlApplicationContext("spring-aop.xml"); Object host = context.getBean("host"); Rent o = (Rent) host; o.rent("新东方"); o.buyFurniture(); } 复制代码
执行结果为:
目标方法执行前... 前置方法准备执行...... 目标方法名为:rent 目标方法所属类的简单类名:Rent 目标方法所属类的类名:com.msdn.bean.Rent 目标方法声明类型:public abstract 第1个参数为:上海市黄浦区中华路某某公寓19楼2号 被代理的对象:com.msdn.bean.Host@524d6d96 代理对象自己:com.msdn.bean.Host@524d6d96 前置方法执行结束...... 出租位于上海市黄浦区中华路某某公寓19楼2号处的房屋 目标方法执行后... 执行后置方法 ******************** 目标方法执行前... 前置方法准备执行...... 目标方法名为:buyFurniture 目标方法所属类的简单类名:Rent 目标方法所属类的类名:com.msdn.bean.Rent 目标方法声明类型:public abstract 被代理的对象:com.msdn.bean.Host@524d6d96 代理对象自己:com.msdn.bean.Host@524d6d96 前置方法执行结束...... 添置家具 目标方法执行后... 执行后置方法 复制代码
总结
前面三篇文章只是在介绍 Spring AOP 的原理和基本使用,从本文开始准备深入学习 AOP,其中就先了解 AOP 的核心概念,对于后续源码的学习非常有必要。如果文中有什么不对的地方,欢迎指正。