SpringBoot之旅5| 快速上手SpringAOP、深入刨析动态/静态两种代理模式

目录

[1. AOP概述](#1. AOP概述)

[2. Spring AOP快速入门](#2. Spring AOP快速入门)

[2.1 引入AOP依赖](#2.1 引入AOP依赖)

[2.2 编写AOP程序](#2.2 编写AOP程序)

[3. Spring AOP 详解](#3. Spring AOP 详解)

[3.1 Spring AOP核心概念](#3.1 Spring AOP核心概念)

[3.1.1 切点(Pointcut)](#3.1.1 切点(Pointcut))

[3.1.2 连接点(Join Point)](#3.1.2 连接点(Join Point))

[3.1.3 通知(Advice)](#3.1.3 通知(Advice))

[3.1.4 切面(Aspect)](#3.1.4 切面(Aspect))

[3.2 通知类型](#3.2 通知类型)

[3.3 @PointCut](#3.3 @PointCut)

[3.4 切面优先级](#3.4 切面优先级)

[3.5 切点表达式](#3.5 切点表达式)

[3.5.1 execution表达式](#3.5.1 execution表达式)

[3.5.2 @annotation](#3.5.2 @annotation)

[3.5.2.1 自定义注解](#3.5.2.1 自定义注解)

[3.5.2.2 切面类](#3.5.2.2 切面类)

[3.5.2.3 添加自定义注解](#3.5.2.3 添加自定义注解)

[4. Spring AOP 原理](#4. Spring AOP 原理)

[4.1 代理模式](#4.1 代理模式)

[4.2 静态代理](#4.2 静态代理)

[4.3 动态代理](#4.3 动态代理)

[4.3.1 JDK动态代理](#4.3.1 JDK动态代理)

[4.3.2 CGLIB 动态代理类实现步骤](#4.3.2 CGLIB 动态代理类实现步骤)


1. AOP概述

学习完Spring的统一功能之后, 我们进入到AOP的学习. AOP是Spring框架的第二大核心(第一大核心是 IoC)

什么是AOP?

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

什么是面向切面编程呢? 切面就是指某一类特定问题, 所以AOP也可以理解为面向特定方法编程. 什么是面向特定方法编程呢? 比如上个章节学习的"登录校验", 就是一类特定问题. 登录校验拦截器, 就是对"登录校验"这类问题的统一处理. 所以, 拦截器也是AOP的一种应用. AOP是一种思想, 拦截器是AOP 思想的一种实现. Spring框架实现了这种思想, 提供了拦截器技术的相关接口. 同样的, 统一数据返回格式和统一异常处理, 也是AOP思想的一种实现

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

什么是Spring AOP? AOP是一种思想, 它的实现方法有很多, 有Spring AOP,也有AspectJ、CGLIB等. Spring AOP是其中的一种实现方式

学会了统一功能之后, 是不是就学会了Spring AOP呢, 当然不是. 拦截器作用的维度是URL (一次请求和响应), @ControllerAdvice 应用场景主要是全局异常处理 (配合自定义异常效果更佳), 数据绑定, 数据预处理. AOP作用的维度更加细致(可以根据包、类、方法 名、参数等进行拦截), 能够实现更加复杂的业务逻辑

举个例子: 我们现在有一个项目,项目中开发了很多的业务功能

我们想要记录这些业务功能(这里指的是某个方法)执行的耗时的话,可以使用如下方法

java 复制代码
public static void main(String[] args) {
        long time1 = System.currentTimeMillis();
        function();
        long time2 = System.currentTimeMillis();
        System.out.println("function方法耗时:"+(time2-time1));
}

但这样修改代码的话,尤其是项目很大的情况下,很费时费力

那么,AOP就可以做到在不改动这些原始方法的基础上, 针对特定的方法进行功能的增强.

AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦) 接下来我们来看Spring AOP如何来实现

2. Spring AOP快速入门

学习什么是AOP后, 我们先通过下面的程序体验下AOP的开发, 并掌握Spring中AOP的开发步骤.

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

2.1 引入AOP依赖

在pom.xml文件中添加配置

XML 复制代码
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.2 编写AOP程序

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

java 复制代码
@Slf4j
@Aspect
@Component
public class TimeAspect {
    //实现的功能,记录controller下的方法的耗时时间
    @Around("execution(* com.mxy.book.controller.*.*(..))")//.*.*表示对于controller路径下的所有的类,所有的方法生效
    public Object timeRecord(ProceedingJoinPoint pjp) throws Throwable {
        //1.记录开始时间
        long start = System.currentTimeMillis();
        //2.执行目标方法
        Object proceed = pjp.proceed();
        long end = System.currentTimeMillis();
        log.info(pjp.getSignature().toString()+"接口耗时"+(end-start)+"ms");
        return proceed;
    }
}

对程序进行简单的讲解:

  1. @Aspect: 标识这是一个切面类

  2. @Around: 环绕通知, 在目标方法的前后都会被执行. 后面的表达式表示对哪些方法进行增强.

  3. ProceedingJoinPoint.proceed() 让原始方法执行

我们通过AOP入门程序完成了业务接口执行耗时的统计. 通过上面的程序, 我们也可以感受到AOP面向切面编程的一些优势:

• 代码无侵入: 不修改原始的业务方法, 就可以对原始的业务方法进行了功能的增强或者是功能的改变

• 减少了重复代码

• 提高开发效率

• 维护方便

3. Spring AOP 详解

下面我们再来详细学习AOP, 主要是以下几部分

• Spring AOP中涉及的核心概念

• Spring AOP通知类型

• 多个AOP程序的执行顺序

3.1 Spring AOP核心概念

3.1.1 切点(Pointcut)

切点(Pointcut), 也称之为"切入点"

Pointcut 的作用就是提供一组规则 (使用 AspectJ pointcut expression language 来描述), 告诉程序对哪些方法来进行功能增强.

上面的表达式 execution(* com.example.demo.controller.*.*(..)) 就是切点表达式

3.1.2 连接点(Join Point)

满足切点表达式规则的方法, 就是连接点. 也就是可以被AOP控制的方法

以入门程序举例, 所有 com.mxy.book.controller 路径下的方法, 都是连接点.

java 复制代码
package com.example.demo.controller;
@RequestMapping("/book")
@RestController
public class BookController {
 @RequestMapping("/addBook")
 public Result addBook(BookInfo bookInfo) {
 //...代码省略
 }
 @RequestMapping("/queryBookById")
 public BookInfo queryBookById(Integer bookId){
 //...代码省略
 }
 @RequestMapping("/updateBook")
 public Result updateBook(BookInfo bookInfo) {
 //...代码省略
 }
}

上述BookController 中的方法都是连接点

切点和连接点的关系:

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

比如:

切点表达式: 所有大学生

连接点就是: 张三,李四等学生

3.1.3 通知(Advice)

通知就是具体要做的工作, 指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

比如上述程序中记录业务方法的耗时时间, 就是通知

在AOP面向切面编程当中, 我们把这部分重复的代码逻辑抽取出来单独定义, 这部分代码就是通知的内容

3.1.4 切面(Aspect)

切面(Aspect) = 切点(Pointcut) + 通知(Advice)

切面所在的类, 我们一般称为切面类(被@Aspect注解标识的类)

注意:

ProceedingJoinPoint pjp = 代表你要执行的目标方法

pjp.proceed() = 让目标方法真正运行,并拿到返回值

3.2 通知类型

上面我们讲了什么是通知, 接下来学习通知的类型. @Around 就是其中一种通知类型, 表示环绕通知. Spring中AOP的通知类型有以下几种:

• @Around: 环绕通知, 此注解标注的通知方法在目标方法前, 后都被执行

• @Before: 前置通知, 此注解标注的通知方法在目标方法前被执行

• @After: 后置通知, 此注解标注的通知方法在目标方法后被执行, 无论是否有异常都会执行

• @AfterReturning: 返回后通知, 此注解标注的通知方法在目标方法后被执行, 有异常不会执行

• @AfterThrowing: 异常后通知, 此注解标注的通知方法发生异常后执行

java 复制代码
package com.mxy.aop.aspect;

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 AspectDemo0 {

    @Around("execution(* com.mxy.aop.controller.*.*(..))")
    public Object timeRecord(ProceedingJoinPoint pjp){
        log.info("目标方法执行前");
        Object proceed = null;
        try {
            proceed = pjp.proceed();
        } catch (Throwable e) {
            log.error("do Around Throwing");
        }
        log.info("目标方法执行后");
        return proceed;
    }

    @Before("execution(* com.mxy.aop.controller.*.*(..))")
    public void doBefore(){
        log.info("doBefore");
    }

    @After("execution(* com.mxy.aop.controller.*.*(..))")
    public void doAfter(){
        log.info("doAfter");
    }

    @AfterReturning("execution(* com.mxy.aop.controller.*.*(..))")
    public void doAfterReturning(){
        log.info("doAfterReturning");
    }

    @AfterThrowing("execution(* com.mxy.aop.controller.*.*(..))")
    public void doAfterThrowing(){
        log.info("doAfterThrowing");
    }
}

TestController.java类

java 复制代码
package com.mxy.aop.controller;


import com.mxy.aop.aspect.MyAspect;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public Integer t1(){
        log.info("执行t1");

//        int a = 10/0;
        return 1;
    }
    @MyAspect
    @RequestMapping("/t2")
    public Boolean t2(){
        log.info("执行t2");
        int a = 10/0;
        return true;
    }
    @RequestMapping("/t3")
    public String t3(){
        log.info("执行t3");

        return "t3";
    }

}

我们来先测试正常运行的情况

利用postman客户端请求t1方法

打开服务器日志

程序正常运行的情况下, @AfterThrowing 标识的通知方法不会执行从上图也可以看出来, @Around 标识的通知方法包含两部分, 一个"前置逻辑", 一个"后置逻辑".其中"前置逻辑" 会先于 @Before 标识的通知方法执行, "后置逻辑" 会晚于 @After 标识的通知方法执行

我们来接下来测试异常时的情况

利用postman客户端请求t1方法

程序发生异常的情况下:

• @AfterReturning 标识的通知方法不会执行, @AfterThrowing 标识的通知方法执行了

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

注意事项:

• @Around 环绕通知需要调用 ProceedingJoinPoint.proceed() 来让原始方法执行, 其他 通知不需要考虑目标方法执行.

• @Around 环绕通知方法的返回值, 必须指定为Object, 来接收原始方法的返回值, 否则原始方法执 行完毕, 是获取不到返回值的

一个切面类可以有多个切点

3.3 @PointCut

上面代码存在一个问题, 就是存在大量重复的切点表达式

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

Spring提供了 @PointCut 注解, 把公共的切点表达式提取出来, 需要用到时引用该切入点表达式即可. 上述代码就可以修改为:

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo0 {
    @Pointcut("execution(* com.mxy.aop.controller.*.*(..))")
    private void pt(){}

    @Around("pt()")
    public Object timeRecord(ProceedingJoinPoint pjp){
        log.info("目标方法执行前");
        Object proceed = null;
        try {
            proceed = pjp.proceed();
        } catch (Throwable e) {
            log.error("do Around Throwing");
        }
        log.info("目标方法执行后");
        return proceed;
    }

    @Before("pt()")
    public void doBefore(){
        log.info("doBefore");
    }

    @After("pt()")
    public void doAfter(){
        log.info("doAfter");
    }

    @AfterReturning("pt()")
    public void doAfterReturning(){
        log.info("doAfterReturning");
    }

    @AfterThrowing("pt()")
    public void doAfterThrowing(){
        log.info("doAfterThrowing");
    }
}

注意:

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

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo2 {
 //前置通知
 @Before("com.example.mxy.aspect.AspectDemo0.pt()")
 public void doBefore() {
 log.info("执行 AspectDemo2 -> Before 方法");
 }
}

3.4 切面优先级

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

我们还是通过程序来求证:

定义多个切面类:

为简单化, 只写了 @Before 和 @After 两个通知

java 复制代码
package com.mxy.aop.aspect;

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 AspectDemo1 {

    //定义切点
    @Pointcut("execution(* com.mxy.aop.controller.*.*(..))")
    public void pt(){};

    //实现的功能,记录controller下的方法的耗时时间
    @Around("pt()")//.*.*表示对于controller路径下的所有的类,所有的方法生效
    public Object timeRecord(ProceedingJoinPoint pjp){
        log.info("目标方法执行前");
        //2.执行目标方法
        Object result = null;
        try {
            result = pjp.proceed();
        } catch (Throwable e) {
            log.error("AspectDemo1 do Around throwing...");
        }
        log.info("目标方法执行后");

        return result;
    }

    @Before("pt()")
    public void doBefore(){
        log.info("AspectDemo1 doBefore");
    }

    @After("pt()")
    public void doAfter(){
        log.info("AspectDemo1 doAfter");
    }

    @AfterReturning("pt()")
    public void doAfterReturning(){
        log.info("AspectDemo1 doAfterReturning");
    }

    @AfterThrowing("pt()")
    public void doAfterThrowing(){
        log.info("AspectDemo1 doAfterThrowing");
    }
}
java 复制代码
package com.mxy.aop.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class AspectDemo2 {

    @Before("execution(* com.mxy.aop.controller.*.*(..))")
    public void doBefore(){
        log.info("AspectDemo2 doBefore");
    }

    @After("execution(* com.mxy.aop.controller.*.*(..))")
    public void doAfter(){
        log.info("AspectDemo2 doAfter");
    }

}
java 复制代码
package com.mxy.aop.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class AspectDemo3 {

    @Before("execution(* com.mxy.aop.controller.*.*(..))")
    public void doBefore(){
        log.info("AspectDemo3 doBefore");
    }

    @After("execution(* com.mxy.aop.controller.*.*(..))")
    public void doAfter(){
        log.info("AspectDemo3 doAfter");
    }

}
java 复制代码
package com.mxy.aop.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class AspectDemo4 {

    @Before("execution(* com.mxy.aop.controller.*.*(..))")
    public void doBefore(){
        log.info("AspectDemo4 doBefore");
    }

    @After("execution(* com.mxy.aop.controller.*.*(..))")
    public void doAfter(){
        log.info("AspectDemo4 doAfter");
    }

}

通过上述程序的运行结果, 可以看出: 存在多个切面类时, 默认按照切面类的类名字母排序

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

• @After 通知:字母排名靠前的后执行 但这种方式不方便管理, 我们的类名更多还是具备一定含义的.

Spring 给我们提供了一个新的注解, 来控制这些切面通知的执行顺序: @Order 使用方式如下:

添加@Order(),其中,里面的数字越小优先级越高

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo1 {
//......
}
@Slf4j
@Aspect
@Order(3)
@Component
public class AspectDemo2 {
//......
}
@Slf4j
@Aspect
@Order(5)
@Component
public class AspectDemo3 {
//......
}
@Slf4j
@Aspect
@Order(1)//值越小优先级越高
@Component
public class AspectDemo4 {
//......
}

通过上述程序的运行结果, 得出结论:

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

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

• @After 通知:数字越大先执行 @Order 控制切面的优先级, 先执行优先级较高的切面, 再执行优先级较低的切面, 最终执行目标方法

其中优先级demo4>demo2>demo3>demo1

3.5 切点表达式

上面的代码中, 我们一直在使用切点表达式来描述切点.

下面我们来介绍一下切点表达式的语法. 切点表达式常见有两种表达方式

  1. execution(......):根据方法的签名来匹配

  2. @annotation(......) :根据注解匹配

3.5.1 execution表达式

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

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

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

  1. * :匹配任意字符,只匹配一个元素(比如:返回类型, 包, 类名, 方法或者方法参数之一)

a. 包名使用 * 表示任意包(一层包使用一个*)

b. 类名使用 * 表示任意类

c. 返回值使用 * 表示任意返回值类型

d. 方法名使用 * 表示任意方法

e. 参数使用 * 表示一个任意类型的参数

  1. .. :匹配多个连续的任意符号, 可以通配任意层级的包, 或任意类型, 任意个数的参数

a. 使用 .. 配置包名,标识此包以及此包下的所有子包

b. 可以使用 .. 配置参数,任意个任意类型的参数

切点表达式示例

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

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

省略访问修饰符public

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..*(..))

3.5.2 @annotation

execution表达式更适用有规则的, 如果我们要匹配多个无规则的方法呢, 比如:TestController中的t1() 和UserController中的u1()这两个方法.

这个时候我们使用execution这种切点表达式来描述就不是很方便了. 我们可以借助自定义注解的方式以及另一种切点表达式 @annotation 来描述这一类的切点

实现步骤:

  1. 编写自定义注解

  2. 使用 @annotation 表达式来描述切点

  3. 在连接点的方法上添加自定义注解

我们先准备两个测试类TestController以及UserController

TestController.java

java 复制代码
import com.mxy.aop.aspect.MyAspect;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public Integer t1(){
        log.info("执行t1");

//        int a = 10/0;
        return 1;
    }
    @MyAspect
    @RequestMapping("/t2")
    public Boolean t2(){
        log.info("执行t2");
        return true;
    }

}

UserController.java

java 复制代码
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
    @RequestMapping("/u1")
    public String u1(){
        log.info("执行u1");
        return "u1";
    }
    @RequestMapping("/u2")
    public String u2(){
        log.info("执行u2");
        return "u2";
    }

}
3.5.2.1 自定义注解

@MyAspect 创建一个注解类(和创建Class文件一样的流程, 选择Annotation就可以了)

java 复制代码
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 所修饰的对象范围, 即该注解可以用在什么地方.

常用取值:

ElementType.TYPE: 用于描述类、接口(包括注解类型) 或enum声明

ElementType.METHOD: 描述方法

ElementType.PARAMETER: 描述参数

ElementType.TYPE_USE: 可以标注任意类型

  1. @Retention 指Annotation被保留的时间长短, 标明注解的生命周期

@Retention 的取值有三种:

  1. RetentionPolicy.SOURCE:表示注解仅存在于源代码中, 编译成字节码后会被丢弃 . 这意味着在运行时无法获取到该注解的信息, 只能在编译时使用. 比如 @SuppressWarnings , 以及 lombok提供的注解 @Data , @Slf4j

  2. RetentionPolicy.CLASS:编译时注解. 表示注解存在于源代码和字节码中, 但在运行时会被丢弃. 这意味着在编译时和字节码中可以通过反射获取到该注解的信息, 但在实际运行时无法获取. 通常用于一些框架和工具的注解.

  3. RetentionPolicy.RUNTIME:运行时注解. 表示注解存在于源代码, 字节码和运行时中. 这意味着在编译时, 字节码中和实际运行时都可以通过反射获取到该注解的信息. 通常用于一些需要在运行时处理的注解, 如Spring的 @Controller @ResponseBody

3.5.2.2 切面类

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

切面类代码如下

java 复制代码
@Slf4j
@Aspect
@Component
//扫描标记了 @MyAspect 的方法,给它们加逻辑
public class MyAspectDemo {
    @Around("@annotation(com.mxy.aop.aspect.MyAspect)")//找到MyAspect标记的方法
    public Object timeRecord(ProceedingJoinPoint pjp){
        log.info("目标方法执行前");
        //2.执行目标方法
        Object result = null;
        try {
            result = pjp.proceed();
        } catch (Throwable e) {
            log.error("do Around throwing...");
        }
        log.info("目标方法执行后");

        return result;
    }
}
3.5.2.3 添加自定义注解

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

java 复制代码
@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {
    @MyAspect
    @RequestMapping("/t1")
    public Integer t1(){
        log.info("执行t1");

//        int a = 10/0;
        return 1;
    }
    
    @RequestMapping("/t2")
    public Boolean t2(){
        log.info("执行t2");
        return true;
    }

}
java 复制代码
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
    @MyAspect
    @RequestMapping("/u1")
    public String u1(){
        log.info("执行u1");
        return "u1";
    }

    @RequestMapping("/u2")
    public String u2(){
        log.info("执行u2");
        return "u2";
    }

}

首先测试test/t1

然后测试user/u1

我们接着测试user/u2,发现切面方法中的通知没有执行

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

  1. 基于注解 @Aspect (参考上述课件内容)

  2. 基于自定义注解 (参考自定义注解 @annotation 部分的内容)

  3. 基于Spring API (通过xml配置的方式, 自从SpringBoot 广泛使用之后, 这种方法几乎看不到了)

  4. 基于代理来实现(更加久远的一种实现方式, 写法笨重, 不建议使用)

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

4. Spring AOP 原理

上面我们主要学习了Spring AOP的应用, 接下来我们来学习Spring AOP的原理, 也就是Spring是如何实现AOP的. Spring AOP 是基于动态代理来实现AOP

4.1 代理模式

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

定义:为其他对象提供一种代理以控制对这个对象的访问. 它的作用就是通过提供一个代理类 , 让我们在调用目标方法的时候, 不再是直接对目标方法进行调用, 而是通过代理类间接调用. 在某些情况下, 一个对象不适合或者不能直接引用另一个对象, 而代理对象可以在客户端和目标对象之 间起到中介的作用.

使用代理前vs使用代理之后:

生活中的代理

• 艺人经纪人: 广告商找艺人拍广告, 需要经过经纪人,由经纪人来和艺人进行沟通.

• 房屋中介: 房屋进行租赁时, 卖方会把房屋授权给中介, 由中介来代理看房, 房屋咨询等服务.

• 经销商: 厂商不直接对外销售产品, 由经销商负责代理销售.

• 秘书/助理: 合作伙伴找老板谈合作, 需要先经过秘书/助理预约.

代理模式的主要角色

  1. Subject: 业务接口类. 可以是抽象类或者接口(不一定有)

  2. RealSubject: 业务实现类. 具体的业务执行, 也就是被代理对象.

  3. Proxy: 代理类. RealSubject的代理.

比如房屋租赁

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

RealSubject: 房东

Proxy: 中介

从 RealSubject 指向 Proxy 的实线箭头,代表:

Proxy 持有 RealSubject 的引用(组合 / 关联关系)

这是代理的核心逻辑:中介手里必须有房东的联系方式 ,才能在租客要租房时,把请求转发给真实的房东。

代码层面就是:Proxy 类里会有一个 private RealSubject realSubject; 成员变量。

4.2 静态代理

静态代理: 在程序运行前, 已经存在相应的代理类 (在出租房子之前, 中介已经做好了相关的工作, 就等租户来租房子了)

我们通过代码来加深理解. 以房租租赁为例

  1. 定义接口(定义房东要做的事情, 也是中介需要做的事情)
java 复制代码
public interface HouseSubject {
    void rentHouse();
}
  1. 实现接口(房东出租房子)
java 复制代码
public class RealHouseSubject implements HouseSubject{
    @Override
    public void rentHouse() {
        System.out.println("我是房东,我要出租房子");
    }
}
  1. 代理(中介, 帮房东出租房子)
java 复制代码
public class HouseProxy implements HouseSubject{
    RealHouseSubject realHouseSubject = new RealHouseSubject();
    @Override
    public void rentHouse() {
        System.out.println("我是中介,开始代理");
        realHouseSubject.rentHouse();
        System.out.println("我是中介,结束代理");
    }
}
  1. 调用方,调用代理,代理内部调用目标对象
java 复制代码
public class Main {
    public static void main(String[] args) {
        HouseProxy houseProxy = new HouseProxy();
        houseProxy.rentHouse();
    }
}

运行结果为:

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

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

接下来新增需求: 中介又新增了其他业务: 代理房屋出售我们需要对上述代码进行修改

  1. 接口定义修改
java 复制代码
public interface HouseSubject {
    void rentHouse();

    void saleHouse();
}
  1. 接口实现修改
java 复制代码
public class RealHouseSubject implements HouseSubject{
    @Override
    public void rentHouse() {
        System.out.println("我是房东,我要出租房子");
    }

    @Override
    public void saleHouse() {
        System.out.println("我是房东,我要出售房子");
    }
}
  1. 代理类修改
java 复制代码
public class HouseProxy implements HouseSubject{
    RealHouseSubject realHouseSubject = new RealHouseSubject();
    @Override
    public void rentHouse() {
        System.out.println("我是中介,开始代理");
        realHouseSubject.rentHouse();
        System.out.println("我是中介,结束代理");
    }

    @Override
    public void saleHouse() {
        System.out.println("我是中介,开始代理");
        realHouseSubject.saleHouse();
        System.out.println("我是中介,结束代理");
    }
}

从上述代码可以看出, 我们修改接口(Subject)和业务实现类(RealSubject)时, 还需要修改代理类 (Proxy).

同样的, 如果有新增接口(Subject)和业务实现类(RealSubject), 也需要对每一个业务实现类新增代理类 (Proxy). 既然代理的流程是一样的, 有没有一种办法, 让他们通过一个代理类来实现呢? 这就需要用到动态代理技术


4.3 动态代理

相比于静态代理来说,动态代理更加灵活. 我们不需要针对每个目标对象都单独创建一个代理对象, 而是把这个创建代理对象的工作推迟到程序运行时由JVM来实现. 也就是说动态代理在程序运行时, 根据需要动态创建生成.

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

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

1.JDK动态代理

  1. CGLIB动态代理

(动态代理在我们日常开发中使用的相对较少,但是在框架中几乎是必用的一门技术. 学会了动态代理 之后, 对于我们理解和学习各种框架的原理也非常有帮助)

4.3.1 JDK动态代理

JDK 动态代理类实现步骤

  1. 定义一个接口及其实现类(静态代理中的 HouseSubject 和 RealHouseSubject )

  2. 自定义 InvocationHandler 并重写 invoke 方法,在 invoke 方法中我们会调用目标方法(被代理类的方法)并自定义一些处理逻辑

  3. 通过 Proxy.newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h) 方法创建代理对象

定义JDK动态代理类

  1. InvocationHandler

InvocationHandler 接口是Java动态代理的关键接口之一, 它定义了一个单一方法 invoke() , 用于 处理被代理对象的方法调用

java 复制代码
package com.mxy.aop.proxy1;

import java.lang.reflect.Method;

public interface InvocationHandler {
    /**
     * 参数说明
     * proxy:代理对象
     * method:代理对象需要实现的方法,即其中需要重写的方法
     * args:method所对应方法的参数
     */
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable;
}

通过实现 InvocationHandler 接口, 可以对被代理对象的方法进行功能增强(该接口内的invoke方法会被实现)

**作用:**只要调用代理对象的任何方法,都会跑到这里的 invoke ()!

  1. Proxy

Proxy 类中使用频率最高的方法是: newProxyInstance() , 这个方法主要用来生成一个代理 对象

这是中介逻辑,需要实现 InvocationHandler 接口

java 复制代码
package com.mxy.aop.proxy1;

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

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

这里 3 个参数超级重要:

  1. proxy:代理对象自己(不用管)

  2. method:当前调用的方法(rentHouse?saleHouse?)

  3. args:方法参数

其中

java 复制代码
Object retVal = method.invoke(target, args);

即让 房东(target) 去执行 方法(method),并传入参数(args)

创建一个代理对象并使用

java 复制代码
package com.mxy.aop.proxy1;

import com.mxy.aop.proxy1.JDKInvocationHandler;
import com.mxy.aop.proxy1.RealHouseSubject;

// 正确 JDK 动态代理包(必须是这两个!)
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class Main {

    public static void main(String[] args) {
        // 1. 创建真实对象(房东)
        HouseSubject target = new RealHouseSubject();

        // 2. JDK 动态代理创建代理对象
        HouseSubject houseProxy = (HouseSubject) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),  // 更好的写法
                new JDKInvocationHandler(target)           // 不用强转!
        );

        // 3. 调用方法
        houseProxy.rentHouse();
        System.out.println("------------");
        houseProxy.saleHouse();
    }
}
java 复制代码
HouseSubject houseProxy = (HouseSubject) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),  // 更好的写法
                new JDKInvocation(target)           // 不用强转!
        );

其中这个方法一共有 3 个参数:

Loader: 类加载器, 用于加载代理对象.

interfaces : 被代理类实现的一些接口(这个参数的定义, 也决定了JDK动态代理只能代理实现了接口的 一些类)

h : 实现了 InvocationHandler 接口的对象

运行结果为:

注意:

  1. 动态代理vs静态代理
静态代理 JDK 动态代理
代理类数量 一个目标一个代理 一个 handler 通用
实现方法 必须手动实现每个方法 自动统一处理
新增接口 要新建代理类 不用改代码
代码量 多、重复 极少
灵活性

静态代理是手动给每个类写中介;动态代理是自动生成中介,一套逻辑能给所有类做增强,代码更少、扩展性更强、更灵活。

  1. 动态代理到底方便在哪?(重点)

一个 InvocationHandler 能代理所有类

不管是:

  • HouseSubject

  • CarSubject

  • UserService

  • OrderService

全都能用同一个 JDKInvocationHandler!

不用再写 N 个代理类。

不用手动实现接口里的每一个方法

静态代理要一个个实现方法:rentHouse、saleHouse、checkHouse......

动态代理:所有方法自动进入 invoke (),统一处理!

java 复制代码
// 一个方法处理所有接口的所有方法
public Object invoke(...) {
    System.out.println("前置");
    method.invoke(target, args);
    System.out.println("后置");
}

接口新增方法,静态代理要改代码,动态代理不用

接口加方法→ 静态代理必须去代理类里实现→ 动态代理完全不用动,自动增强

可以在运行时才决定代理谁,更灵活

静态代理是写死的:

复制代码
HouseProxy proxy = new HouseProxy();

动态代理可以:

复制代码
Proxy.newProxyInstance(任意目标对象);
  1. 代码整体的执行流程
角色 对应代码 作用
租客 Main 方法 发起租房 / 卖房请求(调用 houseProxy.rentHouse()
代理对象(前台中介) houseProxy 接管所有请求,对外透明
InvocationHandler(后台处理中心) JDKInvocationHandler 统一处理所有请求,做增强、转发给房东
房东(真实对象) RealHouseSubject 真正执行租房 / 卖房业务
  1. 步骤 1:客户调用 Main 类
  • 对应代码:main方法入口

  • 逻辑:客户(调用方)从 Main 方法发起整个流程

  1. 步骤 2:创建代理对象(前台代理)
  • 对应代码:Proxy.newProxyInstance(...)

  • 逻辑:

    • 传入目标对象target(房东)、JDKInvocationHandler(后台代理)

    • JDK 在内存中动态生成$Proxy0类,实例化为houseProxy(前台代理)

    • 角色定义:houseProxy是前台代理,负责接管客户的方法调用

    • 我们图中标注的$Proxy.rentHouse()/$Proxy.saleHouse(),就是 JDK 自动生成的代理类方法

  1. 步骤 3:后台代理(InvocationHandler)
  • 对应代码:JDKInvocationHandler

  • 逻辑:

    • 前台代理houseProxy所有方法调用,都会转发到invoke()方法

    • 它是整个代理的核心逻辑中枢,负责前置增强、反射调用真实方法、后置增强

    • 角色定义:InvocationHandler后台代理,统一处理所有代理逻辑。

  1. 步骤 4:后台代理调用房东类
  • 对应代码:method.invoke(target, args)

  • 逻辑:

    • 通过反射调用真实目标对象targetRealHouseSubject,房东)的方法

    • 真正的租房 / 卖房业务由房东执行,代理只做增强和转发

    • 链路:invoke()method.invoke() → 房东方法。

其中JDK 自动生成的代理类 $Proxy0 示意如下:

java 复制代码
// JDK 自动生成的代理类 $Proxy0(内存中,硬盘无文件)
public final class $Proxy0 extends Proxy implements HouseSubject {
    // 你传入的 InvocationHandler(中介逻辑)
    private final InvocationHandler h;

    // 构造方法,注入 handler
    public $Proxy0(InvocationHandler h) {
        this.h = h;
    }

    // 重写 rentHouse() 方法
    @Override
    public void rentHouse() {
        try {
            // 核心:直接调用 handler 的 invoke() 方法!
            h.invoke(this, HouseSubject.class.getMethod("rentHouse"), null);
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }

    // 重写 saleHouse() 方法
    @Override
    public void saleHouse() {
        try {
            // 同样,直接调用 invoke()
            h.invoke(this, HouseSubject.class.getMethod("saleHouse"), null);
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }
}
角色 职责 对应图中元素
目标类(RealHouseSubject) 真正执行业务逻辑的真实对象(房东) HouseSubject target = new RealHouseSubject()
代理对象(houseProxy / $Proxy0) JDK 自动生成,接管所有方法调用,对外透明 Proxy.newProxyInstance 生成的对象
InvocationHandler(JDKInvocationHandler) 统一处理所有代理方法,执行增强逻辑,反射调用目标方法 右侧的 invoke() 方法
接口(HouseSubject) 统一规范,保证代理对象与真实对象行为一致 中间的接口定义

4.3.2 CGLIB 动态代理类实现步骤

  1. 定义一个类(被代理类)

  2. 自定义 MethodInterceptor 并重写 intercept 方法, intercept 用于增强目标方法,和 JDK 动态代理中的 invoke 方法类似

  3. 通过 Enhancer 类的 create()创建代理类

接下来看下实现:

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

XML 复制代码
<dependency>
 <groupId>cglib</groupId>
 <artifactId>cglib</artifactId>
 <version>3.3.0</version>
</dependency>

自定义 MethodInterceptor(方法拦截器)

实现MethodInterceptor接口

java 复制代码
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;
 }
}

MethodInterceptor 和 JDK动态代理中的 JDKInvocationHandler 类似, 它只定义了一个方法 intercept() , 用于增强目标方法.

java 复制代码
public class Main {
    public static void main(String[] args) {
        HouseSubject target = new RealHouseSubject();

        //Cglib动态代理
        HouseSubject houseSubject = (HouseSubject) Enhancer.create(target.getClass(), new CGlibMethodInterceptor(target));
        houseSubject.rentHouse();
    }
}

参数说明:

type: 被代理类的类型(类或接口)

callback: 自定义方法拦截器 MethodInterceptor

相关推荐
lolo大魔王2 小时前
Go语言的基础语法
开发语言·后端·golang
千桐科技2 小时前
数据仓库 vs 数据中台:从“数据库的豪华升级版”到“企业的数据操作系统”
数据库·数据仓库·数据治理·数据中台·数据资产·数据服务·qdata
FuckPatience2 小时前
Halcon 寻找方形Mark
前端·javascript·数据库
小陈工2 小时前
Python Web开发入门(八):用户认证系统实现,给你的应用加上安全锁
开发语言·前端·数据库·python·安全·django·sqlite
jwt7939279372 小时前
基于SpringBoot和Leaflet的行政区划地图掩膜效果实战
java·spring boot·后端
難釋懷2 小时前
Redis缓存预热
redis·spring·缓存
Miki Makimura2 小时前
SQL 核心对象学习
数据库·sql·学习
亚马逊云开发者2 小时前
IDEA 里装个 AI 助手:Amazon Q Developer for JetBrains 实测体验
java·ide·intellij-idea
qqacj2 小时前
SpringBoot【十一】mybatis-plus实现多数据源配置,开箱即用!
spring boot·后端·mybatis