AOP 的真相:注解只是声明,代理才是执行

前言

很多人用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的本质是三层结构

  1. 声明层 :你写的@Before@Around等注解,只是配置,不执行任何逻辑
  2. 触发层:代理对象在方法调用时,通过表达式匹配决定哪些切面要执行
  3. 业务层:被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);
}

关键点

  1. 表达式只是字符串,不会自动匹配
  2. 必须在运行时解析表达式并和方法名比对
  3. 这个匹配发生在每次方法调用时

通知的执行顺序

多个切面叠加时,执行顺序是这样的:

代码实现:

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 次
==> 保存用户: 张三
[日志] 方法执行完毕

可以看到:

  1. 日志切面先执行(Order=1)
  2. 重试切面包裹在外面(Order=2)
  3. 失败两次后第三次成功

和Spring AOP的对比

相同点

  1. 都使用JDK动态代理
  2. 都通过切点表达式匹配方法
  3. 都支持多种通知类型和优先级

不同点

特性 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不会自动执行,必须有代码去:

  1. 读取注解
  2. 解析表达式
  3. 匹配方法
  4. 执行逻辑

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的理解彻底通透了:

  1. AOP不是注解的魔法,是代理对象在运行时的动态匹配和调用
  2. 切点表达式只是字符串,需要有匹配器来解析和判断
  3. 通知的执行顺序由代理对象控制,不是注解决定的
  4. Spring AOP只是把这套机制做得更完善,本质和800行代码没区别

下次遇到"为什么@Transactional没生效"这种问题,就能快速定位:是不是代理没创建?是不是表达式没匹配上?是不是调用方式不对?

相关推荐
努力的小郑2 小时前
MyBatis 两个隐蔽深坑实录:Arrays.asList() 与数字 0 的“离奇失踪”
java·面试·mybatis
Xの哲學2 小时前
Linux AQM 深度剖析: 拥塞控制
linux·服务器·算法·架构·边缘计算
故渊ZY2 小时前
SpringMVC核心原理与实战全解析
java·spring
renke33642 小时前
Flutter 2025 状态管理工程体系:从简单共享到复杂协同,构建可预测、可测试、可维护的状态流架构
flutter·架构
勤劳打代码2 小时前
循序渐进 —— Flutter GetX 状态管理
flutter·面试·前端框架
廋到被风吹走2 小时前
【Spring】核心类研究价值排行榜
java·后端·spring
徐小夕2 小时前
pxcharts 多维表格开源!一款专为开发者和数据分析师打造的轻量化智能表格
前端·架构·github
Mintopia3 小时前
🏗️ B端架构中的用户归因与埋点最佳实践
前端·react.js·架构
czlczl200209253 小时前
SpringBoot实践:从验证码到业务接口的完整交互生命周期
java·spring boot·redis·后端·mysql·spring