Spring 入门(二):AOP

AOP 介绍

什么是 AOP

AOP 为Aspect Oriented Programming的缩写,意为面向切面编程,是一种编程思想,通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能,是面向对象编程(OOP)的一种补充。

传统业务代码中,通常都会进行权限认证、日志记录和事务处理等操作。虽然 OOP 可以通过组合或者继承的方式来达到代码的重用,但如果要实现某个功能(如日志记录),同样的代码仍然会分散到各个方法中。这样,如果想要关闭某个功能,或者对其进行修改,就必须要修改所有的相关方法。这不但增加了开发人员的工作量,而且提高了代码的出错率,为了解决这一问题,AOP 思想随之产生。

AOP 采取横向抽取的方式,将那些与业务无关,但却对多个类产生影响的公共行为和逻辑(如日志记录),抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect)。然后在程序编译或运行时,通过代理,在不修改源码的基础上,将“切面”模块的代码织入到现有方法的前后,以增强现有方法。

AOP 使用代理技术,在不修改源码的情况下对已有方法进行增强,拥有以下优势:

  • 减少系统的重复代码
  • 降低模块间的耦合度
  • 有利于未来的可操作性和可维护性

Spring AOP 和 AspectJ

☕️ AOP 的实现可分为两种:

  • 静态织入(Aspect):在代码的编译阶段植入 Pointcut 的内容 。优点是性能好,但是需要特定的编译器。

  • 动态代理(Spring AOP):在代码执行阶段,在内存中截获对象,动态的插入 Pointcut 的内容。优点是不需要额外的编译,无需特定的编译器,但是性能比静态织入要低。

Spring 2.0以后引入了对 AspectJ 语法的支持,但是它本质上还是使用动态代理。原因在于,Spring 并没有真正引入 AspectJ 框架,只是借鉴了 AspectJ 的语法风格,本质上还是 Spring 的原生实现。因此,Spring 中的 AOP 在运行时仍旧是纯的Spirng AOP,而不依赖 AspectJ 的编译器或者织入器。

☕️ Spring AOP的动态代理主要有两种方式:

  • 基于接口的动态代理:JDK 官方的 Proxy 类,要求被代理类最少实现一个接口。
  • 基于子类的动态代理:第三方的 CGLib 库,要求被代理类不能为用 final 修饰的类(最终类)。

在 Spring 中,框架会根据被代理类是否实现了接口来决定采用哪种动态代理的方式。

☕️ 前面一直提 AOP 的实现技术是代理,那什么是代理

代理(Proxy)是一种设计模式,提供了间接对目标对象(被代理对象)进行访问的方式,即通过代理对象访问目标对象。这样做的好处是可以在实现目标对象功能的基础上,增加额外的功能,即扩展目标对象的功能。例如:明星与经纪人之间就是代理和被代理的关系,明星出演活动的时候,明星就是目标对象,他只要负责活动中的节目,而其它琐碎的事情(额外的功能)就交给他的经纪人(代理对象)处理。

下面介绍 JDK 动态代理和 CGLib 动态代理的基本使用。

JDK 动态代理

JDK 动态代理是通过java.lang.reflect.Proxy类来实现的,调用 Proxy 类的newProxylnstance()方法来创建代理代理对象,要求被代理类(目标类)最少实现一个接口。具体使用如下:

⭐️ 创建目标类的接口和实现类

package com.example.jdk;

// 要求被代理类(目标类)最少实现一个接口
public interface TargetInterface {
    void method();
}
package com.example.jdk;

// 被代理类(目标类)
public class Target implements TargetInterface{
    @Override
    public void method() {
        System.out.println("running method...");
    }
}

⭐️ 使用 JDK 动态代理

package com.example.jdk;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class TargetProxy {

    public static void main(String[] args) {
        // 创建被代理对象(目标对象)
        Target target = new Target();

        // 创建代理对象
        /**
         * 创建方式:
         *   Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
         * 参数含义:
         *   ClassLoader:和被代理对象(目标对象)使用相同的类加载器
         *   Interfaces:和被代理对象(目标对象)具有相同的行为,实现相同的接口
         *   InvocationHandler:如何代理
         */
        TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance(
                target.getClass().getClassLoader(), target.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 执行被代理对象的任何方法,都会经过该方法
                     * @param proxy 代理对象的引用。不一定每次都用得到
                     * @param method 当前执行的方法对象(目标对象中的执行方法)
                     * @param args 执行方法所需的参数
                     * @return 当前执行方法的返回值
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, 
                                         Object[] args) throws Throwable {
                        System.out.println("前置增强代码...");
                        Object invoke = method.invoke(target, args);
                        System.out.println("后置增强代码...");
                        return invoke;
                    }
                });

        // 执行方法
        proxy.method();
    }
}
前置增强代码...
running method...
后置增强代码...

CGLib 动态代理

JDK 动态代理的使用具有一定的局限性,使用动态代理的对象必须实现一个或多个接口 。如果要对没有实现接口的类进行代理,可以使用 CGLib 代理,该代理要求被代理类(目标类)不能用 final 修饰的类(最终类)。 具体使用如下:

✏️ 创建目标类

package com.example.cglib;

// 被代理类(目标类)不能为 final 修饰的类(最终类)
public class Target {

    public void method() {
        System.out.println("running method...");
    }
}

✏️ 使用 CGLib 动态代理

package com.example.cglib;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class TargetProxy {

    public static void main(String[] args) {
        // 创建被代理对象(目标对象)
        Target target = new Target();
        // 创建代理对象
        /**
         * 创建方式:
         *   Object create(Class type, Callback callback)
         * 参数含义:
         *   type:被代理对象(目标对象)的 class 对象
         *   callback:如何处理
         */
        Target cglibTarget = (Target)  Enhancer.create(target.getClass(),
                new MethodInterceptor() {
                    /**
                     * 执行被代理对象的任何方法,都会经过该方法
                     * @param proxy 根据指定父类生成的代理对象
                     * @param method 当前执行的方法对象(目标对象中的执行方法)
                     * @param args 执行方法所需的参数
                     * @param methodProxy 当前执行方法的代理对象,用于执行父类的方法
                     * @return
                     */
                    @Override
                    public Object intercept(Object proxy, Method method, Object[] args,
                                            MethodProxy methodProxy) throws Throwable {
                        System.out.println("前置增强代码...");
                        // 以下两种方式都行
                        Object invoke = method.invoke(target, args);
                        // Object invoke = methodProxy.invokeSuper(proxy, args);
                        System.out.println("后置增强代码...");
                        return invoke;
                    }
                });
        // 执行方法
        cglibTarget.method();
    }
}
前置增强代码...
running method...
后置增强代码...

AOP 术语

1)切面(Aspect)

切面由切点和通知组成。在实际应用中,切面通常是指封装的用于横向插入系统功能(如事务、曰志等)的类。该类要被 Spring 容器识别为切面,需要在配置文件中通过<bean>元素指定。

2)连接点(JoinPoint)

连接点是程序执行的某个特定位置:如类开始初始化前、类初始化后、类某个方法调用前、调用后、方法抛出异常后。一个类或一段程序代码拥有一些具有边界性质的特定点,这些点中的特定点就称为“连接点”。

Spring 仅支持方法的连接点,即仅能在方法调用前、方法调用后、方法抛出异常时以及方法调用前后这些程序执行点织入通知。

连接点由两个信息确定:第一是用方法表示的程序执行点;第二是用相对位置表示的方位。如在Test.foo()方法执行前的连接点,执行点位Test.foo(),方位为该方法执行前的位置。Spring 使用切点对执行点进行定位,而方位在通知中定义。

3)切点(Pointcut)

每个程序类都拥有多个连接点,如一个拥有两个方法的类,这两个方法都是连接点,即连接点是程序类中客观存在的事物。AOP 通过切点定位某些特定的连接点,连接点相当于数据库中的记录,而切点相当于查询条件。切点和连接点不是一对一的关系,一个切点可以匹配多个连接点。

确切地说,切点应该是执行点,而不是连接点。因为在 Spring 中,切点只定位到某个方法上,而连接点是方法执行前、后等包括方位信息的具体程序执行点,所以如果希望定位到具体的连接点,还需要提供具体的方位。

4)通知(Advice)

通知是织入目标类连接点上的一段代码。在 Spring 中,通知除用于描述一段程序代码外,还拥有另外一个和连接点相关的信息,这便是执行点的方位。结合执行点的方位信息和切点信息,就可以找到特定的连接。

在 Spring 中,通过 Advice 定义横切逻辑。按照通知在方法上的织入位置,可以分为前置通知(Before)、后置通知(AfterReturning)、环绕通知(Around)、异常抛出通知(AfterThrowing)和最终通知(After)五种。

5)Target Object(目标对象)

是指所有被通知的对象,也称为被增强对象。如果 AOP 框架采用的是动态的 AOP 实现,那么该对象就是一个被代理对象。

6)Proxy(代理)

将通知应用到目标对象之后,被动态创建的对象。

7)Weaving(织入)

将切面代码插入到目标对象上,从而生成代理对象的过程。


AspectJ 开发

Spring 2.0以后,Spring AOP引入了对 AspectJ 语法的支持,并允许直接使用 AspectJ 语法进行编程,新版本的 Spring 框架,也建议使用 AspectJ 语法来开发 AOP。Spring AOP的底层实现原理是 JDK 动态代理和 CGLib 动态代理,Spring 框架会根据被代理类是否实现了接口来决定使用哪种动态代理。

通知是切面的具象化体现,此处介绍下Spring AOP的五类通知:

  • 前置通知(Before):在目标方法执行之前执行的通知。
  • 后置通知(AfterReturning):在目标方法正常执行之后执行的通知。在后置通知方法中,可以配置形参接收目标方法的返回值。需要注意,后置通知和异常通知只会执行一个。
  • 异常通知(AfterThrowing):在目标方法抛出异常之后执行的通知。在异常通知方法中,可以配置形参接收目标方法抛出的异常。需要注意,后置通知和异常通知只会执行一个。
  • 环绕通知(Around):在目标方法执行之前和之后执行的通知。在环绕通知方法中,必须显式调用目标方法,并且必须有返回值(该值为目标方法的执行结果),要不然有返回值的目标方法的调用者只能得到 null。
  • 最终通知(After):目标方法执行之后执行的通知。和后置通知不同的是,后置通知是在目标方法正常执行后执行的通知,如果目标方法抛出异常,则后置通知不执行;而最终通知是无论目标方法是否抛出异常,都会执行。另外,后置通知方法可以设置形参接收目标方法的返回值,而最终通知不能。

XML 方式

📚 在pom.xml中导入相关依赖

<dependencies>
    <!-- spring-context,该 jar 包会将 aop、beans、core、expression 一并下下来 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>

    <!-- Aspectj 语法的解析 -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.6</version>
    </dependency>

    <!-- junit 测试 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
    </plugins>
</build>

📚 创建目标类

package com.example.entity;

public class Target {

    // 正常返回的方法
    public int method1(int val) {
        System.out.println("method1 方法运行中...\n");
        return val;
    }

    // 抛出异常的方法
    public int method2() {
        System.out.println("method2 方法运行中...\n");
        return 1 / 0;
    }

    @Override
    public String toString() {
        return "目标对象的 toString() 方法被调用";
    }
}

📚 创建切面类

package com.example.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

public class MyAspect {
    /**
     * 前置通知方法(在目标方法执行之前执行的通知)
     * @param joinPoint 该参数是可选项,如果存在必须为方法的第一个参数;
     *                  代表当前的连接点,通过该对象可以获取目标对象和目标方法相关的信息
     */
    public void myBefore(JoinPoint joinPoint) {
        System.out.println("前置通知,前置代码增强...");
        // getTarget() 返回的是目标对象,所以此处调用目标对象的 toString() 方法
        System.out.println(joinPoint.getTarget());
        System.out.println("目标对象的全限定类名:" + joinPoint.getSignature().getDeclaringTypeName());
        System.out.println("目标方法名:" + joinPoint.getSignature().getName() + "\n");
    }

    /**
     * 后置通知方法(在目标方法正常执行之后执行的通知)
     * @param joinPoint 该参数是可选项,如果存在必须为方法的第一个参数;
     *                  代表当前的连接点,通过该对象可以获取目标对象和目标方法相关的信息
     * @param returnVal 该参数是可选项,参数名必须和 XML 配置统一,表示目标方法的返回值
     */
    public void myAfterReturning(JoinPoint joinPoint, Object returnVal) {
        System.out.println("后置通知,后置代码增强...");
        System.out.println("目标方法返回值为:" + returnVal + "\n");
    }
    
    /**
     * 异常方法(在目标方法抛出异常之后执行的通知)
     * @param joinPoint 该参数是可选项,如果存在必须为方法的第一个参数;
     *                  代表当前的连接点,通过该对象可以获取目标对象和目标方法相关的信息
     * @param e 该参数是可选性,表示目标方法抛出的异常
     */
    public void myAfterThrowing(JoinPoint joinPoint, Throwable e) {
        System.out.println("异常通知," + e.getMessage() + "\n");
    }

    /**
     * 环绕通知(在目标方法执行之前和之后执行的通知)
     * @param proceedingJoinPoint 该参数必须有,JoinPoint 的子接口;
     *                            该接口的 proceed() 方法会执行目标方法
     * @return 返回值必须有,返回目标方法的执行结果
     */
    public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知,环绕前置代码增强...\n");
        // 执行目标方法
        Object obj = proceedingJoinPoint.proceed();
        System.out.println("环绕通知,环绕后置代码增强...\n");
        // 必须存在返回值,要不然有返回值的目标方法调用者只能接收 null
        return obj;
    }

    /**
     * 最终通知(目标方法执行之后执行的通知,无论目标方法是否正常执行)
     * @param joinPoint 该参数是可选项,如果存在必须为方法的第一个参数;
     *                  代表当前的连接点,通过该对象可以获取目标对象和目标方法相关的信息
     */
    public void myAfter(JoinPoint joinPoint) {
        System.out.println("最终通知,最终代码增强...\n");
    }
}

📚 在 applicationContext.xml 文件中配置相关的 bean 和通知:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
        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
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 配置被代理类(目标类)的 bean -->
    <bean id="target" class="com.example.entity.Target"/>
    <!-- 配置切面类的 bean -->
    <bean id="myAspect" class="com.example.aspect.MyAspect"/>

    <!-- aop:config:在该标签里配置切面 -->
    <aop:config>
        <!-- aop:aspect:配置切面,在该标签里配置通知
             属性:id:切面的唯一的标识
                  ref:引用配置好的切面类的 bean 的 id
        -->
        <aop:aspect id="aspect1" ref="myAspect">
            <!-- aop:pointcut:用于配置切点表达式,目标方法是根据切点表达式来匹配的
                 属性:id:切点表达式的唯一标识
                      expression:切点表达式
            -->
            <aop:pointcut id="p1" expression="execution(* com.example..*.*(..))"/>

            <!-- aop:before:配置前置通知
                 属性:method:指定切面类中的方法名(前置通知方法)
                      pointcut-ref:指定切点表达式的引用
                      pointcut:直接使用切点表达式(和 pointcut-ref 二选一)
            -->
            <aop:before method="myBefore" pointcut-ref="p1"/>

            <!-- aop:after-returning:配置后置通知
                 属性:returning:可选性,设置目标方法返回值的参数名,要和后置通知方法的形参名统一
                      其它属性和 aop:before 一样
            -->
            <aop:after-returning method="myAfterReturning" pointcut-ref="p1" returning="returnVal"/>
            
            <!-- aop:after-throwing:配置异常通知
                 属性:throwing:可选性,设置目标方法抛出的异常的参数名,要和异常通知方法的形参名统一
                      其它属性和 aop:before 一样
            -->
            <aop:after-throwing method="myAfterThrowing" pointcut-ref="p1" throwing="e"/> 

            <!-- aop:around:配置环绕通知,属性和 aop:before 一样 -->
            <aop:around method="myAround" pointcut-ref="p1"/>

            <!-- aop:after:配置最终通知 -->
            <aop:after method="myAfter" pointcut-ref="p1"/>
        </aop:aspect>
    </aop:config>
</beans>

此处介绍下切点表达式,格式为execution([修饰符] 返回值类型 包名.类名.方法名(参数)),可以使用两种通配符:第一种是*,表示匹配单个单词,或者是以某个词为前缀或后缀的单词;第二种是..,表示匹配 0 个或多个单词。具体使用如下:

  • 修饰符是可选项,可以省略;

  • 返回值可以使用*,匹配任意数据类型:* com.example.entity.Target.method1(int)

  • 包名可以使用* ,匹配任意包名,一个*只能代表一级包:* *.*.*.Target.method1(int)

  • 类名可以使用*,匹配任意类名:* *.*.*.*.method1(int)

  • 方法名可以使用*,匹配任意方法名:* *.*.*.*.*(int)

  • 参数列表中的参数可以使用*,匹配任意参数,一个*只能代表一个参数:* *.*.*.*.*(*)

  • 类的全限定类名(包名.类名)可以使用..,匹配 0 个或多个单词,但是..不能放在包名开头:* *..*(*)

  • 参数列表中的参数可以使用..,匹配 0 个或多个参数:* *..*(..)

📚 对正常方法 method1() 进行测试:

@Test
public void test1() {
    // 加载 Spring 配置文件,初始化 IoC 容器
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");

    // 获取 Target 类的 bean
    Target target = (Target) applicationContext.getBean("target");
    // 执行 method1() 方法
    System.out.println("目标方法返回值为:" + target.method1(0));
}
前置通知,前置代码增强...
目标对象的 toString() 方法被调用
目标对象的全限定类名:com.example.entity.Target
目标方法名:method1

环绕通知,环绕前置代码增强...

method1 方法运行中...

最终通知,最终代码增强...

环绕通知,环绕后置代码增强...

后置通知,后置代码增强...
目标方法返回值为:0

目标方法返回值为:0

📚 对异常方法 method2() 进行测试:

@Test
public void test2() {
    // 加载 Spring 配置文件,初始化 IoC 容器
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");

    // 获取 Target 类的 bean
    Target target = (Target) applicationContext.getBean("target");
    // 执行 method2() 方法
    System.out.println("目标方法返回值为:" + target.method2());
}
前置通知,前置代码增强...
目标对象的 toString() 方法被调用
目标对象的全限定类名:com.example.entity.Target
目标方法名:method2

环绕通知,环绕前置代码增强...

method2 方法运行中...

最终通知,最终代码增强...

异常通知,/ by zero

注解方式

注解方式可以采用注解 + XML 配置,也可以使用纯注解配置。

注解 + XML 方式

✌ 创建目标类

package com.example.entity;

import org.springframework.stereotype.Component;

@Component
public class Target {

    // 正常返回的方法
    public int method1(int val) {
        System.out.println("method1 方法运行中...\n");
        return val;
    }

    // 抛出异常的方法
    public int method2() {
        System.out.println("method2 方法运行中...\n");
        return 1 / 0;
    }

    @Override
    public String toString() {
        return "目标对象的 toString() 方法被调用";
    }
}

✌ 创建切面类

package com.example.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect      // 表明当前类是一个切面类
public class MyAspect {

    /**
     * @Pointcut 注解:配置切点表达式,方法名(记得带括号)是其标识
     *   value 属性——指定切点表达式内容
     */
    @Pointcut("execution(* com.example..*(..))")
    public void myPoint(){}

    /**
     * @Before 注解:配置前置通知
     *   value 属性——用于指定切点表达式或者切点表达式的引用
     */
    @Before("myPoint()")    // 注意:记得带括号
    public void myBefore(JoinPoint joinPoint) {
        System.out.println("前置通知,前置代码增强...");
        // getTarget() 返回的是目标对象,所以此处调用目标对象的 toString() 方法
        System.out.println(joinPoint.getTarget());
        System.out.println("目标对象的全限定类名:" + joinPoint.getSignature().getDeclaringTypeName());
        System.out.println("目标方法名:" + joinPoint.getSignature().getName() + "\n");
    }

    /**
     * @AfterReturning 注解:配置后置通知
     *   属性:value——用于指定切点表达式或者切点表达式的引用
     *        returning--可选性,设置目标方法返回值的参数名,要和后置通知方法的形参名统一
     */
    @AfterReturning(value = "myPoint()", returning = "returnVal")
    public void myAfterReturning(JoinPoint joinPoint, Object returnVal) {
        System.out.println("后置通知,后置代码增强...");
        System.out.println("目标方法返回值为:" + returnVal + "\n");
    }

    /**
     * @AfterThrowing 注解:配置异常通知
     *   属性:value--用于指定切点表达式或者切点表达式的引用
     *        throwing--可选性,设置目标方法抛出的异常的参数名,要和异常通知方法的形参名统一
     */
    @AfterThrowing(value = "myPoint()", throwing = "e")
    public void myAfterThrowing(JoinPoint joinPoint, Throwable e) {
        System.out.println("异常通知," + e.getMessage() + "\n");
    }

    /**
     * @Around 注解:配置环绕通知
     *   value 属性--用于指定切点表达式或者切点表达式的引用
     */
    @Around("myPoint()")
    public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知,环绕前置代码增强...\n");
        // 执行目标方法
        Object obj = proceedingJoinPoint.proceed();
        System.out.println("环绕通知,环绕后置代码增强...\n");
        // 必须存在返回值,要不然有返回值的目标方法调用者只能接收 null
        return obj;
    }

    /**
     * @After 注解:配置最终通知
     *  属性:value--用于指定切点表达式或者切点表达式的引用
     */
    @After("myPoint()")
    public void myAfter(JoinPoint joinPoint) {
        System.out.println("最终通知,最终代码增强...\n");
    }
}

✌ 在 applicationContext.xml 中开启注解扫描和 AOP 注解支持:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
        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
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 配置注解扫描的包路径 -->
    <context:component-scan base-package="com.example.entity, com.example.aspect"/>

    <!-- 开启 Spring 对注解 AOP 的支持 -->
    <aop:aspectj-autoproxy/>
</beans>

✌ 对方法 method1() 进行测试:

@Test
public void test1() {
    // 加载 Spring 配置文件,初始化 IoC 容器
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");

    // 获取 Target 类的 bean
    Target target = (Target) applicationContext.getBean("target");
    System.out.println("目标方法返回值为:" + target.method1(0));
}
环绕通知,环绕前置代码增强...

前置通知,前置代码增强...
目标对象的 toString() 方法被调用
目标对象的全限定类名:com.example.entity.Target
目标方法名:method1

method1 方法运行中...

后置通知,后置代码增强...
目标方法返回值为:0

最终通知,最终代码增强...

环绕通知,环绕后置代码增强...

目标方法返回值为:0

纯注解方式

✌ 使用 Spring 配置类来取代 XML 配置:

package com.example.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

// @Configuration  // 此处不必添加 @Configuration 注解
@ComponentScan({"com.example.entity", "com.example.aspect"}) // 配置注解扫描的包路径
@EnableAspectJAutoProxy  // 开启 Spring 对注解 AOP 的支持
public class MyConfiguration {
    
}

✌ 对方法 method1() 进行测试:

@Test
public void test1() {
    // 加载 Spring 配置类,初始化 IoC 容器
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfiguration.class);

    // 获取 Target 类的 bean
    Target target = (Target) applicationContext.getBean("target");
    System.out.println("目标方法返回值为:" + target.method1(0));
}
环绕通知,环绕前置代码增强...

前置通知,前置代码增强...
目标对象的 toString() 方法被调用
目标对象的全限定类名:com.example.entity.Target
目标方法名:method1

method1 方法运行中...

后置通知,后置代码增强...
目标方法返回值为:0

最终通知,最终代码增强...

环绕通知,环绕后置代码增强...

目标方法返回值为:0

注意事项

影响Spring AOP的通知的执行顺序的因素很多,通知注册的先后、XML 方式和注解方式的使用都会影响执行顺序。但是其核心执行顺序是不变的:

  • 前置通知 -> 目标方法执行 -> 后置通知/异常通知(两者只会执行一个)
  • 环绕通知(前置)-> 目标方法执行 -> 环绕通知(后置)
  • 目标方法执行 -> 最终通知(无论目标方法是否正常执行,都会执行)

参考

  1. 从动态代理到SpringAop以及AspectJ风格
posted @ 2020-08-21 23:16  呵呵233  阅读(252)  评论(0编辑  收藏  举报