当前位置: 首页 > article >正文

Spring AOP:面向切面编程的探索之旅

目录

1. AOP

2. Spring AOP 快速入门

2.1 引入 Spring AOP 依赖

2.2 Spring AOP 简单使用

3. Spring AOP 核心概念

3.1 切点

3.1.1 @Pointcut 定义切点

3.1.2 切点表达式

 3.1.2.1 execution 表达式

3.1.2.2 @annotation 表达式

3.2 连接点

3.3 通知(Advice)

3.3.1 通知类型

3.4 切面

3.4.1 @Aspect

3.4.2 切面优先级

3.4.2.1 @Order

4. Spring AOP 原理

4.1 代理模式

4.1.1 静态代理

4.1.2 动态代理

4.2 Spring AOP 面试题

面试题1: Spring AOP 底层代码是怎么写的??

面试题2: Spring AOP 是怎么实现的??

面试题3: jdk 代理和 CGLib 代理的区别??

面试题4: Spring 什么情况下使用 jdk, 什么情况下使用 CGLib??

面试题5: Spring AOP 的实现方式??


1. AOP

AOP(Aspect-Oriented Programming), 是一种思想, 面向切面编程.

其中, 切面, 并非是数学中的 "切面", 而是指某一类具体的事情. 而面向切面, 就是指对某一类事情集中处理(而且是不影响原有业务逻辑的集中处理), 关注的是 "横切关注点"

其实, 我们之前用到的 拦截器, 统一结果返回, 统一异常处理, 都是在集中处理某一类事情, 但又不影响原有代码的正常运行. 这些都是 AOP 思想的体现(只是 AOP 思想, 并非 Spring AOP).

因此, AOP 可以理解为: 在不改动核心业务代码的情况下, 偷偷加功能.(一张无形的网,悄悄地把业务代码拦住, 悄悄地加点料, 然后再放行)

AOP是一种思想, 实现它的方式有很多: SpringAOP, AspectJ, CGLIB, ....等

Spring AOP 只是其中的一种实现方式.

Spring 共有两大核心机制, 一个是 Spring IoC, 一个就是 Spring AOP.

在这句话中,第一个 "Spring" 指的是 Spring Framework,而不是 Spring Boot

原因是,Spring IoC(控制反转)和 Spring AOP(面向切面编程)是 Spring Framework 的两大核心特性。这些特性是 Spring Framework 的基础,Spring Boot 则是在 Spring Framework 的基础上进行封装和简化配置,使得开发者能更快速地构建应用。

  • Spring IoC 主要负责对象的管理和依赖注入。

  • Spring AOP 用于面向切面编程,通过代理机制提供横切关注点(如日志、事务管理等)。

Spring Boot 作为 Spring Framework 的扩展,简化了配置和开发流程,但核心机制(如 IoC 和 AOP)依然是基于 Spring Framework 的。所以当提到 "Spring 的核心机制" 时,通常是指 Spring Framework 中的 IoC 和 AOP。

2. Spring AOP 快速入门

2.1 引入 Spring AOP 依赖

Spring AOP 依赖不能在创建项目时引入, 必须手动引入.

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

2.2 Spring AOP 简单使用

我们使用 Spring AOP 编写一个程序, 记录接口的执行时长.

如上图所示, 通过 Spring AOP, 我们只需在外部通过 Spring AOP 就可以获取到接口的执行时长. 达到了 "获取执行时长" 这一功能和业务代码的解耦.

如果不使用 Spring AOP, 我们就需要在每个接口的起始位置和结束位置获取时间戳, 再计算执行时长, 这样不仅入侵了业务代码, 还需要手动对每个要实现这个功能的接口都编写这些代码.

3. Spring AOP 核心概念

Spring AOP 有以下 4 个核心概念:

  1. 切点
  2. 连接点
  3. 通知
  4. 切面

现实例子
假如你要在所有 Controller 方法执行前打印日志,那么:

  • 切点(Pointcut) = "所有 Controller 里的方法"

  • 连接点(JoinPoint)= "Controller 中某个具体的方法"

  • 通知(Advice) = "方法执行前,打印日志"

  • 切面(Aspect) = "拦截所有 Controller 方法,在执行前打印日志"

3.1 切点

切点(Pointcut)本质上只是一个筛选规则不会影响代码执行, 也不会真正“拦截”任何东西, 它只是告诉 Spring 要对哪些方法进行拦截, 对哪些方法生效.

  • 切点 = @Pointcut 注解(声明切点) + 切点表达式(定义规则)

3.1.1 @Pointcut 定义切点

切点表达式是切点的一部分, 它决定了切点的“筛选规则”.

切点通过切点表达式定义一套规则, 这个规则表名了对哪些方法生效/拦截哪些方法(是一个集合), 描述哪些方法可能成为连接点. 

除了上图在 @Around(通知类型注解) 中声明切点表达式外, 还可以通过 @Pointcut 来声明切点, 并在注解中定义切点表达式.

@Pointcut 注解用于标记一个方法, 表明这个方法将作为一个切点, 并且在注解中定义规则(哪些方法被拦截).

被 @Pointcut 注解标记的方法通常是一个空方法(方法体为), 定义这个方法的目的是为了定义一个可复用的切点名称.

通过 @Pointcut 定义切点后, 这个切点不仅能在本切面中使用, 还可以在其他切面中使用(使用时, 需要写出这个切点的全限定名):

3.1.2 切点表达式

常见的切点表达式有以下两种:

  1. execution: 根据方法的签名来匹配 (如上图所示)
  2. @annotation(......): 根据注解匹配
 3.1.2.1 execution 表达式

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

其中, 访问限定修饰符和异常可以省略.

使用方式如下: 

其中, 通配符 (..) 和 (*) 的含义如下:

  • * : 匹配任意字符, 只匹配一个元素(返回类型, 包, 类名, 方法或者方法参数)
符号匹配含义使用场景及说明
 *匹配任意字符,但只匹配一个元素(返回类型、包名、类名、方法名或者方法参数类型)
a. 包名使用 *表示任意一层包(一层包使用一个 *)
b. 类名使用 *表示任意类
c. 返回值使用 *表示任意返回值类型
d. 方法名使用 *表示任意方法
e. 参数使用 *表示一个任意类型的参数
 ..匹配多个连续的任意符号,可以匹配任意层级的包,或者任意类型的参数,任意个数的参数
a. 使用 .. 配置包名标识此包以及此包下的所有子包
b. 可以使用 .. 配置参数任意个任意类型的参数

示例:

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..*(..))
3.1.2.2 @annotation 表达式

通过 execution 定义切点表达式, Spring AOP 拦截的是符合方法签名规则的方法.

而通过 @annotation 定义切点表达式, Spring AOP 拦截的是标注了特定注解的方法(可以是自定义注解, 也可以是 Spring 提供的注解), 因此更加灵活.

@annotation 中, 需要写出该注解的完全限定名.

如下图所示, 自定义注解 @MyAspect, 并使用 @annotation 将切点规则定义为被 @MyAspect 标记的方法, Spring AOP 将会对这些方法进行拦截并执行通知(Advice):

除了使用自定义的注解标记切点, 也可以使用 Spring 提供的注解定义切点规则.

如下图所示, 使用了 @RequestMapping 的方法, 是连接点(满足切点规则的一个具体方法), 当这些方法执行时, 都会被 AOP 所拦截并进行通知:

3.2 连接点

包含在切点表达式中的某个具体的方法, 在程序执行过程中实际被执行的那个方法, 就是一个连接点(即目标方法).

  • 切点(Pointcut)是一个筛选规则,用来定义哪些方法(连接点)会被 AOP 代理。

  • 连接点(Join Point)是具体的方法,符合切点规则的方法就是连接点。

3.3 通知(Advice)

通知(Advice), 就是 AOP 拦截到目标方法(连接点)后, 具体要做的事/具体要执行的逻辑.

简单来说, 通知就是决定拦截后要做什么事情 .

  • 切点(Pointcut) 只是一个“筛选规则”,它决定哪些方法(连接点)需要被拦截,但它本身不执行任何逻辑.

  • 通知(Advice) 是真正 “干活的人”,它决定拦截后要做什么事情(比如打印日志、权限校验、事务管理等)

3.3.1 通知类型

Spring AOP 提供了 5 种常见的通知,不同的通知类型, 执行的时机不同:

通知类型作用例子
@Before - 前置通知在目标方法执行之前运行进入方法前打印日志
@After - 后置通知在目标方法执行之后运行(无论成功或异常)方法结束后记录操作
@AfterReturning - 返回通知方法成功执行后运行记录方法返回值
@AfterThrowing - 异常通知方法抛异常时运行记录错误日志
@Around - 环绕通知方法执行前和执行后运行计算方法执行时间

使用以上注解, 可以在切面类中将方法标注为通知方法.

对以上 5 种通知类型, 我们可以进行单元测试, 观察结果:

如上图所示, 不同通知类型, 按照其规定的时机, 执行了通知中定义的逻辑.

程序正常运行的情况下, @AfterThrowing 标识的通知方法不会执行, 只有抛出异常时, 该通知方法才会执行. 接下来我们制造异常, 再次观察结果:

 综上, 通知类型被分成了两大类:

  1. 目标方法执行前运行(@Around, @Before)
  2. 目标方法执行后运行(@Around, @After, @AfterReturning, @AfterThrowing)

不难得出, @Around 注解具备了其余注解所有的功能, 因此我们使用 @Around 代替其他注解:

3.4 切面

切面(Aspect) = 切点(Pointcut) + 通知(Advice), 一整套规则+执行逻辑的封装.

简单来说,切面就是“规则 + 行为”,它由切点(Pointcut)和通知(Advice)组成,决定在哪些地方(切点)做什么事(通知).

3.4.1 @Aspect

在 Spring 中, 使用 AOP, 需要用到两个注解:

  1. @Aspect: 将类标记为切面类
  2. @Component: 将这个类的 Bean 交给 Spring 管理, 让 Spring 自动管理这个切面, 以便 Spring 调用类中的通知方法

3.4.2 切面优先级

Spring AOP 允许多个切面作用于同一个目标方法.

当多个切面类, 作用于同一个目标方法(连接点)时, 切面之间是有优先级的:

  • 先执行优先级高的切面中的通知, 后执行优先级低的切面中的通知.

默认的切面优先级是按照名称来排序的:

3.4.2.1 @Order

此外, 可以使用 @Order 注解 来显式指定切面的优先级:

  • 数值越小(负数也可以), 优先级越高(越先执行)

注意: 

  • 对于 JDK 代理. Spring AOP 只对 public 修饰的方法生效,即切点匹配的目标方法必须是 public, 切面的通知才会生效.
  • 对于 CGLib 代理, Spring AOP 对非 private 非 final 修饰的方法生效,即切点匹配的目标方法不能是 private 或者 final 的.
  • SpringBoot 默认使用的是 CGLib 代理. 

综上, 如果要对我们项目中的某个方法进行 AOP 拦截通知, 那么这个方法不能是 private 或者 final 修饰的.

4. Spring AOP 原理

Spring AOP 是通过动态代理来创建代理对象, 从而实现 AOP (面向切面编程).

4.1 代理模式

代理模式, 又称为委托模式. 代理模式是常见设计模式的一种.

代理模式, 通过引入一个中间层(代理对象), 使得客户端在访问真实对象(目标方法)时, 不再是直接对目标方法进行调用, 而是通过代理类间接调用. 并且客户端可以通过代理对象进行额外的控制和操作(不影响真实对象的代码).

代理模式中, 有以下三个核心角色:

  1. 抽象主题(Subject): 是一个接口或抽象类, 它定义了真实对象(被代理对象)和代理对象需要共同实现的方法, 确保代理和被代理对象接口统一(使得用户不需要关心当前使用的是代理对象还是真实对象, 因为它们都遵循相同的接口)
  2. 被代理对象(目标/真实对象, Real Subject): 客户端最终想要访问和操作的对象

  3. 代理对象(Proxy): 对真实对象进行代理, 实现了真实对象的接口(使得客户端看来, 代理和真实对象是相同的). 客户端通过它间接访问目标对象.

综上, 代理模式提供了透明性可替换性.

举个例子: 出租房子 

  1. Subject: 将房东要做的事情, 交给中介来处理. 比如: 给用户展示房子.
  2. Real Subject: 房东
  3. Proxy: 房屋中介

代理模式分为以下两种:

  1. 静态代理: 程序执行前, 代理类的源代码就已经编写完成, 并且在程序编译阶段就已经生成了对应的 .class 文件.
  2. 动态代理: 代理类是在程序运行期间, 由 JVM 根据需要动态地创建出来的, 源代码不需要显式编写.

4.1.1 静态代理

静态代理是指: 在程序执行前, 代理类的源代码就已经编写完成, 并且在程序编译阶段就已经生成了对应的 .class 文件.

举个例子:

一个明星, 他有一个固定的经纪人, 每次有商业活动, 这个经纪人就会帮他事先处理这些事务.

(这个经纪人是这个明星的专属经纪人, 只帮这个明星办事) => 一对一

但是, 人毕竟不是万能的, 这个经纪人也有不擅长的地方, 当经纪人的能力不满足这个明星的需求时, 就有麻烦了.

这就是静态代理.

4.1.2 动态代理

动态代理是指: 代理类是在程序运行期间, 由 JVM 根据需要动态地创建出来的, 源代码通常并不需要显式编写.

还是以明星-经纪人为例.

后来, 这个明星发展的越来越好, 接触的资源越来越多, 因此这个明星签了一个公司, 每次有活动时, 公司都会根据活动的需求, 临时的为这个明星分配合适的经纪人, 这个经纪人就会帮明星处理本次活动相关事务.

这就是动态代理.

Spring AOP 就是通过动态代理来实现的, 其实现方式有以下两种:

  1. JDK 动态代理 (JDK Dynamic Proxy)
  2. CGLIB 代理 (Code Generation Library)

4.2 Spring AOP 面试题

面试题1: Spring AOP 底层代码是怎么写的??

  1. 从 Spring 容器中找到 @Aspect 注解标记的切面类的 Bean (找到程序中的所有切面) 注: @Aspect 是由 Aspect 声明的, 但是具体实现是由 Spring 完成的
  2. 再从这些 Bean 中找 @Around/@Before/... 标记的通知方法, 生成 Advisor(通知类) 的实例, 放到 List 中
  3. 创建代理对象, 并在调用代理对象方法时, 根据通知类型, 在特定的时机执行通知方法中定义的逻辑

面试题2: Spring AOP 是怎么实现的??

Spring AOP 实现动态代理有两种方式:

  1. 通过 jdk 实现动态代理
  2. 通过 CGLib 实现动态代理

面试题3: jdk 代理和 CGLib 代理的区别??

  1. jdk 只能代理接口(目标对象只能是接口)
  2. CGLIb 既可以代理类, 也可以代理接口(目标对象可以是类, 也可以是接口)
  3. "网上有一些文章, 说使用 CGLIb 性能要高于 jdk, 但是这一点我还没有进行验证"

面试题4: Spring 什么情况下使用 jdk, 什么情况下使用 CGLib??

对于 Spring, 其使用哪种进行动态代理, 与目标对象以及代理工厂中的 proxyTargetClass 属性有关.

Spring 是通过工厂模式来创建代理的, 并且根据的代理工厂中的 proxyTargetClass  属性值进行创建:

proxyTargetClass目标对象代理方式
false实现了接口jdk代理
false未实现接口(只是单独一个类)cglib代理
true实现了接口cglib代理
true未实现接口(只是单独一个类)cglib代理

由于 SpringBoot 是基于 Spring Framework 进行封装的, 因此大多数情况下, SpringBoot 和Spring Framework是保持一致的.

但是 Spring AOP 这里是个例外:

  1. SpringFramework 的 proxytargetclass 默认值为 false
  2. Springboot 2.x 默认值为false
  3. Springboot 3.x 默认值为true

因此, 我们现在的 SpringBoot 项目, 默认使用 CGLib 动态代理实现 AOP.

面试题5: Spring AOP 的实现方式??

面试官:谈谈你对IOC和AOP的理解及AOP四种实现方式[通俗易懂]-腾讯云开发者社区-腾讯云


END


http://www.kler.cn/a/612436.html

相关文章:

  • PTA 1105-链表合并(C++)
  • SpringMVC实战——转发和重定向及实际场景
  • Dify实现自然语言生成SQL并执行
  • 高级数据结构01BST树
  • 如何使用VS中的Android Game Development Extension (AGDE) 来查看安卓 Logcat 日志
  • ‌JVM 内存模型(JDK8+)
  • 如何使用Python爬虫按关键字搜索1688商品?
  • 测谎仪策略思路
  • linux 安装open webui
  • 第二十章:类型属性的重载_《C++ Templates》notes
  • 【商城实战(80)】从0到1剖析:区块链如何重塑商城生态
  • Lisp语言的数据库交互
  • WPF 依赖项属性
  • 使用django的DRF业务逻辑应该放在序列化器类还是模型类
  • 前端空白/红幕报错 undefined
  • JavaScript性能优化实战手册:从V8引擎到React的毫秒级性能革命
  • <track>标签在<video>或<audio>元素中的作用,如何利用它实现字幕功能?
  • 将pytroch模型转为paddlelite模型并集成到android程序中
  • [学成在线]06-视频分片上传
  • C# 打印模板设计-ACTIVEX打印控件-多模板加载