文章目录
- 1. AOP 是什么?为什么要用它?
- 1.1 什么是 AOP?
- 1.2 一个直观的例子
- 2. 核心概念速览
- 3. 快速上手:Spring AOP 实战
- 3.1 依赖引入
- 3.2 定义切面
- 4. 核心原理:动态代理与字节码增强
- 4.1 JDK 动态代理 (基于接口)
- 4.2 CGLIB 动态代理 (基于继承)
- 5. Spring AOP 架构与生命周期流程图
- 架构对比:JDK vs CGLIB
- AOP 代理创建流程图
- 6. AOP 调用流程图(责任链模式)
- 7. 实战应用指南
- 7.1 自定义注解 + AOP (权限校验)
- 7.2 典型应用场景
- 8. 避坑指南与常见问题
- 8.1 同类内部调用失效
- 8.2 private/protected/final 方法
- 8.3 循环依赖
- 9. 总结与延伸
1. AOP 是什么?为什么要用它?
1.1 什么是 AOP?
AOP(Aspect-Oriented Programming)是一种编程范式,旨在通过分离横切关注点来提高模块化程度。
- OOP (面向对象):通过封装、继承将业务逻辑封装成类。
- AOP (面向切面):将业务逻辑中与核心业务无关,但多处重复调用的逻辑(如日志、权限、事务)抽离出来,形成一个“切面”。
1.2 一个直观的例子
想象一个电商系统:
- 业务逻辑:创建订单、取消订单、查询订单。
- 横切逻辑:每一步操作前都要检查权限,每一步操作后都要记录日志。
没有 AOP 时:
publicvoidcreateOrder(){checkPermission();// 重复代码// ... 创建订单逻辑 ...logInfo();// 重复代码}如果 100 个方法都需要权限检查,代码就会变得冗余且难以维护。
有了 AOP 后:
业务代码只关注“创建订单”,权限和日志像“切面”一样织入到业务代码的周围。
2. 核心概念速览
在 Spring AOP 中,你需要掌握以下术语:
| 术语 | 含义 | 类比 |
|---|---|---|
| Join Point (连接点) | 程序执行的某个特定位置(通常是方法执行时)。 | 门上的“门缝” |
| Pointcut (切点) | 匹配连接点的表达式(定义了在哪里切入)。 | 选定好要钻的那些“门缝” |
| Advice (通知) | 在切点处执行的动作(如方法前、后、异常时)。 | 钻进门缝后的动作(如:挂门帘、贴封条) |
| Aspect (切面) | 切点 + 通知的组合。 | 一个拿着工具包准备干活的人 |
| Target (目标对象) | 被代理的对象。 | 原始的门 |
| Proxy (代理) | AOP 框架创建的对象,用来包裹目标对象。 | 门框 |
3. 快速上手:Spring AOP 实战
3.1 依赖引入
确保引入了spring-boot-starter-aop:
<dependency><groupId>org.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>3.2 定义切面
这里我们实现一个简单的“性能监控”切面。
importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.*;importorg.springframework.stereotype.Component;@Aspect// 1. 标记为切面@Component// 2. 交由 Spring 管理publicclassPerformanceAspect{// 3. 定义切点:匹配 com.example.service 包下所有类的所有方法@Pointcut("execution(* com.example.service.*.*(..))")publicvoidserviceLayer(){}// 4. 环绕通知:可以在方法执行前后做处理,甚至控制是否执行@Around("serviceLayer()")publicObjectlogPerformance(ProceedingJoinPointpjp)throwsThrowable{longstart=System.currentTimeMillis();Objectresult=null;try{// 执行目标方法result=pjp.proceed();returnresult;}finally{longend=System.currentTimeMillis();System.out.println(pjp.getSignature()+" took "+(end-start)+" ms");}}// 5. 异常通知@AfterThrowing(pointcut="serviceLayer()",throwing="ex")publicvoidlogError(Exceptionex){System.err.println("Exception occurred: "+ex.getMessage());}}4. 核心原理:动态代理与字节码增强
Spring AOP 的核心机制是代理模式。它不会修改原有类的字节码,而是创建一个代理对象来拦截调用。
Spring AOP 主要使用两种方式创建代理:
4.1 JDK 动态代理 (基于接口)
- 条件:目标类实现了接口。
- 原理:利用反射机制生成一个实现代理接口的匿名类。
- 限制:必须基于接口。
4.2 CGLIB 动态代理 (基于继承)
- 条件:目标类没有实现接口。
- 原理:基于 ASM 框架操作字节码,生成一个目标类的子类,并覆盖其中的方法。
- 限制:因为是继承,所以无法代理 final 修饰的类或方法。
5. Spring AOP 架构与生命周期流程图
AOP 并不是在代码编译时就完成的(除非使用 AspectJ 编译器),而是在 Spring 容器启动时,创建 Bean 的过程中发生的。
架构对比:JDK vs CGLIB
AOP 代理创建流程图
这是 Spring Bean 生命周期中,AOP 发生的关键环节。
6. AOP 调用流程图(责任链模式)
当你在代码中调用被代理的 Bean 方法时,实际执行的流程如下。这是一个典型的拦截器链模式。
7. 实战应用指南
7.1 自定义注解 + AOP (权限校验)
这是开发中最常用的模式:通过自定义注解标记方法,配合 AOP 进行逻辑处理。
1. 定义注解
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceRequirePermission{Stringvalue();}2. 定义切面
@Aspect@ComponentpublicclassSecurityAspect{@Before("@annotation(requirePermission)")// 匹配带注解的方法publicvoidcheckPermission(JoinPointjp,RequirePermissionrequirePermission){StringneededPerm=requirePermission.value();// 获取当前用户权限StringcurrentUserPerm=getCurrentUserPerm();if(!neededPerm.equals(currentUserPerm)){thrownewAccessDeniedException("Permission Denied!");}}}3. 使用
@ServicepublicclassOrderService{@RequirePermission("admin:delete")publicvoiddeleteOrder(Longid){// 业务逻辑}}7.2 典型应用场景
- 日志记录:记录方法入参、出参、执行耗时。
- 声明式事务:
@Transactional是 Spring AOP 最经典的应用。 - 异常处理:统一捕获 DAO 层或 Service 层的异常,转换为业务异常。
- 缓存:在方法执行前查缓存,执行后更新缓存。
- 限流/降级:在方法入口处判断是否超过阈值。
8. 避坑指南与常见问题
8.1 同类内部调用失效
现象:在同一个类中,A 方法调用了 B 方法(B 方法有切面),发现切面不生效。
原因:Spring AOP 基于代理。外部调用this.A()时,this指向的是代理对象,代理对象会处理切面;但如果在 A 方法内部直接调用B(),这时的this是目标对象本身,而非代理对象,因此绕过了 AOP。
解决:
- 注入自身(
@Autowired private SelfService self;)。 - 使用
AopContext.currentProxy()。
8.2 private/protected/final 方法
现象:切面不生效。
原因:
- JDK 动态代理:只能代理接口方法。
- CGLIB:通过继承子类实现,无法重写
private或final方法。
解决:切面方法修饰符改为public或protected(默认为 public),不要使用 final。
8.3 循环依赖
现象:Bean A 依赖 Bean B,Bean B 的切面又依赖 Bean A。
原因:AOP 创建代理对象是一个“半成品”,在初始化阶段可能会因为循环引用导致 Bean 创建失败。
解决:Spring 3.0+ 已经处理了部分场景,但最佳实践是重构代码结构,避免循环依赖,或使用@Lazy注解。
9. 总结与延伸
| 维度 | 内容 |
|---|---|
| 核心思想 | 横切关注点分离,提升模块化 |
| 技术选型 | Spring AOP(简单、集成好) vs AspectJ(功能全、需编译期织入) |
| 适用场景 | 日志、安全、事务、缓存、监控、重试等 |
| 底层机制 | 动态代理(JDK / CGLIB) |
| 学习路径 | 概念 → 切面编写 → 表达式 → 原理 → 高级应用 |
注意:AOP 是利器,但不是银弹。过度使用会导致逻辑隐晦、调试困难。只在真正需要解耦横切逻辑时使用它。