15- [AOP] 面向切面编程

15- [AOP] 面向切面编程
Prorise3. [AOP] 面向切面编程
摘要: 在我们的项目中,日志记录、事务管理、权限校验等通用逻辑散布在各个业务方法中,造成了代码冗余。本章我们将学习 Spring AOP,这是一种强大的编程范式,它能将这些横切关注点从主业务逻辑中优雅地分离出来。我们将通过实战,创建一个自定义的日志切面,并最终实现一个基于注解的声明式操作权限校验。
前置知识要求: 在深入学习本章之前,我们强烈建议您回顾或先行学习我们的 《Java微服务(二):3.0 SpringMVC - 前后端交互核心内核 章节,因为本章中的许多概念,如拦截器 (Interceptor),都与 AOP 的思想一脉相承。理解 Spring MVC 的请求处理流程将极大地帮助您 grasp(掌握)AOP 的核心精髓。
为了不影响读者的阅读和,这里提供一个整理好的仓库供读者快速Clone Spring_Mvc_Study: 教学用的SpringMVC文件 我们后续还会在这个项目的基础上加以改进
3.1. AOP 核心概念入门
3.1.1. 痛点:什么是横切关注点?(Why AOP)
让我们回顾一下之前的代码。在 UserServiceImpl
中,我们可能希望在每个公开的业务方法(如 findUserById
, saveUser
)执行前后都打印日志,用于追踪和调试。一个朴素的实现方式可能是这样的:
1 |
|
这种写法暴露了严重的问题:
核心逻辑混杂:缓存处理的代码(从缓存读、写入缓存)与真正的业务逻辑(查询数据库、对象转换)紧紧地耦合在一起,使得代码难以阅读和维护。
代码重复:如果未来 findProductById
、findOrderById
等方法也需要缓存,我们就必须在每个方法里都重复编写这套缓存逻辑。
像日志、事务、权限校验、性能监控这类需要“横向”地应用到多个业务模块中的功能,我们就称之为 横切关注点。
AOP (Aspect-Oriented Programming, 面向切面编程) 的核心目标,就是将这些横切关注点从主业务逻辑中优雅地剥离出来,使它们模块化,从而降低代码的耦合度,提升系统的可维护性和可扩展性。
3.1.2. 核心术语:构建 AOP 的“积木”
要理解 AOP 是如何工作的,我们必须先掌握它的几个核心术语。您可以将它们想象成一套用于构建“切面”的乐高积木。
核心概念 | 作用/比喻 | 简明解释 |
---|---|---|
连接点 (Join Point) | 所有可能的时机 | 程序执行过程中可以插入切面的点,如方法调用或执行。 |
切点 (Pointcut) | 选定的具体时机 | 一个或多个连接点的集合,它精确定义了通知将在哪里执行。 |
通知 (Advice) | 要做的具体事情 | 在切点匹配的连接点上执行的代码,例如记录日志。 |
切面 (Aspect) | 事情和时机的组合 | 切点和通知的结合体,封装了一个完整的横切关注点功能。 |
关系总结
- 切面 (Aspect) = 切点 (Pointcut) + 通知 (Advice)
- 通知 (Advice) 被应用到由 切点 (Pointcut) 筛选出的一系列 连接点 (Join Point) 上。
1. 连接点 (Join Point)
定义: 程序执行过程中的一个明确的点,例如方法的调用、异常的抛出等。在 Spring AOP 中,连接点总是指代方法的执行。
您可以把它想象成程序流程中的一个个“可以插入逻辑”的“时机点”。
2. 切点 (Pointcut)
定义: 一个谓词或表达式,它用于匹配和选中一组感兴趣的连接点。
如果说连接点是程序中所有可能插入逻辑的点,那么切点就是我们的“筛选器”,它精确地定义了我们到底要在哪些方法上应用我们的横切逻辑。例如,我们可以定义一个切点来选中 UserController
中所有以 get
开头的方法。
3. 通知 (Advice)
定义: 在切点所匹配的连接点上具体要执行的操作。
通知定义了我们的横切逻辑“做什么”以及“什么时候做”。Spring AOP 提供了五种标准的通知类型:
@Before
: 在目标方法执行之前执行。@AfterReturning
: 在目标方法成功返回之后执行。@AfterThrowing
: 在目标方法抛出异常之后执行。@After
: 无论目标方法是成功返回还是抛出异常,在它之后都会执行(类似于finally
块)。@Around
: 环绕通知。这是最强大的通知类型,它能完全包裹目标方法的执行,我们可以在方法执行前后自定义逻辑,甚至可以决定是否执行目标方法。
4. 切面 (Aspect)
定义: 通知 (Advice) 和 切点 (Pointcut) 的一个模块化组合。
一个切面将“在哪里做(切点)”和“做什么(通知)”这两件事有机地结合在了一起,形成了一个完整的横切关注点模块。例如,我们可以创建一个“日志切面”,它包含一个匹配所有 Service 层方法的切点,以及一个在方法执行前后打印日志的环绕通知。
3.2. [实战] 创建声明式缓存切面
现在,我们将亲手实践 AOP 的真正威力:创建一个自定义的 @SimpleCache
注解和一个配套的缓存切面。最终实现的效果是,任何方法只要加上 @SimpleCache
注解,就自动具备了专业的、带过期时间的缓存能力。
3.2.1. 引入 AOP Starter 依赖
首先,请确保 demo-framework
模块的 pom.xml
中已添加 spring-boot-starter-aop
依赖。
文件路径: demo-framework/pom.xml
1 | <dependency> |
3.2.2. 创建自定义缓存注解
我们在 demo-common
模块中创建 @SimpleCache
注解。
文件路径: demo-common/src/main/java/com/example/democommon/annotation/SimpleCache.java
(新增文件)
1 | package com.example.democommon.annotation; |
3.2.3. 技术选型:Hutool-Cache 简介
在实现切面之前,我们先来了解一下即将使用的强大工具——Hutool-cache
。它提供了几种成熟的缓存策略,让我们可以轻松应对不同场景。
缓存策略 | 核心思想 | 淘汰机制 | 容量限制 |
---|---|---|---|
FIFOCache | 先进先出 (First In, First Out) | 缓存满时,淘汰最先存入的数据 | 有 |
LFUCache | 最少使用 (Least Frequently Used) | 缓存满时,淘汰使用频率最低的数据 | 有 |
LRUCache | 最近最久未使用 (Least Recently Used) | 缓存满时,淘汰最长时间未被访问的数据 | 有 |
TimedCache | 定时过期 (Time-based Expiration) | 数据达到设定的超时时间后自动过期 | 无 |
补充说明:
FIFO
、LFU
、LRU
这三种策略都是容量驱动的缓存。它们的核心目标是在缓存达到容量上限时,决定应该淘汰哪些数据来为新数据腾出空间。TimedCache
是时间驱动的缓存。它不关心容量是否已满,只关心数据是否“新鲜”,一旦数据过期就会被清理。这与我们@SimpleCache
注解中的timeoutSeconds
属性完美契合,因此是本次实战的最佳选择。
3.2.4. 实现缓存切面
现在,我们来创建 SimpleCacheAspect
,并使用 Hutool 的 TimedCache
来实现专业的缓存逻辑。
文件路径: demo-framework/src/main/java/com/example/demoframework/aspect/SimpleCacheAspect.java
(新增文件)
1 | package com.example.demoframework.aspect; |
3.3. 应用与测试
3.3.1. 在 Service 中应用注解
现在,我们回到 UserServiceImpl
,移除之前手写的缓存代码,并换上我们崭新的 @SimpleCache
注解。
文件路径: demo-system/src/main/java/com/example/demosystem/service/impl/UserServiceImpl.java
(修改)
1 | package com.example.demosystem.service.impl; |
3.3.2. 回归测试:验证缓存与过期效果
重启您的 demo-admin
模块。
- 重复调用请求: 使用 Swagger UI 调用
GET /users/1
。观察控制台日志,您会看到:
您会发现,这次没有打印“正在执行 findUserById 核心业务逻辑…”,证明原方法根本没有被执行,数据直接从缓存返回。
在十秒后重新请求,可以看到耗时又变回去了,证明缓存条目已经因过期而被自动清除,程序重新执行了数据库查询。
3.3.3. AOP 与拦截器的对比总结
现在,我们可以清晰地总结 AOP 和拦截器的区别了,这对于选择正确的技术至关重要。
对比维度 | 拦截器 (Interceptor) | Spring AOP (Aspect) |
---|---|---|
作用层面 | Web 层,与 HttpServletRequest 强绑定 | Spring Bean 的方法执行层面,与 Web 无关 |
粒度 | 粗粒度,拦截所有匹配的 HTTP 请求 | 细粒度,可精确到具体类的具体方法 |
能力 | 可获取和修改 HTTP 请求和响应对象 | 可获取和修改方法参数、返回值;可决定是否执行原方法 |
典型场景 | 用户认证、全局日志、CORS、解决重复提交 | 事务管理、缓存、权限校验、性能监控等业务横切点 |
3.4. [实战] 使用 execution
监控 Service 层性能
在之前的缓存案例中,我们使用了 @annotation
来精确地“定点”增强某一个方法。现在,我们将学习如何使用 execution
来进行“范围”增强,实现一个对整个 Service 层所有公共方法进行性能监控的切面。
我们的目标:自动记录 demo-system
模块下,service
包及其子包内所有 public
方法的执行耗时,而无需修改任何 Service 代码。
3.4.1. 创建性能监控切面
文件路径: demo-framework/src/main/java/com/example/demoframework/aspect/PerformanceAspect.java
(新增文件)
1 | package com.example.demoframework.aspect; |
3.4.2 回归测试
您无需做任何额外配置!因为这个切面已经被注册为 Spring Bean,并且它的切点会自动匹配所有符合条件的 Service 方法。
- 重启
demo-admin
应用。 - 使用 API 工具调用任意一个会触发 Service 层方法的接口,例如
GET /users
或POST /users
。 - 观察控制台日志,您会看到除了我们之前的 Web 日志,现在还多出了性能监控日志:
结论:execution
指示符为我们提供了一种极其强大的、基于包和方法签名进行“范围扫描”的能力。它与 @annotation
的“定点精确打击”形成了完美互补,两者结合,可以让我们随心所欲地将 AOP 的能力应用到项目的任何一个角落。
解惑:@Around
是否能替代所有其他通知?
从技术上讲,@Around
环绕通知确实是功能最强大的,它可以完全模拟 @Before
, @AfterReturning
, @AfterThrowing
和 @After
的所有功能。
既然如此,为什么还需要其他通知注解呢?
答案是:为了代码的简洁性、可读性和意图的清晰性。
我们可以把这看作是“瑞士军刀”和“专用工具”的区别:
@Around
(瑞士军刀): 功能万能,但使用起来也最复杂。您必须手动管理目标方法的执行(通过调用pjp.proceed()
),并且需要自己处理异常。如果忘记调用pjp.proceed()
,原始方法将永远不会被执行,这可能是一个难以发现的严重 bug。@Before
,@AfterReturning
等 (专用工具): 功能专一,但使用起来非常简单安全。它们清晰地表达了您的意图,并且您无需关心如何以及何时执行原始方法,Spring 框架会为您处理好一切。
最佳实践建议:
当您只想… | 最佳选择 | 为什么? |
---|---|---|
在方法执行前做些事 | @Before | 最简单,意图最明确,不会意外影响方法执行。 |
在方法成功返回后做些事 | @AfterReturning | 可直接获取返回值,代码简洁。 |
只在方法抛出异常时做些事 | @AfterThrowing | 专门的异常处理通道,逻辑清晰。 |
必须在方法执行前后都操作,或需要控制/改变方法执行流程时 | @Around | 只有在这种复杂场景下,才动用功能最强大的工具。 |