Spring AOP

代理模式

场景:我们设计一个计算器类,在加减乘除功能添加日记功能,表明是那个功能执行了

声明接口

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);

}

实现接口

public class CalculatorImpl 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;
  }
}

出现的问题

代码功能重复

对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力

附加功能分散在各个业务功能方法中,不利于统一维护

解决方法

解耦,抽取重复代码,由于在方法内部无法抽取到父类,所以使用代理解决问题

代理模式的概念

二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护

静态代理

@Slf4j
public class CalculatorStaticProxy implements Calculator {

  // 将被代理的目标对象声明为成员变量
  private Calculator target;

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

  @Override
  public int add(int i, int j) {

    // 附加功能由代理类中的代理方法来实现
    log.debug("[日志] add 方法开始了,参数是:" + i + "," + j);

    // 通过目标对象来实现核心业务逻辑
    int addResult = target.add(i, j);

    log.debug("[日志] add 方法结束了,结果是:" + addResult);

    return addResult;
  }
  ……
}

总结

  • 静态代理实现解耦,抽取代码
  • 问题:声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理

动态代理

解决上面静态代理,分散和重复代码问题

生产代理对象的工厂类

JDK本身就支持动态代理,这是反射技术的一部分

创建一个代理类(生产代理对象的工厂类):

package ioc.proxy;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.proxy.InvocationHandler;
import org.springframework.cglib.proxy.Proxy;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

@Slf4j
// 泛型T要求是目标对象实现的接口类型,本代理类根据这个接口来进行代理
public class LogDynamicProxyFactory<T> {

  // 将被代理的目标对象声明为成员变量
  private T target;

  public LogDynamicProxyFactory(T target) {
    this.target = target;
  }

  public T getProxy() {

    // 创建代理对象所需参数一:加载目标对象的类的类加载器
    ClassLoader classLoader = target.getClass().getClassLoader();

    // 创建代理对象所需参数二:目标对象的类所实现的所有接口组成的数组
    Class<?>[] interfaces = target.getClass().getInterfaces();

    // 创建代理对象所需参数三:InvocationHandler对象
    // Lambda表达式口诀:
    // 1、复制小括号
    // 2、写死右箭头
    // 3、落地大括号
    InvocationHandler handler = (
      // 代理对象,当前方法用不上这个对象
      Object proxy,

      // method就是代表目标方法的Method对象
      Method method,

      // 外部调用目标方法时传入的实际参数
      Object[] args)->{

      // 我们对InvocationHandler接口中invoke()方法的实现就是在调用目标方法
      // 围绕目标方法的调用,就可以添加我们的附加功能

      // 声明一个局部变量,用来存储目标方法的返回值
      Object targetMethodReturnValue = null;

      // 通过method对象获取方法名
      String methodName = method.getName();

      // 为了便于在打印时看到数组中的数据,把参数数组转换为List
      List<Object> argumentList = Arrays.asList(args);

      try {

        // 在目标方法执行前:打印方法开始的日志
        log.debug("[动态代理][日志] " + methodName + " 方法开始了,参数是:" + argumentList);

        // 调用目标方法:需要传入两个参数
        // 参数1:调用目标方法的目标对象
        // 参数2:外部调用目标方法时传入的实际参数
        // 调用后会返回目标方法的返回值
        targetMethodReturnValue = method.invoke(target, args);

        // 在目标方法成功后:打印方法成功结束的日志【寿终正寝】
        log.debug("[动态代理][日志] " + methodName + " 方法成功结束了,返回值是:" + targetMethodReturnValue);

      }catch (Exception e){

        // 通过e对象获取异常类型的全类名
        String exceptionName = e.getClass().getName();

        // 通过e对象获取异常消息
        String message = e.getMessage();

        // 在目标方法失败后:打印方法抛出异常的日志【死于非命】
        log.debug("[动态代理][日志] " + methodName + " 方法抛异常了,异常信息是:" + exceptionName + "," + message);

      }finally {

        // 在目标方法最终结束后:打印方法最终结束的日志【盖棺定论】
        log.debug("[动态代理][日志] " + methodName + " 方法最终结束了");

      }

      // 这里必须将目标方法的返回值返回给外界,如果没有返回,外界将无法拿到目标方法的返回值
      return targetMethodReturnValue;
    };

    // 创建代理对象
    T proxy = (T) Proxy.newProxyInstance(classLoader, interfaces, handler);

    // 返回代理对象
    return proxy;
  }
}

测试

@Test
public void test1() {
  // 创建被代理的对象
  Calculator calculator = new CalculatorImpl();
  // /创建能够生产代理对象的工厂对象
  LogDynamicProxyFactory<Calculator> calculatorProxy = new LogDynamicProxyFactory<>(calculator);
  Calculator proxy = calculatorProxy.getProxy();
  int res = proxy.add(10, 20);
  log.debug("add res= " + res);

}

执行结果

/**
 * [动态代理][日志] add 方法开始了,参数是:[10, 20]
 * [日志] add 方法开始了,参数是:10,20
 * 方法内部 result = 30
 * [日志] add 方法结束了,结果是:30
 * [动态代理][日志] add 方法成功结束了,返回值是:30
 * [动态代理][日志] add 方法最终结束了
 * add res= 30
 */

AOP

aop 面向切面编程

作用:

  • 简化代码:把方法某个位置的重复代码抽取处理
  • 增强代码:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了

核心套路

横切关注点

横切关注点是一个『逻辑层面』的概念,而不是『语法层面』的概念。

从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强,有十个附加功能,就有十个横切关注

通知

每一个横切关注点要实现的功能所写的方法就是通知方法

通知分类

  • 前置通知:在被代理的目标方法前执行
  • 返回通知:在被代理的目标方法成功结束后执行
  • 异常通知:在被代理的目标方法异常结束后执行
  • 后置通知:在被代理的目标方法最终结束后执行
  • 环绕通知:使用 try…catch…finally 结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

切面

封装通知方法的类就是切面类

  • 日志功能:日志切面
  • 缓存功能:缓存切面
  • 事务功能:事务切面

目标和代理

目标就是被代理的目标对象

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

就动态代理技术而言,JDK会在运行过程中根据我们提供的接口动态生成接口的实现类。那么我们这里谈到的代理对象就是这个动态生成的类的对象

连接点

把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点

切入点

定位连接点的方式。

我们通过切入点,可以将通知方法精准的植入到被代理目标方法的指定位置。

每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。

如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。

Spring 的 AOP 技术可以通过切入点定位到特定的连接点。

切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

封装了代理逻辑的通知方法就像一颗制导导弹,在切入点这个引导系统的指引下精确命中连接点这个打击目标

注解实现AOP

添加依赖

<!-- spring-aspects会帮我们传递过来aspect jweaver -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>5.3.1</version>
</dependency>

配置bean

组件

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);
}
@Component
@Slf4j
public class CalculatorImpl implements Calculator {
    @Override
    public int add(int i, int j) {
        int result = i + j;
        log.debug("方法内部 result = " + result);
        return result;
    }
  ...
}

创建切面类

@Slf4j
@Aspect
@Component
public class LogAspect {
  /**
  * 前置通知
  * Before 注解表示前置通知方法,value填写指定切入点表达式,表示用于哪一个方法上
  */
  @Before(value = "execution(public int aopComponent.Calculator.add(int,int))")
  public void beforeLog(){
    log.debug("前置通知:方法开始了");
  }

  /**
     * 返回通知
     * AfterReturning 注解表示返回通知方法
     */
  @AfterReturning(value = "execution(public int aopComponent.Calculator.add(int,int))")
  public void afterReturnLog(){
    log.debug("返回通知:方法返回了");

  }

  /**
     * 异常通知
     * AfterThrowing 注解表示异常通知方法
     */
  @AfterThrowing(value = "execution(public int aopComponent.Calculator.add(int,int))")
  public void afterThrowLog(){
    log.debug("返回通知:方法出现异常了");

  }

  /**
     * 后置通知
     * After 注解表示后置通知方法
     */
  @After(value = "execution(public int aopComponent.Calculator.add(int,int))")
  public void afterLog(){
    log.debug("返回通知:方法结束了");

  }
}

spring 配置文件

<!-- 开启基于注解的AOP功能 -->
<aop:aspectj-autoproxy/>
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="aopComponent"/>

测试代码

@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:aop.xml"})
public class AOPTest {

    @Autowired
    private Calculator calculator;

    @Test
    public void testAnnotationAOP() {
        int res = calculator.add(10,20);
        log.debug("结果:add 10+20: "+ res);
        /* 运行结果
        前置通知:方法开始了
        方法内部 result = 30
        返回通知:方法返回了
        后置通知:方法结束了
        结果:add 10+20: 30
         */
    }
}

没有接口的情况

在目标类没有实现任何接口的情况下,Spring会自动使用cglib技术实现代理

xml 实现 aop

不常用,推荐使用注解

配置文件

<!-- 配置目标类的bean -->
<bean id="calculatorPure" class="example.aop.imp.CalculatorPureImpl"/>

<!-- 配置切面类的bean -->
<bean id="logAspect" class="example.aop.aspect.LogAspect"/>

<!-- 配置AOP -->
<aop:config>

  <!-- 配置切入点表达式 -->
  <aop:pointcut id="logPointCut" expression="execution(* *..*.*(..))"/>

  <!-- aop:aspect标签:配置切面 -->
  <!-- ref属性:关联切面类的bean -->
  <aop:aspect ref="logAspect">
    <!-- aop:before标签:配置前置通知 -->
    <!-- method属性:指定前置通知的方法名 -->
    <!-- pointcut-ref属性:引用切入点表达式 -->
    <aop:before method="printLogBeforeCore" pointcut-ref="logPointCut"/>

    <!-- aop:after-returning标签:配置返回通知 -->
    <!-- returning属性:指定通知方法中用来接收目标方法返回值的参数名 -->
    <aop:after-returning
                         method="printLogAfterCoreSuccess"
                         pointcut-ref="logPointCut"
                         returning="targetMethodReturnValue"/>

    <!-- aop:after-throwing标签:配置异常通知 -->
    <!-- throwing属性:指定通知方法中用来接收目标方法抛出异常的异常对象的参数名 -->
    <aop:after-throwing
                        method="printLogAfterCoreException"
                        pointcut-ref="logPointCut"
                        throwing="targetMethodException"/>

    <!-- aop:after标签:配置后置通知 -->
    <aop:after method="printLogCoreFinallyEnd" pointcut-ref="logPointCut"/>

    <!-- aop:around标签:配置环绕通知 -->
    <!--<aop:around method="……" pointcut-ref="logPointCut"/>-->
  </aop:aspect>

</aop:config>

获取方法信息

JoinPoint接口

JoinPoint接口可以获取各个通知获取细节信息

  1. JoinPoint 接口通过 getSignature() 方法获取目标方法的签名(方法声明时的完整信息)

  2. 通过目标方法签名对象获取方法名

  3. 通过 JoinPoint 对象获取外界调用目标方法时传入的实参列表组成的数组

需要获取方法签名、传入的实参等信息时,可以在通知方法声明JoinPoint类型的形参

// @Before注解标记前置通知方法
// value属性:切入点表达式,告诉Spring当前通知方法要套用到哪个目标方法上
// 在前置通知方法形参位置声明一个JoinPoint类型的参数,Spring就会将这个对象传入
// 根据JoinPoint对象就可以获取目标方法名称、实际参数列表
@Before(value = "execution(public int aopComponent.Calculator.add(int,int))")
public void printLogBeforeCore(JoinPoint joinPoint) {

  // 1.通过JoinPoint对象获取目标方法签名对象
  // 方法的签名:一个方法的全部声明信息
  Signature signature = joinPoint.getSignature();

  // 2.通过方法的签名对象获取目标方法的详细信息
  String methodName = signature.getName();
  log.debug("methodName = " + methodName);

  int modifiers = signature.getModifiers();
  log.debug("modifiers = " + modifiers);

  String declaringTypeName = signature.getDeclaringTypeName();
  log.debug("declaringTypeName = " + declaringTypeName);

  // 3.通过JoinPoint对象获取外界调用目标方法时传入的实参列表
  Object[] args = joinPoint.getArgs();

  // 4.由于数组直接打印看不到具体数据,所以转换为List集合
  List<Object> argList = Arrays.asList(args);

  log.debug("[AOP前置通知] " + methodName + "方法开始了,参数列表:" + argList);
}

方法返回值

在返回通知中,通过 @AfterReturning注解的 returning 属性获取目标方法的返回值

// @AfterReturning注解标记返回通知方法
// 在返回通知中获取目标方法返回值分两步:
// 第一步:在@AfterReturning注解中通过returning属性设置一个名称
// 第二步:使用returning属性设置的名称在通知方法中声明一个对应的形参
@AfterReturning(
  value = "execution(public int aopComponent.Calculator.add(int,int))",
  returning = "methodReturnVal"
)
public void afterReturnLog(Object methodReturnVal){

  log.debug("返回通知:方法返回值:" + methodReturnVal);

}

方法抛出的异常

在异常通知中,通过@AfterThrowing注解的throwing属性获取目标方法抛出的异常对象

// @AfterThrowing注解标记异常通知方法
// 在异常通知中获取目标方法抛出的异常分两步:
// 第一步:在@AfterThrowing注解中声明一个throwing属性设定形参名称
// 第二步:使用throwing属性指定的名称在通知方法声明形参,Spring会将目标方法抛出的异常对象从这里传给我们
@AfterThrowing(
  value = "execution(public int aopComponent.Calculator.add(int,int))",
  throwing = "targetMethodEx"
)
public void afterThrowLog(Throwable targetMethodEx){
  log.debug("返回通知:方法出现异常: " + targetMethodEx);

}

切入点表达式

语法细节

  • *号代替“权限修饰符”和“返回值”这两个部分的整体,表示“权限修饰符”和“返回值”不限

  • 在包名的部分,一个*号只能代表包的层次结构中的一层,表示这一层是任意的

    比如:*.Hello 匹配com.Hello,不匹配com.example.Hello

  • 在包名的部分,使用*..表示包名任意、包的层次深度任意

  • 在类名的部分,类名部分整体用*号代替,表示类名任意

  • 在类名的部分,可以使用*号代替类名的一部分

  • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符

    正确写法:execution(public int *..*Service.*(.., int))

    错误写法:execution(* int *..*Service.*(.., int))

典型场景:在基于 XML 的声明式事务配置中需要指定切入点表达式。这个切入点表达式通常都会套用到所有 Service 类(接口)的所有方法。那么切入点表达式将如下所示

execution(* *..*Service.*(..))

重用切入点表达式

我们可以先声明一个切入点表达式,然后在需要用的地方直接引用即可,方便维护

声明

// 切入点表达式重用
@Pointcut("execution(public int example.aop.api.Calculator.add(int,int)))")
public void declarPointCut() {}

同一个类内部引用

@Before(value = "declarPointCut()")
public void printLogBeforeCoreOperation(JoinPoint joinPoint) {

不同类引用

@Around(value = "example.aop.aspect.LogAspect.declarPointCut()")
public Object roundAdvice(ProceedingJoinPoint joinPoint) {

集中管理

而作为存放切入点表达式的类,可以把整个项目中所有切入点表达式全部集中过来,便于统一管理:

@Component
public class myPointCut {

  @Pointcut(value = "execution(public int *..Calculator.sub(int,int))")
  public void atguiguGlobalPointCut(){}

  @Pointcut(value = "execution(public int *..Calculator.add(int,int))")
  public void atguiguSecondPointCut(){}

  @Pointcut(value = "execution(* *..*Service.*(..))")
  public void transactionPointCut(){}
}

环绕通知

@Around(value = "execution(public int aopComponent.Calculator.add(int,int))")
public Object aroundLog(ProceedingJoinPoint  joinPoint){
  // 通过ProceedingJoinPoint对象获取外界调用目标方法时传入的实参数组
  Object[] args = joinPoint.getArgs();

  // 通过ProceedingJoinPoint对象获取目标方法的签名对象
  Signature signature = joinPoint.getSignature();

  // 通过签名对象获取目标方法的方法名
  String methodName = signature.getName();

  // 声明变量用来存储目标方法的返回值
  Object targetMethodReturnValue = null;

  try {

    // 在目标方法执行前:开启事务(模拟)
    log.debug("[AOP 环绕通知] 开启事务,方法名:" + methodName + ",参数列表:" + Arrays.asList(args));

    // 过ProceedingJoinPoint对象调用目标方法
    // 目标方法的返回值一定要返回给外界调用者
    targetMethodReturnValue = joinPoint.proceed(args);

    // 在目标方法成功返回后:提交事务(模拟)
    log.debug("[AOP 环绕通知] 提交事务,方法名:" + methodName + ",方法返回值:" + targetMethodReturnValue);

  }catch (Throwable e){

    // 在目标方法抛异常后:回滚事务(模拟)
    log.debug("[AOP 环绕通知] 回滚事务,方法名:" + methodName + ",异常:" + e.getClass().getName());

  }finally {

    // 在目标方法最终结束后:释放数据库连接
    log.debug("[AOP 环绕通知] 释放数据库连接,方法名:" + methodName);

  }

  return targetMethodReturnValue;
}

切面的优先级

相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。

  • 优先级高的切面:外面
  • 优先级低的切面:里面

使用 @Order 注解可以控制切面的优先级

  • @Order(较小的数):优先级高
  • @Order(较大的数):优先级低

AOP 管理 bean 场景

根据类型获取 bean

场景一

bean 对应的类没有实现任何接口

根据 bean 本身的类型获取 bean

  • 测试:IOC容器中同类型的 bean 只有一个

    正常获取到 IOC 容器中的那个 bean 对象

  • 测试:IOC容器中同类型的 bean 只有多个

    会抛出 NoUniqueBeanDefinitionException 异常,表示 IOC 容器中这个类型的 bean 有多个

场景二

bean 对应的类实现了接口,这个接口也只有这一个实现类

  • 测试:根据接口类型获取 bean
  • 测试:根据类获取 bean
  • 结论:上面两种情况其实都能够正常获取到 bean,而且是同一个对象

场景三

声明一个接口

接口有多个实现类

接口所有实现类都放入 IOC 容器

  • 测试:根据接口类型获取 bean

    会抛出 NoUniqueBeanDefinitionException 异常,表示 IOC 容器中这个类型的 bean 有多个

  • 测试:根据类获取bean

    正常

场景四

声明一个接口

接口有一个实现类

创建一个切面类,对上面接口的实现类应用通知

  • 测试:根据接口类型获取bean

    正常

  • 测试:根据类获取bean

    无法获取

无法根据类获取 bean 的原因:

  • 应用了切面后,真正放在IOC容器中的是代理类的对象
  • 目标类并没有被放到IOC容器中,所以根据目标类的类型从IOC容器中是找不到的
  • 从内存分析的角度来说,IOC容器中引用的是代理对象,代理对象引用的是目标对象。IOC容器并没有直接引用目标对象,所以根据目标类本身在IOC容器范围内查找不到

场景五

声明一个类

创建一个切面类,对上面的类应用通知

  • 测试:根据类获取 bean,能获取到(静态代理)


参考资料