原文来自于:zha-ge.cn/java/133
从代理到切面:Spring AOP 的本质与应用场景解析
几年前我第一次听到 "AOP(面向切面编程)" 这个词时,脑子里只有一个想法------
"这不就是在代码里偷偷加东西吗?"
后来项目一大、模块一多,横切逻辑(日志、权限、事务、监控)越来越多,重复代码堆成山。
那时候我才真正明白:AOP 不是魔法,而是帮我们从"局部关注"中抽身出来的一种架构哲学。
一、先别慌:AOP 到底是干嘛的?
AOP(Aspect-Oriented Programming)------面向切面编程。
它的核心思想一句话概括:
把那些在多个地方都会出现的"通用逻辑",从业务逻辑中剥离出来,集中管理。
比如日志记录、异常处理、权限校验、事务控制......
这些逻辑跟具体业务无关,却又"无处不在",这就是所谓的横切关注点(Cross-cutting Concern)。
Spring AOP 做的事,就是在不改原代码的情况下,把这些逻辑"切"进去。
二、直观理解:AOP 是如何"切"的?
假设我们有一个用户注册服务:
typescript
@Service
public class UserService {
public void register(String name) {
System.out.println("注册用户:" + name);
}
}
现在我想在所有服务执行前后自动打印日志。
传统做法?------每个方法加日志代码,复制粘贴一百次。
AOP 的做法?------在原逻辑外套一层"代理"。
调用方 → 代理对象(增强逻辑)→ 目标对象
于是调用变成这样:
less
@Before
System.out.println("[LOG] 方法开始执行...");
调用目标方法
@After
System.out.println("[LOG] 方法执行结束。");
而这中间,你的 UserService
从头到尾都没改。
这就是 AOP 的威力。
三、Spring AOP 的核心概念图谱
名称 | 含义 |
---|---|
Aspect(切面) | 横切逻辑的集合(比如日志、事务) |
Join Point(连接点) | 可以被增强的方法调用点 |
Pointcut(切点) | 一组匹配规则,定义"哪些连接点要增强" |
Advice(通知) | 增强逻辑,分为前置、后置、异常、环绕等 |
Weaving(织入) | 把切面逻辑插入目标对象的过程 |
Proxy(代理) | 增强后的对象,负责拦截方法调用 |
简单来说:
切面决定"做什么",切点决定"在哪做",织入决定"怎么做"。
四、Spring 是怎么做到"无侵入增强"的?
Spring 的 AOP 是基于 动态代理机制 实现的。
它主要有两种策略👇
1️⃣ JDK 动态代理(基于接口)
- 要求目标类实现接口。
- 通过
java.lang.reflect.Proxy
在运行时生成代理对象。
ini
UserService proxy = (UserService) Proxy.newProxyInstance(
classLoader,
new Class[]{UserService.class},
(proxy, method, args) -> {
System.out.println("前置逻辑");
Object result = method.invoke(target, args);
System.out.println("后置逻辑");
return result;
}
);
Spring 会在容器启动时自动替你干完这些。
2️⃣ CGLIB 动态代理(基于子类)
- 当目标类没有接口时,Spring 使用 CGLIB(Code Generation Library)。
- 通过生成目标类的子类并重写方法实现代理。
ini
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
System.out.println("环绕逻辑");
return proxy.invokeSuper(obj, args);
});
UserService proxy = (UserService) enhancer.create();
所以你加上 @Transactional
、@Async
、@Around
时,Spring 都是在背后生成这样的代理对象。
五、最常见的 AOP 注解实战
1️⃣ 声明一个切面类
java
@Aspect
@Component
public class LogAspect {
// 定义切点(匹配规则)
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
// 前置通知
@Before("serviceMethods()")
public void before() {
System.out.println("[AOP] 方法执行前");
}
// 后置通知
@AfterReturning("serviceMethods()")
public void after() {
System.out.println("[AOP] 方法执行后");
}
// 环绕通知
@Around("serviceMethods()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("[AOP] 环绕前");
Object result = joinPoint.proceed();
System.out.println("[AOP] 环绕后");
return result;
}
}
2️⃣ 开启 AOP 支持
在配置类上加一句:
css
@EnableAspectJAutoProxy
Spring 会自动扫描带 @Aspect
的切面,并在运行时为目标 Bean 织入代理。
六、AOP 的常见应用场景
场景 | 说明 |
---|---|
日志监控 | 方法调用、参数、耗时统一打印 |
权限校验 | 拦截特定业务逻辑,验证用户权限 |
事务管理 | 进入方法前开启事务,异常后回滚 |
性能统计 | 统计接口耗时,记录慢查询 |
缓存处理 | 查询前命中缓存,返回结果后更新缓存 |
统一异常处理 | 拦截 Controller 层异常并封装响应 |
这些场景的共同点是:横切关注点,与业务逻辑无关但必须存在。
七、AOP 与事务的关系:两者其实是一家人
很多人以为 AOP 和事务是两个独立概念。其实 Spring 的事务底层正是通过 AOP 实现的!
typescript
@Transactional
public void saveOrder() {
...
}
当 Spring 扫描到 @Transactional
时,它会自动为该方法生成代理对象,
在方法执行前后织入:
- 开启事务(Before)
- 提交事务(After)
- 回滚事务(Exception)
所以当你手动 new 出一个带事务的方法,事务就失效了 ------
因为 AOP 没进来。
八、AOP 常见误区与坑点
🔴 坑 1:自调用导致切面失效
csharp
public void outer() {
inner(); // 同类内调用,不走代理
}
✅ 解决:
- 拆分到另一个 Bean,或
- 从
AopContext.currentProxy()
获取代理再调用。
🔴 坑 2:final / private 方法无法被代理
因为代理无法继承或重写它们。
✅ 解决:
让方法保持 public
、非 final。
🔴 坑 3:切点表达式写错没匹配上
execution(* com.example.service.*.*(..))
少一个 *
就没命中。
✅ 解决:
在切面里加日志或调试 AspectJExpressionPointcut
。
九、面试强化:AOP 的灵魂三问
Q1:Spring AOP 和 AspectJ 有什么区别?
👉 Spring AOP 基于动态代理 ,在运行时织入;
👉 AspectJ 基于编译期或类加载期织入 ,功能更强(如字段级别)。
Spring 内部默认使用 AspectJ 的语法模型,但执行方式是代理级的。
Q2:AOP 是怎么实现的?
👉 通过 JDK Proxy (接口) 或 CGLIB (子类) 生成代理对象,
在调用目标方法前后织入增强逻辑。
Q3:事务、缓存、日志这些功能和 AOP 有什么关系?
👉 它们都是 AOP 的典型应用:Spring 通过代理在运行时织入增强逻辑,从而实现声明式编程。
十、总结:AOP 是解耦的终极武器
Spring AOP 不是黑魔法,而是一种极致优雅的架构思想:
- 把横切逻辑抽离出来,让业务代码更纯粹;
- 用代理机制无侵入织入逻辑;
- 让日志、权限、事务、缓存都能"声明即用"。
一句话记住:
"AOP 让你从写代码的人,变成编织系统行为的人。"