在这里插入图片描述

Ⅰ. AOP 概述

什么是AOP?

  • Aspect Oriented Programming(面向切面编程)

什么是面向切面编程呢?

  • 切面就是指某一类特定问题,所以 AOP 也可以理解为面向特定方法编程。

什么是面向特定方法编程呢?

比如上个章节学习的 “登录校验”,就是一类特定问题。登录校验拦截器,就是对 “登录校验” 这类问题的统一处理。所以,拦截器也是 AOP 的一种应用。AOP 是一种思想,拦截器是 AOP 思想的一种实现。Spring 框架实现了这种思想,提供了拦截器技术的相关接口。

同样的,统一数据返回格式和统一异常处理,也是 AOP 思想的一种实现。

简单来说:AOP 是一种思想,是对某一类事情的集中处理

而 Spring AOP 是 Spring 框架提供的基于代理的面向切面编程实现,用于在不修改源代码的前提下对程序进行增强。比如说统一管理日志打印、权限控制、事务管理等重复、与业务无关的逻辑。

Ⅱ. Spring AOP 快速入门

需求:统计图书系统各个接口方法的执行时间。

1. 引入依赖

首先在 pom.xml 文件中添加配置:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2. 编写 AOP 程序

记录 Controller 中每个方法的执行时间

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class TimeAspect {
    /**
     * 记录方法耗时
     */
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        // 记录方法执行开始时间
        long begin = System.currentTimeMillis();
        
        // 执行原始方法
        Object result = pjp.proceed();
        
        // 记录方法执行结束时间
        long end = System.currentTimeMillis();
        
        // 记录方法执行耗时
        log.info(pjp.getSignature() + "执行耗时: {}ms", end - begin);
        return result;
    }
}
  1. @Aspect:标识这是一个切面类
  2. @Around:环绕通知,在目标方法的前后都会被执行,后面的表达式表示对哪些方法进行增强。
  3. ProceedingJoinPoint.proceed():执行原始方法,也就是指定要运行的那些方法

💥注意事项:

  1. 如果没有返回 pjp.proceed() 的结果对象 result ,则会导致程序死循环或功能异常
  2. 在使用 @Around 时,必须调用 pjp.proceed() 方法
  3. 这里是为了举例子,通常不能直接拦截所有 controller,否则浏览器访问的时候可能携带了其他访问资源,而后端因为拦截所有资源路径而导致不断重定向尝试,最后缓存炸了。

整个代码划分为三部分:

在这里插入图片描述

通过上面的程序,也可以感受到 AOP 面向切面编程的一些优势:

  • 代码无侵入:不修改原始的业务方法,就可以对原始的业务方法进行了功能的增强或者是功能的改变
  • 减少了重复代码
  • 提高开发效率
  • 维护方便

Ⅲ. Spring AOP 详解

一、Spring AOP 核心概念

概念含义
切面(Aspect)横切逻辑的模块(一个类),切面 = 切点 + 通知 + 连接点
通知(Advice)就是具体要做的工作,比如上面的 “记录代码耗时”
切入点(Pointcut)指定哪些方法要被拦截 比如 execution(* com.example.demo.controller.*.*(..)) 就是切点表达式
连接点(JoinPoint)满足切点表达式规则的方法,就是连接点比如切点表达式指定的 com.example.demo.controller 路径下的方法,都是连接点

在这里插入图片描述

切点和连接点的关系:

  • 连接点是满足切点表达式的元素
  • 切点可以看做是保存了众多连接点的一个集合

比如:

  • 切点表达式:学校全体教师
  • 连接点:张三、李四等各个老师

二、通知类型

注解含义
@Before在目标方法执行前执行
@After在目标方法执行后(无论是否异常)
@AfterReturning在方法返回后执行(有异常不会执行)
@AfterThrowing在方法抛出异常时执行
@Around环绕通知,可控制方法执行前后时机,最强大
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class AspectDemo {
    // 前置通知
    @Before("execution(* com.example.demo.controller.*.*(..))")
    public void doBefore() {
        log.info("执行 Before 方法");
    }
    
    // 后置通知
    @After("execution(* com.example.demo.controller.*.*(..))")
    public void doAfter() {
        log.info("执行 After 方法");
    }
    
    // 返回后通知
    @AfterReturning("execution(* com.example.demo.controller.*.*(..))")
    public void doAfterReturning() {
        log.info("执行 AfterReturning 方法");
    }
    
    // 抛出异常后通知
    @AfterThrowing("execution(* com.example.demo.controller.*.*(..))")
    public void doAfterThrowing() {
        log.info("执行 doAfterThrowing 方法");
    }
    
    // 添加环绕通知
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("Around 方法开始执行");
        Object result = joinPoint.proceed();
        log.info("Around 方法结束执行");
        return result;
    }
}

写一些测试程序:

package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        return "t1";
    }
    
    @RequestMapping("/t2")
    public boolean t2() {
        int a = 10 / 0;
        return true;
    }
}
  1. 正常运行的情况:http://127.0.0.1:8080/test/t1

观察日志:

在这里插入图片描述

  • 程序正常运行的情况下, @AfterThrowing 标识的通知方法不会执行
  • @Around 标识的通知方法包含两部分,一个 “前置逻辑”,一个 “后置逻辑”。其中 “前置逻辑” 先于 @Before 标识的通知方法执行“后置逻辑” 晚于 @After 标识的通知方法执行

在这里插入图片描述

  1. 异常时的情况:http://127.0.0.1:8080/test/t2

观察日志:

在这里插入图片描述

程序发生异常的情况下:

  • @AfterReturning 标识的通知方法在出现异常时候不会执行
  • @Around 环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了(因为原始方法调用出异常了)

在这里插入图片描述

💥注意事项:

  • @Around 环绕通知需要调用 ProceedingJoinPoint.proceed() 来让原始方法执行,而其它通知类型不需要考虑目标方法的执行。
  • @Around 环绕通知方法的返回值,必须指定为 Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。
  • 一个切面类可以有多个切点。

三、@PointCut

上面代码存在一个问题,就是存在大量重复的切点表达式 execution(* com.example.demo.controller.*.*(..)),Spring 提供了 @PointCut 注解,把公共的切点表达式提取出来,需要用到时引用该切入点表达式即可

上述代码就可以修改为:

@Slf4j
@Aspect
@Component
public class AspectDemo {
    // 定义切点(公共的切点表达式)
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    private void pt(){}
    
    // 前置通知
    @Before("pt()")
    public void doBefore() {
        //...代码省略
    }
    
    // 后置通知
    @After("pt()")
    public void doAfter() {
        //...代码省略
    }
    
    // 返回后通知
    @AfterReturning("pt()")
    public void doAfterReturning() {
        //...代码省略
    }
    
    // 抛出异常后通知
    @AfterThrowing("pt()")
    public void doAfterThrowing() {
        //...代码省略
    }
    
    // 添加环绕通知
    @Around("pt()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        //...代码省略
    }
}
  • 当切点定义使用 private 修饰时,仅能在当前切面类中使用。

  • 当其他切面类也要使用当前切点定义时,就需要把 private 改为 public,引用方式为:全限定类名.方法名()

      @Slf4j
      @Aspect
      @Component
      public class AspectDemo2 {
          @Before("com.example.demo.aspect.AspectDemo.pt()") // 引用别的文件中的切点!
          public void doBefore() {
              log.info("执行 AspectDemo2 -> Before 方法");
          }
      }
    

四、切面优先级 @Order

当我们在一个项目中,定义了多个切面类时,并且这些切面类的多个切入点都匹配到了同一个目标方法。当目标方法运行的时候,这些切面类中的通知方法都会执行,那么这几个通知方法的执行顺序是什么样的呢?

AspectDemo2

@Component
public class AspectDemo2 {
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    private void pt(){}
    
    //前置通知
    @Before("pt()")
    public void doBefore() {
        log.info("执行 AspectDemo2 -> Before 方法");
    }
    
    //后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执行 AspectDemo2 -> After 方法");
    }
}

AspectDemo3

@Component
public class AspectDemo3 {
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    private void pt(){}
    
    //前置通知
    @Before("pt()")
    public void doBefore() {
        log.info("执行 AspectDemo3 -> Before 方法");
    }
    
    //后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执行 AspectDemo3 -> After 方法");
    }
}

AspectDemo4

@Component
public class AspectDemo4 {
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    private void pt(){}
    
    //前置通知
    @Before("pt()")
    public void doBefore() {
        log.info("执行 AspectDemo4 -> Before 方法");
    }
    
    //后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执行 AspectDemo4 -> After 方法");
    }
}

运行程序,访问接口:http://127.0.0.1:8080/test/t1

观察日志:

在这里插入图片描述

通过上述程序的运行结果,可以看出:

存在多个切面类时,默认按照切面类的类名字母排序

  • @Before 通知:字母排名靠前的先执行
  • @After 通知:字母排名靠前的后执行

但这种方式不方便管理,我们的类名更多还是具备一定含义的。

所以 Spring 提供了一个新的注解,控制这些切面通知的执行顺序@Order

@Aspect
@Component
@Order(2)
public class AspectDemo2 {
    //...代码省略
}

@Aspect
@Component
@Order(1)
public class AspectDemo3 {
    //...代码省略
}

@Aspect
@Component
@Order(3)
public class AspectDemo4 {
    //...代码省略
}

重新运行程序,访问接口:http://127.0.0.1:8080/test/t1

观察日志:

在这里插入图片描述

@Order 注解标识的切面类,执行顺序如下:

  • @Before 通知:数字越小先执行
  • @After 通知:数字越大先执行

总结: @Order 控制切面的优先级,先执行优先级较高的切面,再执行优先级较低的切面,最终执行目标方法

五、切点表达式

切点表达式常见有两种表达方式

  1. execution():根据方法的签名来匹配
  2. @annotation():根据注解匹配

1. execution表达式

execution() 是最常用的切点表达式,用来匹配方法,语法为:

execution(<访问修饰符> <返回类型> <包名.类名.方法(方法参数)> <异常>)

其中【访问修饰符】和【异常】可以省略!

在这里插入图片描述

切点表达式支持通配符表达:

  1. *匹配任意字符, 只匹配一个元素(返回类型、包、类名、方法、方法参数)
    1. 包名使用 * 表示任意包(一层包使用一个 *
    2. 类名使用 * 表示任意类
    3. 返回值使用 * 表示任意返回值类型
    4. 方法名使用 * 表示任意方法
    5. 参数使用 * 表示一个任意类型的参数
  2. ..匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数
    1. 使用 .. 配置包名,标识此包以及此包下的所有子包
    2. 可以使用 .. 配置参数,任意个任意类型的参数

切点表达式示例:

TestController 下的 public 修饰,返回类型为 String 方法名为 t1 的无参方法:

execution(public String com.example.demo.controller.TestController.t1())

省略访问修饰符:

execution(String com.example.demo.controller.TestController.t1())

匹配所有返回类型:

execution(* com.example.demo.controller.TestController.t1())

匹配 TestController 下的所有无参方法:

execution(* com.example.demo.controller.TestController.*())

匹配 TestController 下的所有方法:

execution(* com.example.demo.controller.TestController.*(..))

匹配 controller 包下所有的类的所有方法:

execution(* com.example.demo.controller.*.*(..))

匹配所有包下面的 TestController

execution(* com..TestController.*(..))

匹配 com.example.demo 包下,子孙包下的所有类的所有方法:

execution(* com.example.demo..*(..))

2. @annotation自定义注解

execution() 更适用有规则的,如果我们要匹配多个无规则的方法呢,比如:TestController 中的 t1()UserController 中的 u1() 两个方法,这个时候使用 execution() 这种切点表达式来描述就不是很方便了.

我们可以借助自定义注解的方式以及另一种切点表达式 @annotation 来描述这一类的切点!

实现步骤:

  1. 自定义注解
  2. 使用 @annotation 表达式来描述切点
  3. 在连接点的方法上添加自定义注解

首先提供测试用例:

@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        return "t1";
    }
    
    @RequestMapping("/t2")
    public boolean t2() {
        return true;
    }
}
@RequestMapping("/user")
@RestController
public class UserController {
    @RequestMapping("/u1")
    public String u1(){
        return "u1";
    }
    
    @RequestMapping("/u2")
    public String u2(){
        return "u2";
    }
}
① 自定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {

}
  1. @Target:标识 Annotation 所修饰的对象范围,即该注解可以用在什么地方。
  1. 常用取值:
  • ElementType.TYPE:用于描述类、接口或 enum 声明
  • ElementType.METHOD:描述方法
  • ElementType.PARAMETER:描述参数
  • ElementType.TYPE_USE:可以标注任意类型
  1. @Retention:标明注解的生命周期
  1. 有三种取值:
  • RetentionPolicy.SOURCE:表示注解仅存在于源代码中,编译成字节码后会被丢弃。这意味着在运行时无法获取到该注解的信息,只能在编译时使用。比如 @SuppressWarnings,以及 lombok 提供的注解 @Data、@Slf4j 等。
  • RetentionPolicy.CLASS:编译时注解。表示注解存在于源代码和字节码中,但在运行时会被丢弃。这意味着在编译时和字节码中可以通过反射获取到该注解的信息,但在实际运行时无法获取。通常用于一些框架和工具的注解。
  • RetentionPolicy.RUNTIME:运行时注解。表示注解存在于源代码,字节码和运行时中。这意味着在编译时,字节码中和实际运行时都可以通过反射获取到该注解的信息。通常用于一些需要在运行时处理的注解,如 Spring 的 @Controller、@ResponseBody 等。
② 切面类

使用 @annotation 切点表达式定义切点,只对自定义注解 @MyAspect 生效:

@Slf4j
@Component
@Aspect
public class MyAspectDemo {
    // 前置通知
    @Before("@annotation(com.example.demo.aspect.MyAspect)")
    public void before(){
        log.info("MyAspect -> before ...");
    }
    
    // 后置通知
    @After("@annotation(com.example.demo.aspect.MyAspect)")
    public void after(){
        log.info("MyAspect -> after ...");
    }
}
③ 添加自定义注解

TestController 中的 t1()UserController 中的 u1() 这两个方法上添加自定义注解 @MyAspect,其他方法不添加:

@MyAspect
@RequestMapping("/t1")
public String t1() {
    return "t1";
}
@MyAspect
@RequestMapping("/u1")
public String u1(){
    return "u1";
}

运行程序,测试接口

http://127.0.0.1:8080/test/t1

观察日志:

在这里插入图片描述

可以看到切面通知被执行了。

继续测试:

http://127.0.0.1:8080/test/t2,切面通知未执行

http://127.0.0.1:8080/user/u1,切面通知执行

Spring AOP的实现方式(常见面试题)

  1. 基于注解@Aspect
  2. 基于自定义注解
  3. 基于Spring API(通过 xml 配置的方式,自从 SpringBoot 广泛使用之后,这种方法几乎看不到了,了解即可)
  4. 基于代理来实现(更加久远的一种实现方式,写法笨重,不建议使用)

参考:https://cloud.tencent.com/developer/article/2032268

Ⅳ. Spring AOP 原理

一、代理模式

代理模式,也叫委托模式。

定义:为其他对象提供一种代理以控制对这个对象的访问。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用

因为在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

在这里插入图片描述

生活中的代理

  • 艺人经纪人:广告商找艺人拍广告,需要经过经纪人,由经纪人来和艺人进行沟通
  • 房屋中介:房屋进行租赁时,卖方会把房屋授权给中介,由中介来代理看房,房屋咨询等服务
  • 经销商:厂商不直接对外销售产品,由经销商负责代理销售
  • 秘书/助理:合作伙伴找老板谈合作,需要先经过秘书/助理预约

代理模式的主要角色

  1. Subject:业务接口类。可以是抽象类或者接口
  2. RealSubject:业务实现类。具体的业务执行,也就是被代理对象
  3. Proxy:代理类。RealSubject的代理

比如房屋租赁:

Subject:就是提前定义了房东做的事情,交给中介代理,也是中介要做的事情

RealSubject:房东

Proxy:中介

UML类图如下:

在这里插入图片描述

代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。

根据代理的创建时期,代理模式分为静态代理和动态代理。

  • 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的 .class 文件就已经存在了。
  • 动态代理:在程序运行时,运用反射机制动态创建而成。

1. 静态代理

静态代理:在程序运行前,代理类的 .class 文件就已经存在了。

即在出租房子之前,中介已经做好了相关的工作,就等租户来租房子了

  1. 定义接口(定义房东要做的事情,也是中介需要做的事情)

       public interface HouseSubject {
           void rentHouse();
       }
    
  2. 实现接口(房东出租房子)

       public class RealHouseSubject implements HouseSubject{
           @Override
           public void rentHouse() {
               System.out.println("我是房东, 我出租房子");
           }
       }
    
  3. 代理(中介,帮房东出租房子)

       public class HouseProxy implements HouseSubject{
           //将被代理对象声明为成员变量
           private HouseSubject houseSubject;
           
           public HouseProxy(HouseSubject houseSubject) {
               this.houseSubject = houseSubject;
           }
           
           @Override
           public void rentHouse() {
               //开始代理
               System.out.println("我是中介, 开始代理");
               //代理房东出租房子
               houseSubject.rentHouse();
               //代理结束
               System.out.println("我是中介, 代理结束");
           }
       }
    
  4. 使用

       public class StaticMain {
           public static void main(String[] args) {
               HouseSubject subject = new RealHouseSubject();
               // 创建代理类
               HouseProxy proxy = new HouseProxy(subject);
               // 通过代理类访问目标方法
               proxy.rentHouse();
           }
       }
    

运行结果:

在这里插入图片描述

上面这个代理实现方式就是静态代理(仿佛啥也没干)

从上述程序可以看出,虽然静态代理也完成了对目标对象的代理,但是由于代码都写死了,对目标对象的每个方法的增强都是手动完成的,非常不灵活,所以日常开发几乎看不到静态代理的场景。

2. 动态代理

相比于静态代理来说,动态代理更加灵活。

我们不需要针对每个目标对象都单独创建一个代理对象,而是把这个创建代理对象的工作推迟到程序运行时由 JVM 来实现。

也就是说动态代理在程序运行时,根据需要动态创建生成

比如房屋中介,我不需要提前预测都有哪些业务,而是业务来了我再根据情况创建。

Java 对动态代理进行了实现,并给我们提供了一些 API,常见的实现方式有两种:

  1. JDK 动态代理
  2. CGLIB 动态代理
① JDK 动态代理

实现步骤:

  1. 定义一个接口及其实现类。(比如静态代理中的 HouseSubject 和 RealHouseSubject)
  2. 自定义 InvocationHandler 并重写 invoke 方法,在 invoke 方法中我们会调用目标方法(被代理类的方法)并自定义一些处理逻辑。
  3. 通过 Proxy.newProxyInstance``(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象

实现 InvocationHandler 接口:

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

public class JDKInvocationHandler implements InvocationHandler {
    // 目标对象即就是被代理对象(也就是房东)
    private Object target;
    
    public JDKInvocationHandler(Object target) {
        this.target = target;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 执行代理的事情
        System.out.println("我是中介, 开始代理");
        
        // 执行房东的事情
        Object retVal = method.invoke(target, args);
        
        // 执行代理的事情
        System.out.println("我是中介, 代理结束");
        return retVal;
    }
}

创建一个代理对象并使用:

public class DynamicMain {
    public static void main(String[] args) {
        HouseSubject target= new RealHouseSubject(); // 创建房东
        
        // 创建一个代理类:通过被代理类、被代理实现的接口、方法调用处理器来创建
        HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(
            target.getClass().getClassLoader(), // 房东的类加载器
            new Class[]{HouseSubject.class},    // 房东实现的一系列接口
            new JDKInvocationHandler(target)    // 上面实现了InvocationHandler的接口,用于执行房东的事情
        );
        proxy.rentHouse();
    }
}
② CGLIB 动态代理

JDK 动态代理有一个最致命的问题是只能代理实现了接口的类

有些场景下,我们的业务代码是直接实现的,并没有接口定义。为了解决这个问题,可以用 CGLIB 动态代理机制来解决。

CGLIB(Code Generation Library)是一个基于 ASM 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理,很多知名的开源框架都使用到了 CGLIB。例如 Spring 中 AOP 模块:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。

实现步骤:

  1. 定义一个类(被代理类,即房东)
  2. 自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于增强目标方法,和 JDK 动态代理中的 invoke 方法类似
  3. 通过 Enhancer 类的 create() 创建代理类

注意:需要添加依赖

和 JDK 动态代理不同,CGLIB 实际是属于一个开源项目,如果你要使用它的话,需要手动添加相关依赖。

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

实现 MethodInterceptor 接口:

import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class CGLIBInterceptor implements MethodInterceptor {
    // 目标对象, 即被代理对象
    private Object target;
    
    public CGLIBInterceptor(Object target){
        this.target = target;
    }
    
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 执行代理的事情
        System.out.println("我是中介, 开始代理");
        
        // 执行房东的事情
        Object retVal = methodProxy.invoke(target, objects);
        
        // 执行代理的事情
        System.out.println("我是中介, 代理结束");
        return retVal;
    }
}

创建代理类并使用:

public class DynamicMain {
    public static void main(String[] args) {
        HouseSubject target= new RealHouseSubject();
        HouseSubject proxy= (HouseSubject) Enhancer.create(target.getClass(),new CGLIBInterceptor(target));
        proxy.rentHouse();
    }
}

二、 Spring AOP源码剖析(了解)

Spring AOP主要基于两种方式实现的:JDK及CGLIB的方式

Spring源码过于复杂,我们只摘出一些主要内容,以了解为主

Spring对于AOP的实现,基本上都是靠 AnnotationAwareAspectJAutoProxyCreator 去完成

生成代理对象的逻辑在父类 AbstractAutoProxyCreator 中

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
                            @Nullable Object[] specificInterceptors, TargetSource targetSource) {
    if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
        AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
    }
    //创建代理工厂
    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.copyFrom(this);
    /**
     * 检查proxyTargetClass属性值,spring默认为false
     * proxyTargetClass 检查接口是否对类代理, 而不是对接口代理
     * 如果代理对象为类, 设置为true, 使用cglib代理
     */
    if (!proxyFactory.isProxyTargetClass()) {
        //是否有设置cglib代理
        if (shouldProxyTargetClass(beanClass, beanName)) {
            //设置proxyTargetClass为true,使用cglib代理
            proxyFactory.setProxyTargetClass(true);
        } else {
            /**
             * 如果beanClass实现了接口,且接口至少有一个自定义方法,则使用JDK代理
             * 否则CGLIB代理(设置ProxyTargetClass为true )
             * 即使我们配置了proxyTargetClass=false, 经过这里的一些判断还是可能会将其设为true 
             */
            evaluateProxyInterfaces(beanClass, proxyFactory);
        }
    }
    Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
    proxyFactory.addAdvisors(advisors);
    proxyFactory.setTargetSource(targetSource);
    customizeProxyFactory(proxyFactory);
    proxyFactory.setFrozen(this.freezeProxy);
    if (advisorsPreFiltered()) {
        proxyFactory.setPreFiltered(true);
    }
    // Use original ClassLoader if bean class not locally loaded in overriding class loader
    ClassLoader classLoader = getProxyClassLoader();
    if (classLoader instanceof SmartClassLoader && classLoader != beanClass.getClassLoader()) {
        classLoader = ((SmartClassLoader) classLoader).getOriginalClassLoader();
    }
    //从代理工厂中获取代理
    return proxyFactory.getProxy(classLoader);
}

代理工厂有一个重要的属性:proxyTargetClass,默认值为false.也可以通过程序设置

proxyTargetClass目标对象代理方式
false实现了接口jdk代理
false未实现接口(只有实现类)cglib代理
true实现了接口cglib代理
true未实现接口(只有实现类)cglib代理

可以通过 @EnableAspectJAutoProxy(proxyTargetClass = true) 来设置

注意:

Spring Boot 2.X开始,默认使用CGLIB代理

可以通过配置项 spring.aop.proxy-target-class=false 来进行修改,设置默认为jdk代理

Spring Boot设置 @EnableAspectJAutoProxy 无效,因为Spring Boot默认使用AopAutoConfiguration进行装配

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        /**
         * HouseProxy houseProxy = context.getBean(HouseProxy.class);
         * 设置spring.aop.proxy-target-class=true cglib代理, 运行成功
         * 设置spring.aop.proxy-target-class=false jdk代理, 运行失败, 不能代理类
         * 因为 HouseProxy 是一个类, 而不是接口, 需要修改为
         * HouseSubject houseProxy = (HouseSubject) context.getBean("realHouseSubject")
         *
         */
        HouseProxy houseProxy = context.getBean(HouseProxy.class);
        //HouseSubject houseProxy = (HouseSubject) context.getBean("realHouseSubject");//正确运行
        System.out.println(houseProxy.getClass().toString());
    }
}

使用context.getBean()需要添加注解,使HouseProxy,RealHouseSubject被Spring管理

测试AOP代理,需要把这些类交给AOP管理(自定义注解或使用@Aspect)

我看点进去看代理工厂的代码

public class ProxyFactory extends ProxyCreatorSupport {
    //...代码省略
    //获取代理
    public Object getProxy(@Nullable ClassLoader classLoader) {
        //分两步 先createAopProxy,后getProxy
        return createAopProxy().getProxy(classLoader);
    }
    
    protected final synchronized AopProxy createAopProxy() {
        if (!this.active) {
            activate();
        }
        return getAopProxyFactory().createAopProxy(this);
    }
    //...代码省略
}

createAopProxy的实现在DefaultAopProxyFactory中

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
    //...代码省略
    @Override
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        /**
         * 根据proxyTargetClass判断
         * 如果目标类是接口, 使用JDK动态代理
         * 否则使用cglib动态代理
         */
        if (!NativeDetector.inNativeImage() &&
            (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass == null) {
                throw new AopConfigException("TargetSource cannot determine target class: " +
                                          "Either an interface or a target is required for proxy creation.");
            }
            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) {
                return new JdkDynamicAopProxy(config);
            }
            return new ObjenesisCglibAopProxy(config);
        }
        else {
            return new JdkDynamicAopProxy(config);
        }
    }
    //...代码省略
}

接下来就是创建代理了

JDK动态代理

final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {
    //...代码省略
    @Override
    public Object getProxy(@Nullable ClassLoader classLoader) {
        if (logger.isTraceEnabled()) {
            logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
        }
        return Proxy.newProxyInstance(determineClassLoader(classLoader), this.proxiedInterfaces, this);
    }
    //...代码省略
}

CGLIB动态代理

class CglibAopProxy implements AopProxy, Serializable {
    //...代码省略
    @Override
    public Object getProxy(@Nullable ClassLoader classLoader) {
        //...代码省略
        // Configure CGLIB Enhancer...
        Enhancer enhancer = createEnhancer();
        // Generate the proxy class and create a proxy instance.
        return createProxyClassAndInstance(enhancer, callbacks);
    }
    //...代码省略
}

在这里插入图片描述

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐