程云来 / 杭州
Back to Blog·Spring
·About 4 min

Spring(三):一行注解背后,是一个代理对象

从事务、日志和权限的重复代码开始,理解 Spring 如何用代理实现 AOP 与声明式编程,以及代理失效的边界。


业务方法为什么总被同一圈代码包住

创建订单通常需要开启事务、记录耗时、检查权限:

java
void createOrder(Order order) {
    transaction.begin();
    try {
        audit.start("create-order");
        checkPermission();
        saveOrder(order);
        transaction.commit();
    } catch (Exception error) {
        transaction.rollback();
        throw error;
    }
}

这些代码很重要,却不是“创建订单”本身。它们还会重复出现在支付、退款和库存扣减方法中。

把调用变成一条代理链

Spring 可以让调用先经过代理,再进入目标对象:

正在绘制图表…
查看 Mermaid 源码
flowchart LR
    caller[调用方] --> tx[事务代理]
    tx --> auth[权限代理]
    auth --> target[OrderService 目标对象]
    target --> store[OrderRepository]

Mermaid 大图

可滚动查看图表,点击缩放比例可恢复 100%。按 Esc 关闭。

调用方拿到的引用可能是代理对象,而不是原始的 OrderService。代理在方法前后执行额外逻辑,目标对象仍然只负责业务。

java
@Service
class OrderService {
    @Transactional
    public void createOrder(Order order) {
        saveOrder(order);
    }
}

@Transactional 不是把事务代码插入方法体,而是让容器在创建 Bean 时为它配置事务拦截器。调用代理方法时,拦截器负责开始、提交或回滚事务。

这就是面向切面编程(Aspect-Oriented Programming,AOP)的基本模型:把跨越多个业务模块的横切关注点放到独立切面中,在统一的连接点执行。

声明式编程改变了什么

命令式写法描述步骤:

text
开始事务 → 执行业务 → 提交事务 → 出错回滚

声明式写法描述约束:

text
这个方法需要事务

框架根据声明选择执行策略。事务、缓存和安全都可以采用同样的方式,但它们仍然有自己的失败语义,不能因为写了一行注解就认为操作一定安全。

代理不是魔法,有明确边界

最常见的边界是同类内部调用:

java
class OrderService {
    public void createOrder(Order order) {
        save(order); // 直接调用同一个对象的方法
    }

    @Transactional
    public void save(Order order) {
        // 可能绕过代理
    }
}

如果 createOrder 通过 this.save(...) 调用,调用没有经过外层代理,事务拦截器可能不会执行。把需要独立切面的职责拆到另一个 Bean,通常比强行绕过代理更清楚:

java
@Service
class OrderWriter {
    @Transactional
    public void save(Order order) {
        // 经过代理调用
    }
}

还要注意:代理只能拦截它能观察到的调用。对象被直接 new 出来、方法是 private、或者调用发生在代理之外时,声明式能力都可能失效。

什么时候不该使用 AOP

如果逻辑只服务一个方法,而且直接写出来更容易读,就不必为了“统一”抽成切面。AOP 适合稳定、横向、可独立验证的规则;复杂业务分支放进切面,会让控制流离开调用点,增加排查成本。

下一篇会换一个视角:除了在调用前后增强方法,Spring 还如何把固定流程封装起来,只让业务代码实现少量变化部分?

参考:Spring AOPSpring Transaction Management