Spring自调用导致注解失效
java spring 技术 10

解决方法的核心在于理解Spring事务管理的实现原理——代理模式(Proxy)​

首先拆解一下这个问题和解决方案。

1. 什么是“自调用”?

假设你有一个 UserService类:

@Service
public class UserService {

    public void a() {
        // ... 一些业务逻辑
        this.b(); // 自调用:在同一个类中,通过`this`调用方法b()
        // ... 其他逻辑
    }

    @Transactional // 声明了事务
    public void b() {
        // ... 需要对数据库进行操作,期望在事务中执行
        userRepository.save(...);
    }
}

当你从外部调用 userService.a()时,你期望方法 b()中的数据库操作在一个事务中运行,但实际上它没有。事务注解 @Transactional失效了。

2. 为什么绕过了代理?

我们要先知道Spring是如何实现事务管理的:

  1. Spring使用代理(Proxy)​​:当你给一个Bean的方法加上 @Transactional注解后,Spring在启动时会为这个Bean创建一个代理对象​(可以理解为这个Bean的“管家”或“替身”),并把这个代理对象放入Spring容器中。

  2. 外部调用走代理​:当其他组件(如Controller)通过 @Autowired注入 UserService并调用其方法时,它实际上拿到的是那个代理对象,而不是原始的 UserService对象。

    • 你调用 proxy.a()

    • 代理先做事务管理(如开启事务)

    • 代理再去调用原始对象a()方法

    • 最后代理提交或回滚事务

  3. 自调用不走代理​:但在上面的例子中,你在方法 a()内部使用的是 this.b()。这里的 this指的是原始的 UserService对象本身,而不是它的代理对象。

    • 流程变成了:Controller-> 代理.a()-> 原始对象.a()-> 原始对象.b()

    • 调用 b()方法时,完全绕过了代理管家。管家根本没有机会为 b()方法开启事务。

比喻的说法便于理解:​

  • 代理对象就像你的秘书

  • 你(原始对象)让秘书(代理)去处理一项工作(调用方法 a()),秘书会先做准备工作(开启事务)。

  • 但你在处理工作 a()的过程中,自己亲自去做了另一项本该由秘书安排的工作 b()(直接 this.b())。

  • 秘书不知道你亲自做了 b(),所以也就没有为 b()做任何准备工作(开启事务)。

3. 首选解决方案:“放到另一个Service类中”

解决方案 a的代码示例:

// ServiceA.java
@Service
public class ServiceA {

    @Autowired
    private ServiceB serviceB; // 注入另一个Service

    public void a() {
        // ... 一些业务逻辑
        serviceB.b(); // 通过代理调用另一个Service的事务方法
        // ... 其他逻辑
    }
}
// ServiceB.java
@Service
public class ServiceB {

    @Transactional // 声明了事务
    public void b() {
        // ... 需要对数据库进行操作,期望在事务中执行
        userRepository.save(...);
    }
}

为什么这样就能解决问题?​

  1. 调用路径变化​:现在,ServiceA中的方法 a()是通过 @Autowired注入的 serviceB来调用方法 b()的。

  2. 注入的是代理​:Spring注入的 serviceB并不是原始的 ServiceB对象,而是Spring为 ServiceB创建的代理对象

  3. 代理生效​:所以,调用 serviceB.b()的流程是:

    • ServiceA.a()-> ServiceB的代理.b()

    • 代理先开启事务

    • 代理再调用原始ServiceB对象b()方法

    • 代理根据执行结果提交或回滚事务

这样一来,事务管理就完全正常了。​

其他解决方案

  • b. 注入自身(Self-Injection)​​:在 UserService@Autowired一个 UserService,然后通过这个注入的代理来调用 b()

    • 代码丑陋,显得很绕,不易理解。

    • 可能引起循环依赖的警告。

  • c. 通过AopContext获取代理​:需要暴露代理(@EnableAspectJAutoProxy(exposeProxy = true)),然后在代码里写 ((UserService) AopContext.currentProxy()).b()

    • 严重侵入代码,使得代码与Spring框架强耦合。

    • 性能有轻微开销。

总结:​

当你发现因为自调用导致事务、缓存、异步等注解失效时,第一反应就应该是:“我应该把这个方法抽到一个新的Bean里

Spring自调用导致注解失效
https://xancel.top/archives/springzi-diao-yong-dao-zhi-zhu-jie-shi-xiao
作者
我不是Administrator
发布于
更新于
许可