对 Spring AOP 机制及其配置方式的的总结

1、基本概念梳理

1.1 基本概念

  1. 连接点 (Jointpoint):表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等;
  2. 切入点 (Pointcut):选择一组相关连接点的模式,即可以认为连接点的集合;
  3. 通知 (Advice):在连接点上执行的行为,包括前置通知、后置通知、环绕通知,在 Spring 中通过代理模式实现 AOP;
  4. 方面/切面 (Aspect):可以认为是通知、引入和切入点的组合;
  5. 引入 (inter-type declaration):也称为内部类型声明,为已有的类添加额外新的字段或方法;
  6. 目标对象 (Target Object):需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象;
  7. AOP代理 (AOP Proxy):AOP 框架使用代理模式创建的对象,从而实现在连接点处插入通知(即应用切面),就是通过代理来对目标对象应用切面。
    在 Spring 中,AOP 代理可以用 JDK 动态代理或 CGLIB 代理实现,而通过拦截器模型应用切面。
  8. 织入 (Weaving):织入是一个过程,是将切面应用到目标对象从而创建出 AOP 代理对象的过程,织入可以在编译期、类装载期、运行期进行。

对 Spring AOP 机制及其配置方式的的总结

1.2 通知类型

  1. 前置通知 (Before Advice):在切入点选择的连接点处的方法之前执行的通知。
  2. 后置通知 (After Advice):在切入点选择的连接点处的方法之后执行的通知,包括如下类型的后置通知:
    1. 后置返回通知 (After returning Advice):在切入点选择的连接点处的方法正常执行完毕时执行的通知,必须是连接点处的方法没抛出任何异常正常返回时才调用后置通知。
    2. 后置异常通知 (After throwing Advice):在切入点选择的连接点处的方法抛出异常返回时执行的通知,必须是连接点处的方法抛出任何异常返回时才调用异常通知。
    3. 后置最终通知 (After finally Advice):在切入点选择的连接点处的方法返回时执行的通知,不管抛没抛出异常都执行,类似于 Java 中的 finally 块。
  3. 环绕通知 (Around Advices):环绕通知可以在方法调用之前和之后自定义任何行为,并且可以决定是否执行连接点处的方法、替换返回值、抛出异常等等。

考虑一下一个方法从开始到执行结束,其实上面的几个通知类型也就是覆盖了所有可能出现的情况。

1.3 织入

在目标对象的生命周期里可以有多个点进行织入:

  1. 编译器:切面在目标类编译时织入,这种方式需要特殊的编译器,AspectJ 就是使用这种方式织入的;
  2. 类加载期:需要特殊的类加载器,在目标类被引入到应用之前增强该目标类的字节码;
  3. 运行期:织入切面时,AOP 容器会为目标对象动态创建一个代理对象,Spring AOP 就是使用这种方式进行织入的。

Spring 提供了 4 种类型的 AOP 切面:

  1. 基于代理的经典 Spring AOP;
  2. 纯 POJO 面;
  3. @AspectJ 注解驱动的切面;
  4. 注入式 AspectJ 切面

2、基于 XML 配置 AOP

2.1 在 Spring 中使用 AOP

我们用一个简单的例子来测试一下 Spring 的 AOP 功能,这里我们定义了两个类,一个是 Worker,一个是 LogLog。LogLog 有一个名为 say() 的方法,该方法中输出一行文字到控制台。我们希望 Worker 的每个方法在执行的时候都能够调用 LogLog 的 say() 方法。我们可以做如下的配置:

首先,我们要在基本的命名空间基础上增加一行代码来启用 aop 命名空间。此外,我们还要增加schemaLocation 中的配置:

<beans ...
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
    <!-- .... -->
</beans>

然后,我们就可以使用 AOP 来完成我们想要的功能了:

    <!-- 不论是切入点 -->
    <bean id="logLog" class="me.shouheng.spring.aop.LogLog"/>
    <bean id="worker" class="me.shouheng.spring.aop.Worker"/>
    <!-- 配置 AOP -->
    <aop:config>
        <!-- 定义切入点:id 和规则表达式 -->
        <aop:pointcut id="pointcut" expression="execution(* me.shouheng.spring.aop.Worker.*(..)))"/>
        <!-- 定义切面:指定切面引用的 Bean 及其方法,切入点 -->
        <aop:aspect ref="logLog">
            <aop:before method="say" pointcut-ref="pointcut"/>
            <aop:after method="say" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>

以上的配置没有问题,我们可以实现期望的功能。

这里,我们可以看出,即使是 AOP 也是需要目标对象和切点定义成 Bean。pointcut 是所有方法的一个合集,我们使用 expression 来指定这些方法的规则。aspect 整合了切入点和通知,它需要引用 Bean,并且在子标签中将通知类型、要执行的方法和切入点结合起来。

另外,还需要注意的是,切入点和切面都可以被定义多个,但是是有顺序要求的。除了使用 pointcut-ref 引用某个切入点,还可以使用匿名的切入点:

    <aop:after method="say" pointcut="execution(* me.shouheng.spring.aop.Worker.*(..)))"/>

总结:切面需要引用 Bean,然后指定切入点上执行的该 Bean 的方法。切入点不是 Bean 类型,它是一种表达式规则,用来指定被切入的方法的规则。

OK,实际上,如果你是第一次接触 AOP 的话,看 AOP 的配置的时候还是比较费解的。那么,我们抛开相关文档,自己去想一下实际开发过程中可能会需要哪些功能,并看它们如何配置和实现好了。

我们从几个通知作为思考的起点:

  1. 前置通知:我想要知道方法的所有入参,我可以用它来记录一些日志
  2. 后置返回通知:我想要知道返回的结果是什么,我可以把不机密但重要的信息放在日志里面
  3. 后置异常通知:我想要知道具体出现的异常的类型和异常的具体信息,以把它们记录到日志中,或者根据具体的错误原因做一些其他的处理,比如翻译之后返回给客户端
  4. 后置最终通知:同上
  5. 环绕通知:同上

所以,总结一下我们想要获取的无非下面三个信息:

  1. 方法的参数
  2. 方法的返回结果
  3. 异常的详情

那么,我们接下来就看使用 AOP 如何获取这三个信息。

2.2 获取方法的入参

如下所示,我们这里企图拦截 Worker 类中的所有的包含一个名为 words 参数的方法,然后我们使用 logLogsayWords() 方法进行拦截,并在方法执行之前输出该参数:

    <!-- 定义切面:切面执行的 Bean 是 logLog -->
    <aop:aspect ref="logLog">
        <!-- 定义切入点规则 -->
        <aop:pointcut id="pointcut" expression="execution(* me.shouheng.spring.aop.Worker.working(..)) and args(words)"/>
        <!-- 定义什么时候切入以及在切入点上执行的方法 -->
        <aop:before pointcut-ref="pointcut" method="sayWords" arg-names="words"/>
    </aop:aspect>

注意,这里我们在切点的定义中使用 and 表示要符合两种情况,我们也可以用两个 &&(在 XML 中是 &amp;&amp;)来表示,但是显然前者更加简洁。当我们要拦截的方法中包含多个参数的时候,也可以同时在 args 中指定,只要注意用 , 分隔开即可。

2.3 获取方法的返回结果

为了测试获取方法返回结果,我们在 Worker 中增加了一个新的方法 makeA(),它返回一个字符串类型的数据。我们按照如下所示的方式来获取并使用方法的返回结果:

    <aop:aspect ref="logLog">
        <!-- 定义切入点 -->
        <aop:pointcut id="p2" expression="execution(* me.shouheng.spring.aop.Worker.makeA())"/>
        <!-- 定义要拦截的事件以及相关的参数等 -->
        <aop:after-returning method="sayWords" pointcut-ref="p2" returning="words"/>
    </aop:aspect>

在定义了切点之后,我们使用 aop:after-returning 标签来获取方法的返回值并对其进行处理。这里我们用了 logLogsayWords() 方法,使用 returning 属性指定返回值应用到 sayWords() 方法的参数的名称。

以上程序执行的最终效果就是,在执行了 makeA() 方法之后,将返回的结果作为 words 参数传入到 sayWords() 方法中。

需要注意的地方是,需要使用 aop:after-returning 标签才能获取方法的返回结果并进行处理。

2.4 获取方法的执行的异常

为了测试拦截异常的逻辑,我们需要在 LogLog 类中增加一个方法 handleError(Throwable throwable)。我们还要在 Worker 中定义一个 throwMethod() 方法,它内部抛出一个异常。然后我们进行如下的配置:

    <aop:aspect ref="logLog">
        <!-- 定义切入点规则 -->
        <aop:pointcut id="p3" expression="execution(* me.shouheng.spring.aop.Worker.throwMethod())"/>
        <!-- 拦截异常事件并执行拦截的参数信息等 -->
        <aop:after-throwing method="handleError" pointcut-ref="p3" throwing="throwable"/>
    </aop:aspect>

拦截异常和拦截方法执行结果的逻辑类似,我们需要指定一个方法,然后在 throwing 属性中指定该方法中的参数的名称。当抛出异常的时候,就会把异常作为该参数传入到方法中,我们可以在方法中对异常进行处理。

2.5 环绕通知

环绕通知和上面的几种通知略有不同,你可以在通知方法种指定一个 ProceedingJoinPoint 类型的参数,并按照下面的方式配置:

    public void say2(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println("===========before");
            joinPoint.proceed();
            System.out.println("===========after");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("==========exception");
        } finally {
            System.out.println("===========finally");
        }
    }

当我们调用 joinPoint.proceed() 的时候,实际上是在执行被代理的方法,我们可以像上面这样在各个它调用前、后和出现异常的时候进行处理。而在 XML 中配置的时候,配置的方式和普通的切面定义完全一样:指定切面和方法名即可。

2.6 advisor

在 AOP 的 XML 配置方式中,<aop:aspect> 标签中可用的标签共 3 个:<aop:pointcut><aop:aspect><aop:advisor>.

advisor 是切面和切入点的结合,<aop:advisor> 标签的使用类似于环绕方法。在使用的时候,我们先要定义一个 Bean 用来定义拦截时的处理逻辑:

public class MethodHandler implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("++++++before advice");
        try {
            methodInvocation.proceed();
        } catch (Exception ex) {
            System.out.println("++++++catch advice");
        }
        System.out.println("++++++after advice");
        return null;
    }
}

然后,在 XML 中通过 <aop:advisor> 标签来进行配置:

    <!-- 定义 Bean -->
    <bean id="handler" class="me.shouheng.spring.aop.MethodHandler"/>
    <!-- 定义 Advisor -->
    <aop:config>
        <aop:advisor advice-ref="handler" id="advisor" pointcut-ref="pointcut"/>
    </aop:config>

2.7 总结

切面并没有什么特别复杂的东西,无非是对一些规则是否熟练的问题。AOP 本身是一种工具或者开发思想,主要用来对方法进行统一的处理。我们可以获取要执行方法的参数信息、返回结果和抛出的异常等等,并根据它们来做不同的动作。

3、使用 @Aspect 配置 AOP

除了使用基于 XML 配置 AOP,还可以使用注解来进行配置。Spring 默认不支持 @AspectJ 风格的切面声明,为了支持需要使用如下配置:

    <aop:aspectj-autoproxy/>  

然后,我们就可以进行配置和使用了。如果你用的式基于 Java 的配置方式,你还需要在使用 @Configuration 注解的配置类上面使用 @EnableAspectJAutoProxt 来启用自动代理。该注解的作用和当前标签的作用相同,只是对应于不同的配置方式。

使用注解的方式和使用 XML 配置的方式基本类似,我们看下下面的代码。

首先是切面的定义:

    @Aspect
    public class AspectObj {

        // 定义切点
        @Pointcut(value = "execution(* me.shouheng.spring.aspect.*.*(..)) && args(params)", argNames = "params")
        public void pointcut(String params) { 
        }

        // 定义切点方法执行之前执行的逻辑
        @Before(value = "pointcut(params)", argNames = "params")
        public void before(String params) {
            System.out.println("================= before =================");
        }
    }

这里我们使用 @Aspect 注解,它表明该类是一个切面。然后,我们定义一个方法体为空的方法,并使用 @Pointcut 注解,表明它是一个切点。可以看出它的配置方式和使用 XML 基本一样。

然后,我们需要定义一个通知,这里我们用的是前置通知,我们用 @Before 注解声明方法为前置通知,并指定切点和方法的参数名。

配置完这些之后,我们定义一个测试类,来看一下我们的是否能够在方法执行之前拦截到方法,并获取到方法的参数。我们定义一个名为 HelloAspect 的类,并在其中定义一个方法:

    public void sayHello(String params) {
        System.out.println("Hello, " + params);
    }

这样我们需要在 XML 中将上述定义的两个类作为 Bean 声明在 XML 中,这样它们就可以被 IoC 管理了。OK,我们获取上下文之后调用 Bean 的方法确实可以达到我们预期的效果。

以上是基于注解的 AOP 的基本配置方式,其他类型的通知的配置方式与其基本相似,我们只要知道其中的逻辑就可以了,没必要面面俱到。

附录

1、切面表达式

在这里总结下 Spring 中常用到的通配符、操作符和指示器。它们一起用来指定切入点的方法的规则。

1.1 通配符

符号 含义
.. 任意参数列表,任何数量的包
+ 给定类的任何子类
* 任何数量的字符

1.2 操作符

符号 含义
&& 与操作,两个条件都成立才成立
|| 或操作,两个条件一个成立即成立
! 非操作,不满足条件的时候成立

1.3 指示器

Spring 中常用到的指示器包括下面几种类型:

对 Spring AOP 机制及其配置方式的的总结

类签名表达式 whithin(<type name>)

表达式 含义
within(my.palm..*) 匹配 my.palm 及其子包中所有类的所有方法
within(my.palm.ClassName) 匹配 my.palm.ClassName 类中所有方法
within(MyInterface+) 匹配 MyInterface 接口所有实现类的所有方法
within(my.palm.BaseClass) 匹配 my.palm.BaseClass 及其子类的所有方法

方法签名 execution(<scope> <return type> <full-qulified-class-name>.<method>(params))

表达式 含义
execution(* my.shouhengn.Palm.*(..)) my.shouhengn.Palm 中所有方法
execution(public * my.shouhengn.Palm.*(..)) my.shouhengn.Palm 中所有公共方法
execution(public * my.shouhengn.Palm.*(long,..)) my.shouhengn.Palm 中所有第一个参数为 long 型的公共方法
execution(public String my.shouhengn.Palm.*(..)) my.shouhengn.Palm 中所有返回类型为 String 的公共方法

其他

表达式 含义
bean(*Service) 所有后缀名为 Service 的 Bean
@anotation(my.shouheng.Annotation) 所有使用 Annotaion 注解的方法会被调用
@within(my.shouheng.Annotation) 所有使用 Annotaion 注解的类的方法会被调用
this(my.shouheng.MyInterface) 所有实现了 my.shouheng.MyInterface 接口的代理对象的方法会被调用

这部分内容的重点是 execution() 类型的指示器,其他的可以作为了解内容,需要使用到的时候知道如何使用即可。