一、什么是AOP
AOP 即 Aspect Oriented Programming 的缩写,即是面向切面编程。
什么叫做面向切面编程呢,切面就是指某一类特定问题,所以AOP也可以理解为面向特定方法编程。AOP可以做到在不改动原始方法的基础上,针对特定的方法进行功能的增强。可以在程序运行期间在不修改代码的基础上对已有方法进行增强。
总的来说,AOP是一种思想,是对某一类事情的集中处理
二、AOP解决了什么样的问题
AOP(面向切面编程)主要解决的是横切关注点带来的代码分散和纠缠问题。
简单说,在传统面向对象编程(OOP)中,有些逻辑(如日志,事务,权限)不属于某个具体的业务模块,却散落在很多模块里,导致代码难以维护。AOP就是把这些逻辑从业务代码中抽离出来,统一处理。
在OOP里,我们按功能划分模块。但有些功能会"横切"多个模块,比如:
- 日志记录:每个重要方法可能都要加日志
- 事务管理:多个业务方法需要开启/提交/回滚事务
- 权限校验:很多接口调用前都要检查权限
- 性能监控:需要统计某些方法的执行事件
- 异常处理:统一处理特定异常
这些逻辑如果直接写在业务方法里,会导致:
- 代码重复:同样的日志/事务代码到处复制
- 业务逻辑被污染:业务方法里混杂大量非业务代码,可读性差
- 维护困难:改一个日志格式,要改几十个地方
- 职责不单一:一个方法既处理业务,有管理日志和事务
AOP的解决方式
AOP通过切面把这些横切逻辑模块化,然后通过织入(Weaving)在编译期、类加载期或运行期,动态地把切面代码"插入"到目标方法地合适位置(如方法执行前、后、异常时)。
这样,业务代码只需要关心业务,日志、事务等由切面统一处理。比如:
java
// 业务代码只写业务
public void createOrder(Order order) {
orderRepository.save(order);
}
java
// 切面统一处理日志和事务
@Aspect
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint jp) {
System.out.println("调用方法: " + jp.getSignature());
}
}
总的来说:
AOP的主要价值是:
- 减少重复代码:横切逻辑只写一次
- 提高模块化:业务与非业务逻辑分离
- 易于维护:修改横切逻辑只需修改切面
- 增强可读性:业务方法更纯净
典型应用场景:
- 日志记录
- 事务管理(如Spring的@Transactional)
- 权限/安全校验
- 缓存
- 性能监控
- 统一异常处理
- 数据校验
所以,AOP解决的核心问题是:将横切关注点从业务逻辑中解耦出来,集中管理,从而减低耦合、减少重复、提高可维护性。
三、如何使用 Spring AOP
AOP 是面向切面编程的一种思想,而 Spring AOP 就是AOP思想的一种实现。
那么如何使用Spring AOP 呢
概括地说:
Spring AOP 的用法 = 定义切面(切点 + 通知),交给 Spring 管理,运行时自动织入。
核心概念对照:
| 概念 | 注解/关键字 | 作用 |
|---|---|---|
| 切面 | @Aspect |
横切逻辑的模块 |
| 切点 | @Pointcut |
决定在哪些方法上织入 |
| 通知 | @Before / @After / @Around 等 |
决定织入后做什么 |
| 连接点 | JoinPoint |
被拦截的方法,可获取方法名、参数 |
| 织入 | Weaving | 把切面应用到目标方法的过程 |
具体地说:
1、加依赖
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Spring Boot 会开启AOP代理,无需手动加 @EnableAspectJAutoProxy。
2、写业务代码
如:
java
@Service
public class OrderService {
public void createOrder(Long id) {
System.out.println("创建订单:" + id);
}
}
3、定义切面
java
@Aspect // 声明这是切面
@Component // 交给 Spring 管理
public class LogAspect {
// 切点:匹配 com.example.service 包下所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
// 前置通知:方法执行前
@Before("serviceMethods()")
public void before(JoinPoint jp) {
System.out.println("[前置] 调用:" + jp.getSignature().getName());
}
// 后置返回:方法正常返回后
@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void afterReturning(Object result) {
System.out.println("[返回] 结果:" + result);
}
// 最终通知:无论成功失败都执行
@After("serviceMethods()")
public void after() {
System.out.println("[最终] 方法结束");
}
// 环绕通知:功能最强,可控制执行、改参数、改返回值
@Around("serviceMethods()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed(); // 执行目标方法
} finally {
long cost = System.currentTimeMillis() - start;
System.out.println("[环绕] 耗时:" + cost + "ms");
}
}
}
第4步:调用业务方法,切面自动生效
java
orderService.createOrder(1L);
输出为:
前置 调用:createOrder
创建订单:1
环绕 耗时:1ms
返回 结果:null
最终 方法结束
五种通知的对比
| 通知 | 注解 | 时机 | 能否控制执行 |
|---|---|---|---|
| 前置 | @Before |
方法前 | ❌ |
| 后置返回 | @AfterReturning |
正常返回后 | 可读返回值 |
| 后置异常 | @AfterThrowing |
抛异常后 | 可读异常 |
| 最终 | @After |
方法结束后(总执行) | ❌ |
| 环绕 | @Around |
包裹整个方法 | ✅ |
三个必须要知道的坑
| 坑 | 原因 | 解决 |
|---|---|---|
| 同类内部方法调用失效 | Spring AOP 基于代理,外部调用先经过代理对象,代理才能织入通知;而类内部 this.methodB() 走的是原始对象,不经过代理,所以切面失效 |
① 注入自身 @Autowired private XxxService self;此时注入的是代理对象。 然后 self.methodB();② ((XxxService) AopContext.currentProxy()).methodB()(需开启 exposeProxy=true);③ 把方法拆到另一个 Bean |
| private / static / final 方法不能被切 | private 方法在子类/代理中不可见,无法被覆盖;static 方法属于类而非对象,代理拦不到;final 方法不能被子类重写(CGLIB 靠生成子类)。因此动态代理机制无法在这些方法上织入 | 改为 public、非 static、非 final 方法;若必须增强,考虑用 AspectJ 的编译期/类加载期织入 |
代理方式由 proxyTargetClass 变量决定,不同环境默认值不同 |
Spring Framework 默认 proxyTargetClass = false:目标类有接口走 JDK 动态代理,无接口走 CGLIB;Spring Boot 2.x 的 AopAutoConfiguration 把默认值改为 true:强制走 CGLIB(即使实现了接口也生成子类代理) |
通过 spring.aop.proxy-target-class=false 显式改回 JDK 优先;注意 @Transactional 的代理方式可能需单独用 @EnableTransactionManagement(proxyTargetClass = false) 配置 |
四、Spring AOP 的实现方式有几种
底层代理技术 有 2 种:JDK 动态代理和 CGLIB。切面定义方式有 4 种:@AspectJ 注解、XML aspect、XML advisor、Advisor API。实际开发中主要用 @AspectJ 注解 + Spring Boot 默认的 CGLIB 代理。"
| 方式 | 特点 | 适用场景 |
|---|---|---|
| @AspectJ 注解 | 用 @Aspect、@Before、@Pointcut 等注解 |
首选,Spring 团队推荐 |
XML <aop:aspect> |
在 XML 中配置切面 | 老项目、需要集中配置 |
XML <aop:advisor> |
aspect 和 Advisor 的"混血儿" |
升级旧项目复用 Advice |
| Advisor API | 直接实现 PointcutAdvisor 等接口 |
需要编程式、低版本 Spring |
五、Spring AOP 原理
Spring AOP 通过动态代理为目标对象生成代理对象,在代理对象的方法前后织入通知,从而实现方法增强。
1、核心思想:代理+拦截
Spring AOP的本质是代理模式。
-
调用方拿到的不是目标对象本身 ,而是代理对象
-
代理对象在调用目标方法前后,插入通知逻辑
-
所以业务代码零侵入,增强自动生效
这就是为什么"内部调用会失效" ------内部调用走的是 this(目标对象),绕过了代理对象。
2、两种代理技术
Spring AOP 底层只有两种代理
1、JDK动态代理
-
条件:目标类至少实现一个接口
-
原理:基于接口,在运行时生成一个实现了相同接口的代理类
-
核心 API :
java.lang.reflect.Proxy+InvocationHandler
java
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(p, method, args) -> {
System.out.println("[前置]");
Object result = method.invoke(target, args);
System.out.println("[后置]");
return result;
}
);
2、CGLB动态代理
-
条件:是否实现接口都可代理
-
原理:运行时生成目标类的子类,重写方法,在重写的方法里织入通知
-
核心 API :
net.sf.cglib.proxy.Enhancer+MethodInterceptor
java
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class); // 继承目标类
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
System.out.println("[前置]");
Object result = proxy.invokeSuper(obj, args);
System.out.println("[后置]");
return result;
});
UserService proxy = (UserService) enhancer.create();
最终用哪种代理,是"配置变量 + 目标类型"两个因素共同决定的。
roxyTargetClass |
目标类有接口吗 | 实际用的代理 |
|---|---|---|
true |
有接口 | CGLIB(强制,不看接口) |
true |
无接口 | CGLIB |
false |
有接口 | JDK |
false |
无接口 | CGLIB(自动退回) |