软考高级系统架构师之Spring AOP篇
📅 2026/7/8 3:37:35
👁️ 阅读次数
📝 编程学习
⚡ Spring AOP 核心导读
Spring AOP(Aspect-Oriented Programming)—— Spring 框架的核心模块之一,通过动态代理在运行时将横切关注点(日志、事务、权限等)无侵入地织入业务方法,是 OOP 的有力补充。
- 🧩分离关注点:业务逻辑与系统服务解耦
- 🚀运行时织入:基于 JDK/CGLIB 动态代理,无需修改源码
- 🎯方法级拦截:支持 5 种通知类型,精准控制执行时机
- 🔧注解驱动:
@Aspect+@Pointcut极简配置,与 Spring Boot 无缝集成
💡 一句话:Spring AOP = 动态代理 + 切点表达式 + 通知,让你在不入侵业务代码的前提下,统一处理日志、事务、权限等“横切”逻辑。
系统架构师学习平台(点击这里进入)
📚 一、AOP 核心术语(必须掌握)
| 术语 | 英文 | 通俗解释 | 类比 |
|---|---|---|---|
| 切面 | Aspect | 横切关注点的模块化单元 = 切入点 + 通知 | 如“日志模块”、“事务模块” |
| 连接点 | Join Point | 程序执行中所有可能被拦截的点(Spring 中仅指方法执行) | 所有路口 |
| 切入点 | Pointcut | 通过表达式精准匹配出来的实际拦截点 | 设有红绿灯的路口 |
| 通知 | Advice | 在特定切入点执行的增强代码,定义何时 + 做什么 | 红绿灯规则(停/行) |
| 目标对象 | Target | 被增强的原始业务对象 | 被管理的车辆 |
| 代理对象 | Proxy | AOP 框架动态创建的对象,包含增强逻辑 | 交警(指挥车辆) |
| 织入 | Weaving | 将切面应用到目标对象并生成代理的过程 | 安装交通系统的动作 |
| 引入 | Introduction | 动态为现有类添加新方法/属性 | 给车辆加装新设备 |
🧭 二、五种通知类型(Advice)—— 拦截时机的艺术
| 通知类型 | 注解 | 执行时机 | 典型用途 |
|---|---|---|---|
| 前置通知 | @Before | 目标方法执行之前 | 参数校验、操作日志、权限预检 |
| 后置通知 | @After | 目标方法执行之后(无论是否异常) | 释放资源、关闭连接 |
| 返回通知 | @AfterReturning | 目标方法正常返回后(可获取返回值) | 记录响应数据、缓存更新 |
| 异常通知 | @AfterThrowing | 目标方法抛出异常后 | 统一异常捕获、错误告警 |
| 环绕通知 | @Around | 包围目标方法,可控制是否执行及参数返回值 | 性能监控、事务管理、重试机制 |
⚠️ 原则:能用弱类型通知就别用环绕通知(
@Around最强大但最容易出错,需手动proceed())。
🎯 三、切点表达式(Pointcut)—— 精准定位的艺术
Spring AOP 采用AspectJ 切点表达式,最常用的是execution。
基本语法
execution(修饰符? 返回类型 类全限定名.方法名(参数) 异常?)常见示例
| 表达式示例 | 含义 |
|---|---|
execution(* com.example.service.*.*(..)) | 匹配 service 包下所有类的所有方法 |
execution(public * com.example.service.UserService.*(..)) | 匹配 UserService 的 public 方法 |
execution(* com.example.service..*.*(..)) | 匹配 service 包及子包所有方法 |
execution(* find*(..)) | 匹配所有以find开头的方法 |
execution(* com.example.service.UserService.findUser(Long, ..)) | 匹配第一个参数为 Long 的 findUser 方法 |
常用切点指示符(PCD)
execution:匹配方法执行(最常用)within:匹配指定包/类中的所有方法@annotation:匹配带有特定注解的方法bean:匹配特定 Spring Bean 名称
⚙️ 四、实现原理 —— JDK 动态代理 vs CGLIB
Spring AOP 根据目标类是否实现接口,自动选择代理机制:
| 代理方式 | 适用场景 | 实现机制 | 特点 |
|---|---|---|---|
| JDK 动态代理 | 目标类实现了接口 | java.lang.reflect.Proxy+InvocationHandler | 基于接口,轻量,JDK 原生 |
| CGLIB 代理 | 目标类未实现接口 | 通过字节码技术生成子类(继承) | 不需要接口,但不能代理final方法 |
🚨 注意:Spring AOP只支持方法级连接点(无法拦截字段访问/构造器执行),这是由动态代理的天然限制决定的。
🧩 五、配置方式(注解驱动为主流)
1. 引入依赖(Spring Boot)
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>2. 启用 AOP
- Spring Boot:自动开启(
@EnableAspectJAutoProxy默认生效) - 传统 Spring:在配置类上添加
@EnableAspectJAutoProxy
3. 编写切面(示例代码)
@Component@Aspect@Slf4jpublicclassLoggingAspect{// 1. 定义切入点(复用)@Pointcut("execution(* com.example.service.*.*(..))")publicvoidserviceLayer(){}// 2. 前置通知@Before("serviceLayer()")publicvoidlogBefore(JoinPointjp){log.info("调用方法:{},参数:{}",jp.getSignature().getName(),jp.getArgs());}// 3. 环绕通知(性能监控)@Around("serviceLayer()")publicObjectmeasureTime(ProceedingJoinPointpjp)throwsThrowable{longstart=System.currentTimeMillis();Objectresult=pjp.proceed();// 执行目标方法longcost=System.currentTimeMillis()-start;log.info("方法 {} 耗时 {} ms",pjp.getSignature().getName(),cost);returnresult;}}📌 也可使用 XML 配置(现不常用),注解方式已主导。
🧪 六、实际落地场景(真实项目必备)
| 场景 | 通知类型建议 | 说明 |
|---|---|---|
| 操作日志记录 | @Before+@AfterReturning | 记录请求参数、响应结果、操作人 |
| 声明式事务管理 | @Around | Spring@Transactional底层基于 AOP |
| 权限校验(RBAC) | @Before | 方法执行前检查当前用户是否有权访问 |
| 接口耗时监控 | @Around | 统计慢接口,上报监控系统 |
| 统一异常处理 | @AfterThrowing | 捕获业务异常,封装成统一返回格式 |
| 缓存管理(@Cacheable) | @Around | 方法执行前查缓存,执行后更新缓存 |
| 数据脱敏/加密 | @AfterReturning | 对返回的敏感字段(手机号、身份证)脱敏 |
| 重试机制 | @Around | 方法失败后自动重试(如调用外部 API) |
🔍 七、Spring AOP 与 AspectJ 对比
| 维度 | Spring AOP | AspectJ |
|---|---|---|
| 织入时机 | 运行时(动态代理) | 编译时 / 类加载时(字节码增强) |
| 连接点范围 | 仅方法执行 | 方法、字段、构造器、静态初始化等 |
| 性能 | 有代理开销(每次方法调用都走代理) | 无运行时开销(直接执行增强后的字节码) |
| 易用性 | 简单,与 Spring 无缝集成 | 需要独立编译器/编织器,较复杂 |
| 适用场景 | 大多数企业级应用(Web/微服务) | 对性能极致要求或需要细粒度拦截(如框架开发) |
💡 实际生产中90% 场景使用 Spring AOP 就足够,除非你需要拦截字段访问或追求极致性能。
⚠️ 八、常见坑点 & 注意事项
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 同一个类中方法调用不生效 | 内部调用走this而非代理对象 | ① 注入自身 Bean 调用;② 使用AopContext.currentProxy() |
| 静态方法无法拦截 | 代理无法覆盖静态方法 | 改用 AspectJ 或手动调用 |
final方法无法被 CGLIB 代理 | CGLIB 通过继承,final方法不可覆写 | 避免在代理类中使用final方法 |
| 循环依赖问题 | AOP 代理可能引发循环依赖 | 使用@Lazy或设计上解耦 |
| 切面执行顺序混乱 | 多个切面优先级未指定 | 使用@Order注解或实现Ordered接口 |
📌 九、速记汇总·一图流
- 🧩 核心组成:切面 = 切入点表达式 + 通知(5 种时机)
- 🎯 切入点:
execution语法精准定位方法 - 🔄 代理底层:有接口 → JDK,无接口 → CGLIB
- 📢 通知类型:
@Before@After@AfterReturning@AfterThrowing@Around - 🛠️ 常见场景:日志、事务、权限、监控、缓存、重试
- 🚨 注意坑点:内部调用不走代理,静态/final 无法拦截,多个切面需
@Order
🔥总结:Spring AOP 通过运行时动态代理,把日志、事务等横切逻辑从业务代码中抽离,极大提升代码的复用性和可维护性。它与 Spring 生态天然融合,是每个后端开发者必须熟练掌握的利器。牢记:切点定位 + 通知织入 = 优雅解耦。
适用场景:Web 层请求日志、Service 层事务管理、DAO 层异常转换、任意需要统一增强的业务模块。
编程学习
技术分享
实战经验