Spring(四):AOP:Aspect Oriented Programming「面向切面编程」

AOP(Aspect Oriented Programming,面向切面编程)是一种设计思想,它是面向对象编程的一种补充和完善。它以通过预编译方式和动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术。利用 AOP 可以对业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

问题引入——日志的打印

场景构建

  1. 构建子模块 spring6-aop

  2. 代码环境

    package site.penghao.spring6.aop.example;
    
    /**
     * 计算器接口
     * @author hope
     * @date 2023/3/28 - 16:48
     */
    public interface Calculator {
        int add(int i, int j);
        int sub(int i, int j);
        int mul(int i, int j);
        int div(int i, int j);
    }
    
    package site.penghao.spring6.aop.example;
    
    /**
     * 计算器实现类
     * @author hope
     * @date 2023/3/28 - 16:49
     */
    public class CalculatorImpl implements Calculator {
        @Override
        public int add(int i, int j) {
            int result = i + j;
            System.out.println("方法内部 result = " + result);
            return result;
        }
    
        @Override
        public int sub(int i, int j) {
            int result = i - j;
            System.out.println("方法内部 result = " + result);
            return result;
        }
    
        @Override
        public int mul(int i, int j) {
            int result = i * j;
            System.out.println("方法内部 result = " + result);
            return result;
        }
    
        @Override
        public int div(int i, int j) {
            int result = i / j;
            System.out.println("方法内部 result = " + result);
            return result;
        }
    }
    
    package site.penghao.spring6.aop.example;
    
    /**
     * 带日志的计算器实现类
     *
     * @author hope
     * @date 2023/3/28 - 16:53
     */
    public class CalculatorLogImpl implements Calculator {
    
        @Override
        public int add(int i, int j) {
            System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
            int result = i + j;
            System.out.println("方法内部 result = " + result);
            System.out.println("[日志] add 方法结束了,结果是:" + result);
            return result;
        }
    
        @Override
        public int sub(int i, int j) {
            System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);
            int result = i - j;
            System.out.println("方法内部 result = " + result);
            System.out.println("[日志] sub 方法结束了,结果是:" + result);
            return result;
        }
    
        @Override
        public int mul(int i, int j) {
            System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j);
            int result = i * j;
            System.out.println("方法内部 result = " + result);
            System.out.println("[日志] mul 方法结束了,结果是:" + result);
            return result;
        }
    
        @Override
        public int div(int i, int j) {
            System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j);
            int result = i / j;
            System.out.println("方法内部 result = " + result);
            System.out.println("[日志] div 方法结束了,结果是:" + result);
            return result;
        }
    }
    

    问题:日志代码和核心业务混杂,维护不便。

代理模式

「代理模式」是二十三种设计模式中的一种,属于结构性模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离,也就是「解耦」。

image-20230328170224123

静态代理

实现方式:将代理类的实现设置为调用某个其他实现类的实现方法,将日志在代理类中打印。

package site.penghao.spring6.aop.example;

/**
 * @author hope
 * @date 2023/3/28 - 17:07
 */
public class CalculatorStaticProxy implements Calculator {
    private final Calculator calculator;

    public CalculatorStaticProxy(Calculator calculator) {
        this.calculator = calculator;
    }

    @Override
    public int add(int i, int j) {
        // 输出日志
        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
        
        // 调用目标对象的核心业务方法
        int result = calculator.add(i, j);
        
        System.out.println("[日志] add 方法结束了,结果是:" + result);
        return result;
    }

    @Override
    public int sub(int i, int j) {
        System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);
        int result = calculator.sub(i, j);
        System.out.println("[日志] sub 方法结束了,结果是:" + result);
        return result;
    }

    @Override
    public int mul(int i, int j) {
        System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j);
        int result = calculator.mul(i, j);
        System.out.println("[日志] mul 方法结束了,结果是:" + result);
        return result;
    }

    @Override
    public int div(int i, int j) {
        System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j);
        int result = calculator.div(i, j);
        System.out.println("[日志] div 方法结束了,结果是:" + result);
        return result;
    }
}

实现了解耦,但是由于代码都写死了,不具备灵活性。如果我们希望在其他地方添加日志,就需要声明更多静态代理类,这时就会产生大量的重复代码。日志功能还是分散的,没有统一管理。

——我们能不能将日志功能集中到一个代理类中,将来有任何日志需求,都通过这个代理类统一实现呢?这就需要运用动态代理技术了。

动态代理(AOP 底层)

image-20230328171620801

动态代理实现日志输出:

package site.penghao.spring6.aop.example;

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

/**
 * @author hope
 * @date 2023/3/28 - 17:18
 */
public class ProxyFactory {

    // 目标对象
    private final Object target;

    public ProxyFactory(Object target) {
        this.target = target;
    }

    // 返回代理对象(动态创建)
    public Object getProxy() {
        /*
          Proxy.newProxyInstance() 有三个参数:
          1. ClassLoader loader:加载动态生成代理类的类加载器;
          2. Class[] interfaces:目标对象实现的所有接口类型的 Class;
          3. InvocationHandler h:设置代理对象实现目标对象方法的过程。
         */
        ClassLoader classLoader = target.getClass().getClassLoader();
        Class<?>[] interfaces = target.getClass().getInterfaces();
        InvocationHandler handler = new InvocationHandler() {
            /*
              InvocationHandler 接口的 invoke 方法三个参数:
              1. Object proxy:代理对象
              2. Method method:需要重写目标对象的方法
              3. Object[] args:method 方法的参数

              即:之后使用代理对象执行某个方法的时候,会使用下面的自定义的 invoke 方法替代原本方法的 invoke 执行
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object result = null;
                try {
                    System.out.println("[动态代理][日志] " + method.getName() + ",参数:" + Arrays.toString(args));
                    result = method.invoke(target, args);
                    System.out.println("[动态代理][日志] " + method.getName() + ",结果:" + result);
                } catch (Exception e) {
                    e.printStackTrace();
                    System.out.println("[动态代理][日志] " + method.getName() + ",异常:" + e.getMessage());
                } finally {
                    System.out.println("[动态代理][日志] " + method.getName() + ",方法执行完毕");
                }
                return result;
            }
        };

        return Proxy.newProxyInstance(classLoader, interfaces, handler);
    }
}

执行方式:

@Test
public void testProxy() {
    ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl());
    Calculator proxy = (Calculator) proxyFactory.getProxy();
    proxy.add(114, 514);
}
//[动态代理][日志] add,参数:[114, 514]
//add() 方法内部 result = 628
//[动态代理][日志] add,结果:628
//[动态代理][日志] add,方法执行完毕

AOP 概念及相关术语

概述

AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域的面向切面编程,它是面向对象编程的一种补充和完善。它以通过预编译方式和动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术。利用 AOP 可以对业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

相关术语

横切关注点

方法中抽取出的同样的核心业务被称为「横切关注点」。例如:用户验证、日志管理、事务管理、数据缓存都属于横切关注点。

横切关注点分散在各个模块中,解决相同的问题。在同一个项目中,可以使用多个横切关注点对相关方法进行不同层面的增强,或者说使用不同的横切关键点实现不同的附加功能

注意:横切关注点是一个抽象的逻辑概念,对应某个抽象的业务逻辑,而不关注具体的语法实现。

在前面的示例中,就可以视为具有三个横切关注点(或者说统一到「日志打印」这一个横切关注点中):

image-20230328180227132

通知(增强)

每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就称为「通知方法」或者说「增强方法」。

  • 前置通知(@Before):在被代理的目标方法执行前执行
  • 返回通知(@AfterReturning):在被代理的目标方法成功结束后执行
  • 异常通知(@AfterThrowing):在被代理的目标方法异常结束后执行
  • 后置通知(@After):在被代理的目标方法最终结束后执行
  • 环绕通知(@Around):使用 try...catch...finally 结构围绕整个被代理的目标方法,包括上面四种通知的所有位置。
image-20230328180923041

切面

封装通知方法的

image-20230328181010622

目标

被代理的目标对象。

代理

向目标对象应用通知之后创建的代理对象。

连接点

把方法排成一排,每一个横切位置看成 x 方向,把方法从上到下执行的顺序看成 y 轴,x 轴和 y 轴的交叉点就是连接点。具体地说,连接点就是 spring 允许你使用通知的地方

image-20230328181702556

切入点

可以简单理解为要实际去增强的方法。spring 的 AOP 技术可以通过切入点定位到特定的连接点。

AOP 的作用

  • 简化代码:把方法中固定位置的重复代码抽取,让被抽取的方法更专注于自己的核心功能,提高内聚性。
  • 代码增强:把特定的功能封装到切面类中,通过套用切面逻辑实现方法的增强。

基于注解的 AOP(重要)

基于注解的 AOP 框架

Spring 基于注解的 AOP 基本框架:

image-20230329110845540

AspectJ 注解层

AspectJ 是一个 AOP 框架,其本质是静态代理将代理逻辑「织入」被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver 就是织入器,Spring 只是借用了 AspectJ 中的注解。

具体实现层

具体实现层的动态代理方式有两种:

  • 基于 JDK 的动态代理:代理对象和目标对象都实现相同的接口
  • cglib 的动态代理:代理对象继承目标对象
image-20230329110954441

使用 AOP 框架

Step 1. 引入依赖和创建 spring 配置文件

    <!--spring aop依赖-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>6.0.2</version>
    </dependency>
    <!--spring aspects依赖-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>6.0.2</version>
    </dependency>
<?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:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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 https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 开启组件扫描 -->
    <context:component-scan base-package="site.penghao.spring6.aop.anno_aop"/>

    <!-- 开启 AspectJ 自动代理,为目标对象生成代理 -->
    <aop:aspectj-autoproxy/>
    
</beans>

Step 2. 创建目标资源

为了简化步骤,这里使用前面的 Calculator 和 CalculatorImpl 类作为目标资源。

注意:测试时,需要在 CalculatorImpl 前加上 @Component 注解。

Step 3. 创建切面类

通知类型和注解
  • @Before:前置通知
  • @AfterReturning:返回通知
  • @AfterThrowing:异常通知
  • @After:后置通知
  • @Around:环绕通知

具体的通知类型注解为 @Before(value = "切入点表达式") 切入点表达式的定义见下节。

切入点表达式

通过设置切入点表达式,spring 可以实现对目标方法的选择:

image-20230329113852052

语法图示:

image-20230329113913591
编写和测试切面类

使用下面的切面类进行测试:

测试时,注意导入的是我们配置的 anno_aop 包中的类,否则我们的配置根本就没生效!

如果工程中存在同名类,一定要谨慎地导入。

package site.penghao.spring6.aop.anno_aop;

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

import java.lang.reflect.Array;
import java.util.Arrays;

/**
 * 切面类
 *
 * @author hope
 * @date 2023/3/29 - 11:24
 */
@Component  // IoC 容器
@Aspect     // 切面类
public class LogAspect {

    // 设置切入点和通知类型

    // 前置通知:@Before(value = "切入点表达式")
    // 下面的切入点表达式意思是在 Calculator 类的全部修饰符为 public 并且返回值为 int 的方法
    // 整个注解表示在 Calculator 类的全部修饰符为 public 并且返回值为 int 的方法执行前执行注解修饰的方法
    @Before(value = "execution(public int site.penghao.spring6.aop.anno_aop.Calculator.*(..))")
    public void beforeMethod() {
        System.out.println("------------ Log Before -------------");
    }

    // 参数 joinPoint 整合了切入时的各种有用信息,包含调用的方法、参数等
    @After(value = "execution(public int site.penghao.spring6.aop.anno_aop.Calculator.*(..))")
    public void afterMethod(JoinPoint joinPoint) {
        System.out.println("------------ Log After -------------");

        System.out.println("调用的方法:" + joinPoint.getSignature().getName());

        System.out.println("参数信息:"+ Arrays.toString(joinPoint.getArgs()));
    }

    // 对于返回通知,还可以添加 returning 参数,获取返回值
    // 注意,returning 设置的参数名称和方法的参数名称应该完全一致,比如下面的 res
    @AfterReturning(value = "execution(public int site.penghao.spring6.aop.anno_aop.Calculator.*(..))", returning = "res")
    public void afterReturningMethod(JoinPoint joinPoint, Object res) {
        System.out.println("------------ Log AfterReturning -------------");

        System.out.println("调用的方法:" + joinPoint.getSignature().getName());

        System.out.println("返回值信息:"+ res.toString());
    }

    // 对于异常通知,还可以添加 throwing 参数,获取异常信息(Error 或者 Exception)
    // 注意,throwing 设置的参数名称和方法的参数名称应该完全一致,比如下面的 exp
    @AfterThrowing(value = "execution(public int site.penghao.spring6.aop.anno_aop.Calculator.*(..))", throwing = "exp")
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable exp) {
        System.out.println("------------ Log AfterThrowing -------------");

        System.out.println("调用的方法:" + joinPoint.getSignature().getName());

        System.out.println("返回值信息:"+ exp.toString());
    }

    // 本质上,环绕通知会直接替换掉原方法执行。
    // 所以我们能使用 try...catch...finally 语句实现上述方法的全部通知
    // 环绕通知使用一个 JointPoint 的子类 ProceedingJoinPoint,它是 JointPoint 的增强,
    // 能够使用 proceed() 调用目标方法执行
    // 注意:@Around 修饰的方法应该包含返回值,因为 spring 替换了原方法,要使函数运行不出问题,就需要
    // 保证 aroundMethod 的返回值和原函数返回值一致。
    @Around(value = "execution(public int site.penghao.spring6.aop.anno_aop.Calculator.*(..))")
    public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) {

        String methodName = proceedingJoinPoint.getSignature().getName();
        Object[] args = proceedingJoinPoint.getArgs();
        Object ret = null;

        try {
            // 相当于 @Before 位置
            System.out.println("环绕通知:目标方法执行之前执行");

            // 调用目标方法,能够获得返回值
            ret = proceedingJoinPoint.proceed();

            // 相当于 @AfterReturning 位置
            System.out.println("环绕通知:目标方法返回之后执行");
        } catch (Throwable throwable) {

            // 相当于 @AfterThrowing 位置
            System.out.println("环绕通知:目标方法异常发生之后执行");
        } finally {

            // 相当于 @After 位置
            System.out.println("环绕通知:目标方法执行之后执行");
        }
        return ret;
    }}

测试结果:

    @Test
    public void aopTest() {
        ApplicationContext context =
                new ClassPathXmlApplicationContext("bean.xml");
        Calculator calculator = context.getBean(Calculator.class);

        System.out.println("================================= 无异常执行 =================================");
        calculator.add(114, 514);
        System.out.println("================================= 有异常执行 =================================");
        calculator.div(114514, 0);
    }
================================= 无异常执行 =================================
环绕通知:目标方法执行之前执行
------------ Log Before -------------
add() 方法内部 result = 628
------------ Log AfterReturning -------------
调用的方法:add
返回值信息:628
------------ Log After -------------
调用的方法:add
参数信息:[114, 514]
环绕通知:目标方法返回之后执行
环绕通知:目标方法执行之后执行
================================= 有异常执行 =================================
环绕通知:目标方法执行之前执行
------------ Log Before -------------
------------ Log AfterThrowing -------------
调用的方法:div
返回值信息:java.lang.ArithmeticException: / by zero
------------ Log After -------------
调用的方法:div
参数信息:[114514, 0]
环绕通知:目标方法异常发生之后执行
环绕通知:目标方法执行之后执行
重用切入点表达式

可以通过注解实现切入点表达式的定义和重用

  1. 通过在方法前加入 @Pointcut("切入点表达式") 进行声明

        @Pointcut("execution(public int site.penghao.spring6.aop.anno_aop.Calculator.*(..))")
        public void myPointCut(){}
    
  2. 在同一个切面类使用

    @Around(value = "myPointCut()")
    public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) {
    	// ...
    }
    
  3. 在不同的切面类使用

    @AfterThrowing(value = "site.penghao.spring6.aop.anno_aop.LogAspect.myPointCut()", throwing = "exp")
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable exp) {
        // ...
    }
    
切面的优先级

可以使用 @Order(优先级) 控制优先级,优先级数字越小优先级越高。优先级高的先执行,优先级低的后执行。

从执行结果上看,优先级高的切面在外层,优先级低的切面在内层:

image-20230330102825547

基于 XML 的 AOP

在 XML 配置文件配置 AOP 可以参考下面的方式:

    <!-- 开启组件扫描 -->
    <context:component-scan base-package="site.penghao.spring6.aop.anno_aop"/>

    <aop:config>
        <!--配置切面类-->
        <aop:aspect ref="logAspect">
            <aop:pointcut id="pointCut"
                          expression="execution(public int site.penghao.spring6.aop.anno_aop.Calculator.*(..))"/>
            <aop:before method="beforeMethod" pointcut-ref="pointCut"/>
            <aop:after method="afterMethod" pointcut-ref="pointCut"/>
            <aop:after-returning method="afterReturningMethod" returning="res" pointcut-ref="pointCut"/>
            <aop:after-throwing method="afterThrowingMethod" throwing="exp" pointcut-ref="pointCut"/>
            <aop:around method="aroundMethod" pointcut-ref="pointCut"/>
        </aop:aspect>
    </aop:config>

小结

  • AOP 是一种设计思想,它是面向对象编程的一种补充和完善。利用 AOP 可以对业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发的效率。
  • AOP 是基于预编译方式和动态代理方式实现的,动态代理就是一种通过代理类调用目标类实现目标类功能动态增强的设计模式。
  • 通知(增强)是实现各个横切关注点的途径,根据它和目标方法的关系可以分为前置通知、返回通知、异常通知、后置通知和环绕通知。
  • 基于注解的 AOP 主要步骤有:引入依赖、创建配置文件、创建目标资源、创建切面类。
  • 使用 @Aspect 注解标识切面类,使用 @Before、@After、@AfterReturning、@AfterThrowing、@Around 等注解标识通知类型。
  • 标注通知类型时,需要设定切入点表达式。也就是指明这个通知方法对于哪些目标类的哪些方法生效。
  • 可以使用 @Pointcut 注解实现切入点表达式的重用,以简化开发。
  • 也可以使用 XML 配置 AOP。