前言
很多人用Spring AOP时,以为只要在方法上加个@Transactional、@Retry,就能自动有事务、重试功能。但当代码出问题时,往往一脸懵逼:为什么注解没生效?为什么代理失败了?
这次我用不到1000行代码,手写了一个Mini-AOP框架,纯JDK实现,0依赖。目的很简单:看清楚AOP到底是怎么运作的。
核心认知:AOP不是你想的那样
常见误解
很多人以为AOP是这样的:
java
@Aspect
public class LogAspect {
@Before("UserService.*")
public void log(JoinPoint jp) {
System.out.println("打日志");
}
}
// 然后写业务代码
@Service
public class UserService {
public void saveUser(String name) {
// 自动就有日志了?
}
}
以为加了注解就自动在方法前后插入代码了。这是错的!
真实情况
AOP的本质是三层结构:

- 声明层 :你写的
@Before、@Around等注解,只是配置,不执行任何逻辑 - 触发层:代理对象在方法调用时,通过表达式匹配决定哪些切面要执行
- 业务层:被AOP包裹的真实业务代码
用大白话说:注解不会自动生效,必须有一个代理层来识别注解、匹配表达式、决定执行顺序。
Mini-AOP的核心实现
整体架构
graph LR
A[用户调用] --> B[代理对象]
B --> C{切点匹配}
C -->|匹配成功| D[收集通知]
D --> E[执行Before]
E --> F[执行Around]
F --> G[真实方法]
G --> H[执行After]
C -->|不匹配| G
关键类说明
1. BeanFactory - 容器和代理创建者
java
public class BeanFactory {
private Map<String, Object> beans = new HashMap<>();
private List<Object> aspects = new ArrayList<>();
// 注册Bean时,识别出切面类
public void register(Class<?> clazz) {
Object instance = clazz.newInstance();
if (clazz.isAnnotationPresent(Aspect.class)) {
aspects.add(instance); // 收集切面
}
beans.put(getBeanName(clazz), instance);
}
// 获取Bean时,决定是否创建代理
public <T> T getBean(String name, Class<T> type) {
Object bean = beans.get(name);
if (needProxy(bean)) {
return AopProxy.createProxy(bean, aspects); // 创建代理
}
return (T) bean;
}
}
核心逻辑:
- 注册时收集所有
@Aspect类 - 获取Bean时判断是否需要代理,如果需要就返回代理对象而不是原始对象
2. AopProxy - 代理和切点匹配核心
java
public class AopProxy implements InvocationHandler {
private Object target;
private List<Object> aspects;
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
// 1. 遍历所有切面
for (Object aspect : aspects) {
for (Method aspectMethod : aspect.getClass().getDeclaredMethods()) {
// 2. 检查注解和切点表达式
if (aspectMethod.isAnnotationPresent(Before.class)) {
String pointcut = aspectMethod.getAnnotation(Before.class).value();
// 3. 匹配表达式
if (matches(pointcut, method)) {
beforeAdvices.add(new Advice(aspect, aspectMethod));
}
}
}
}
// 4. 按顺序执行通知链
return executeAdvices(method, args, beforeAdvices, afterAdvices);
}
// 简单的切点匹配
private boolean matches(String pointcut, Method method) {
String fullName = method.getDeclaringClass().getSimpleName()
+ "." + method.getName();
String regex = pointcut.replace("*", ".*");
return fullName.matches(regex);
}
}
核心逻辑:
- 方法调用时,遍历所有切面类
- 读取切面方法上的注解和表达式
- 用表达式匹配当前方法,决定是否执行
- 收集所有匹配的通知,按顺序执行
完整的调用流程
以这段代码为例:
java
// 注册
factory.register(LogAspect.class);
factory.register(UserServiceImpl.class);
// 获取
UserService userService = factory.getBean("userService", UserService.class);
// 调用
userService.saveUser("张三");
实际执行流程:
sequenceDiagram
participant U as 用户代码
participant P as 代理对象
participant A as AopProxy.invoke
participant L as LogAspect
participant R as RetryAspect
participant T as 真实对象
U->>P: saveUser("张三")
P->>A: invoke(method, args)
A->>A: 匹配切点表达式
A->>L: @Before匹配成功
L-->>A: logBefore()
A->>R: @Around匹配成功
R->>A: retry { proceed() }
A->>T: method.invoke(target, args)
T-->>A: 返回结果
A->>L: @After执行
L-->>A: logAfter()
A-->>P: 返回最终结果
P-->>U: 返回给用户
切点表达式的匹配机制
这是最容易被忽略的部分。很多人以为写了@Before("UserService.*")就自动生效,但实际上:
java
// 切面定义
@Before("UserService.*") // 只是声明了一个字符串
public void log(JoinPoint jp) { }
// 必须有代码来解析这个字符串
private boolean matches(String pointcut, Method method) {
String className = method.getDeclaringClass().getSimpleName();
String methodName = method.getName();
String fullName = className + "." + methodName; // 如 "UserService.saveUser"
// 把表达式转成正则
String regex = pointcut.replace("*", ".*"); // "UserService.*" -> "UserService\..*"
// 匹配
return fullName.matches(regex);
}
关键点:
- 表达式只是字符串,不会自动匹配
- 必须在运行时解析表达式并和方法名比对
- 这个匹配发生在每次方法调用时
通知的执行顺序
多个切面叠加时,执行顺序是这样的:

代码实现:
java
private Object executeAdvices(Method method, Object[] args,
List<Advice> beforeAdvices,
List<Advice> afterAdvices,
List<Advice> aroundAdvices) {
// 构建调用链
Supplier<Object> chain = () -> {
try {
// 执行@Before
for (Advice advice : beforeAdvices) {
advice.method.invoke(advice.aspect, new JoinPoint(target, method, args));
}
// 执行真实方法
Object result = method.invoke(target, args);
// 执行@After
for (Advice advice : afterAdvices) {
advice.method.invoke(advice.aspect, new JoinPoint(target, method, args));
}
return result;
} catch (Exception e) {
throw new RuntimeException(e);
}
};
// @Around包裹整个链条
for (Advice around : aroundAdvices) {
Object finalChain = chain;
chain = () -> around.method.invoke(around.aspect,
new ProceedingJoinPoint(target, method, args, finalChain));
}
return chain.get();
}
一个完整示例
切面定义
java
@Aspect
@Order(1)
public class LogAspect {
@Before("UserService.*")
public void logBefore(JoinPoint jp) {
System.out.println("[日志] 方法: " + jp.getSignature());
}
@After("UserService.*")
public void logAfter(JoinPoint jp) {
System.out.println("[日志] 方法执行完毕");
}
}
@Aspect
@Order(2)
public class RetryAspect {
@Around("UserService.save*")
public Object retry(ProceedingJoinPoint pjp) throws Throwable {
for (int i = 0; i < 3; i++) {
try {
System.out.println("[重试] 第 " + (i + 1) + " 次");
return pjp.proceed();
} catch (Exception e) {
if (i == 2) throw e;
Thread.sleep(1000);
}
}
return null;
}
}
业务代码
java
public interface UserService {
void saveUser(String name);
}
@Component
public class UserServiceImpl implements UserService {
private int count = 0;
@Override
public void saveUser(String name) {
count++;
System.out.println("==> 保存用户: " + name);
if (count < 3) {
throw new RuntimeException("保存失败");
}
}
}
运行结果
java
BeanFactory factory = new BeanFactory();
factory.register(LogAspect.class);
factory.register(RetryAspect.class);
factory.register(UserServiceImpl.class);
UserService service = factory.getBean("userService", UserService.class);
service.saveUser("张三");
输出:
ini
[日志] 方法: UserService.saveUser
[重试] 第 1 次
==> 保存用户: 张三
[重试] 第 2 次
==> 保存用户: 张三
[重试] 第 3 次
==> 保存用户: 张三
[日志] 方法执行完毕
可以看到:
- 日志切面先执行(Order=1)
- 重试切面包裹在外面(Order=2)
- 失败两次后第三次成功
和Spring AOP的对比
相同点
- 都使用JDK动态代理
- 都通过切点表达式匹配方法
- 都支持多种通知类型和优先级
不同点
| 特性 | Mini-AOP | Spring AOP |
|---|---|---|
| 代理方式 | 只有JDK代理 | JDK + CGLIB |
| 切点表达式 | 简单通配符 | 完整AspectJ语法 |
| 容器 | 手动注册 | 自动扫描 |
| 依赖注入 | 不支持 | 完整支持 |
| 代码量 | 800行 | 10万行+ |
本质是一样的
Spring AOP的核心流程:

和Mini-AOP对比:
| 步骤 | Mini-AOP | Spring AOP |
|---|---|---|
| 扫描切面 | factory.register(LogAspect.class) |
@ComponentScan + @Aspect |
| 判断是否需要代理 | needProxy(bean) |
AbstractAutoProxyCreator.wrapIfNecessary |
| 创建代理 | AopProxy.createProxy |
ProxyFactory.getProxy |
| 切点匹配 | matches(pointcut, method) |
AspectJExpressionPointcut.matches |
结论:Spring只是把这套逻辑做得更通用、更灵活,但底层思想完全一致。
关键收获
1. 注解不是魔法
写了@Before不会自动执行,必须有代码去:
- 读取注解
- 解析表达式
- 匹配方法
- 执行逻辑
2. 代理是核心
AOP的本质就是动态代理:
java
// 用户以为调用的是
UserService service = new UserServiceImpl();
// 实际拿到的是
UserService service = Proxy.newProxyInstance(...);
所有AOP逻辑都在这个代理对象的invoke方法里。
3. 表达式匹配是关键
切面能不能生效,取决于:
java
if (matches("UserService.*", method)) {
// 执行切面逻辑
}
这个匹配逻辑才是决定"哪些方法被拦截"的真正原因。
4. 三层结构
永远记住:
注解(声明) + 代理(触发) + 匹配(决策) = AOP
缺少任何一层,AOP都不会生效。
总结
手写这个Mini-AOP框架后,对Spring AOP的理解彻底通透了:
- AOP不是注解的魔法,是代理对象在运行时的动态匹配和调用
- 切点表达式只是字符串,需要有匹配器来解析和判断
- 通知的执行顺序由代理对象控制,不是注解决定的
- Spring AOP只是把这套机制做得更完善,本质和800行代码没区别
下次遇到"为什么@Transactional没生效"这种问题,就能快速定位:是不是代理没创建?是不是表达式没匹配上?是不是调用方式不对?