Spring 【多实现切换 & 事务代理机制】深度解析

这篇文章在讲什么

在使用 Spring 的过程中,有两个问题经常让人困惑:

css 复制代码
问题一:一个接口有多个实现类时,Spring 怎么知道注入哪个?怎么做到不改代码就切换实现?

问题二:@Transactional 加在入口方法上,同类内部调用会被事务包含吗?为什么加在被调用方法上就不生效?

这两个问题看似无关,但它们背后指向同一个核心机制:Spring 的代理模式。 理解了代理,两个问题都能回答。


在开始之前,先问你两个问题

scss 复制代码
问题 A:假设你的项目里同时存在 JdbcOrderRepository 和 MongoOrderRepository,
       两个都实现了 OrderRepository 接口,Spring 会怎么处理?
       你怎么控制用哪个?切换实现需要改 Java 代码吗?

问题 B:假设 placeOrder() 方法没有 @Transactional,
       但它内部调用了有 @Transactional 的 createOrder(),
       createOrder() 里的事务会生效吗?为什么?

带着问题往下读。


第一部分:一个接口多个实现------Spring 怎么选择

一、最简单的场景:一个接口,一个实现

java 复制代码
// 接口
public interface OrderRepository {
    void save(Order order);
}

// 唯一的实现
@Component
public class JdbcOrderRepository implements OrderRepository {
    public void save(Order order) {
        System.out.println("用 JDBC 保存到 MySQL");
    }
}
java 复制代码
@Service
public class OrderService {
    @Autowired
    private OrderRepository repository;  // 只有一个实现,直接注入
}

只有一个实现,没有歧义,Spring 直接注入。 不需要任何额外配置。

二、问题出现了:两个实现类

java 复制代码
@Component
public class JdbcOrderRepository implements OrderRepository {
    public void save(Order order) {
        System.out.println("用 JDBC 保存到 MySQL");
    }
}

@Component
public class MongoOrderRepository implements OrderRepository {
    public void save(Order order) {
        System.out.println("用 MongoDB 保存");
    }
}
java 复制代码
@Service
public class OrderService {
    @Autowired
    private OrderRepository repository;  // 两个实现,注入哪个?
}

直接运行会报错:

yaml 复制代码
NoUniqueBeanDefinitionException: 
No qualifying bean of type 'OrderRepository' available: 
expected single matching bean but found 2: jdbcOrderRepository, mongoOrderRepository

Spring 不会替你做选择题------发现有两个匹配的 Bean,不知道选哪个,直接报错。

三、解决方式一:@Primary(指定默认优先级)

java 复制代码
@Component
public class JdbcOrderRepository implements OrderRepository {
    public void save(Order order) {
        System.out.println("用 JDBC 保存到 MySQL");
    }
}

@Primary  // 优先注入这个
@Component
public class MongoOrderRepository implements OrderRepository {
    public void save(Order order) {
        System.out.println("用 MongoDB 保存");
    }
}
java 复制代码
@Service
public class OrderService {
    @Autowired
    private OrderRepository repository;  // 注入 MongoOrderRepository
}

但这种方式还是写死在代码里了。换实现就要改代码(删掉 @Primary 或换到另一个类上)。

四、解决方式二:@Qualifier(精确指定名称)

java 复制代码
@Component("jdbcRepo")
public class JdbcOrderRepository implements OrderRepository {
    // ...
}

@Component("mongoRepo")
public class MongoOrderRepository implements OrderRepository {
    // ...
}
java 复制代码
@Service
public class OrderService {
    @Autowired
    @Qualifier("jdbcRepo")  // 明确指定要哪个
    private OrderRepository repository;
}

同样写死在代码里了。换实现要改 @Qualifier 的值。

五、真正不改代码的切换方式

方式一:通过配置文件切换(@ConditionalOnProperty)

java 复制代码
@ConditionalOnProperty(name = "repository.type", havingValue = "jdbc")
@Component
public class JdbcOrderRepository implements OrderRepository {
    public void save(Order order) {
        System.out.println("用 JDBC 保存到 MySQL");
    }
}

@ConditionalOnProperty(name = "repository.type", havingValue = "mongodb")
@Component
public class MongoOrderRepository implements OrderRepository {
    public void save(Order order) {
        System.out.println("用 MongoDB 保存");
    }
}
java 复制代码
@Service
public class OrderService {
    @Autowired
    private OrderRepository repository;  // 注入哪个取决于配置文件
}
properties 复制代码
# application.properties
repository.type=jdbc      # 用 MySQL
# repository.type=mongodb  # 换这一行就切到 MongoDB

改配置文件,不改 Java 代码。 这就是 Spring Boot 自动配置的思路。

方式二:通过环境切换(@Profile)

java 复制代码
@Profile("mysql")
@Component
public class JdbcOrderRepository implements OrderRepository {
    public void save(Order order) {
        System.out.println("用 JDBC 保存到 MySQL");
    }
}

@Profile("mongodb")
@Component
public class MongoOrderRepository implements OrderRepository {
    public void save(Order order) {
        System.out.println("用 MongoDB 保存");
    }
}
properties 复制代码
# application.properties
spring.profiles.active=mysql     # 切换这一行

方式三:通过引入不同的包切换(最贴近实际业务)

场景:你做了一个支付系统,需要支持微信支付和支付宝支付。

复制代码
项目结构:
  payment-api        ← 定义接口(纯接口,没有实现)
  payment-wechat     ← 微信支付实现(独立的 JAR 包)
  payment-alipay     ← 支付宝支付实现(独立的 JAR 包)
  payment-app        ← 主应用(只依赖接口)

接口模块(payment-api):

java 复制代码
public interface PaymentService {
    PayResult pay(PayRequest request);
}

微信支付模块(payment-wechat):

java 复制代码
@Component
@ConditionalOnClass(name = "com.wechat.sdk.WechatPayClient")
public class WechatPaymentService implements PaymentService {
    
    @Autowired
    private WechatPayClient wechatClient;
    
    public PayResult pay(PayRequest request) {
        return wechatClient.createOrder(request);
    }
}

支付宝支付模块(payment-alipay):

java 复制代码
@Component
@ConditionalOnClass(name = "com.alipay.sdk.AlipayClient")
public class AlipayPaymentService implements PaymentService {
    
    @Autowired
    private AlipayClient alipayClient;
    
    public PayResult pay(PayRequest request) {
        return alipayClient.createOrder(request);
    }
}

主应用只依赖接口,不依赖任何实现:

java 复制代码
@Service
public class OrderService {
    @Autowired
    private PaymentService paymentService;  // 注入哪个取决于引入了哪个包
    
    public void placeOrder(Order order) {
        paymentService.pay(order.toPayRequest());
    }
}

pom.xml 决定用哪个实现:

xml 复制代码
<!-- 用微信支付 -->
<dependencies>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>payment-wechat</artifactId>
    </dependency>
</dependencies>

<!-- 要换支付宝?改 pom.xml,不改 Java 代码 -->
<!--
<dependency>
    <groupId>com.example</groupId>
    <artifactId>payment-alipay</artifactId>
</dependency>
-->

背后的原理:

kotlin 复制代码
引入 payment-wechat
    → payment-wechat.jar 进入 classpath
    → classpath 里有 WechatPayClient.class
    → WechatPaymentService 的 @ConditionalOnClass 生效
    → WechatPaymentService 被创建为 Bean
    → @Autowired PaymentService 注入 WechatPaymentService

引入 payment-alipay
    → payment-alipay.jar 进入 classpath
    → classpath 里有 AlipayClient.class
    → AlipayPaymentService 的 @ConditionalOnClass 生效
    → AlipayPaymentService 被创建为 Bean
    → @Autowired PaymentService 注入 AlipayPaymentService

这跟 Spring Boot 通过换 Starter 切换 Tomcat/Jetty 是完全一样的思路。 Tomcat 和 Jetty 也是两个不同的实现,换一个 JAR 包就切换了,Java 代码一行不动。

六、小结

java 复制代码
多个实现类时 Spring 怎么选择?

  只有一个实现 → 直接注入,不需要额外配置
  多个实现 → 报错,需要你指定

怎么不改代码切换?

  @ConditionalOnProperty  → 通过配置文件切换
  @Profile                → 通过环境切换
  @ConditionalOnClass     → 通过引入不同的包切换(Starter 思路)

核心思想:
  代码依赖接口,不依赖实现。
  切换实现时,改的是配置或依赖,不是 Java 代码。
  这就是 IoC 的价值------控制权交给外部。

第二部分:@Transactional 同类调用------为什么事务不生效

一、先理解 @Transactional 背后的机制

@Transactional 不是魔法,它基于 AOP 动态代理。

当你写:

java 复制代码
@Service
public class OrderService {
    
    @Transactional
    public void createOrder(Order order) {
        repository.save(order);
        inventoryService.deduct(order);
    }
}

Spring 启动时做的事情:

markdown 复制代码
1. 发现 OrderService 有 @Transactional 方法
2. 不直接创建 OrderService 实例
3. 创建一个代理对象(Proxy),包装 OrderService
4. 把代理对象注册到容器里

容器里存的不是 OrderService 本身,而是 OrderService 的代理对象:

ini 复制代码
┌─────────────────────────────┐
│  Spring 容器                  │
│                              │
│  orderService = Proxy ──────→ OrderService(真实对象)
│                              │
└─────────────────────────────┘

当外部代码调用 orderService.createOrder() 时:

scss 复制代码
你调用的是代理对象的 createOrder()
    ↓
代理对象先开启事务
    ↓
代理对象调用真实对象的 createOrder()
    ↓
真实对象执行业务逻辑(repository.save、inventoryService.deduct)
    ↓
如果成功 → 代理对象提交事务
如果抛异常 → 代理对象回滚事务

事务的开启和提交/回滚是代理做的,不是你的代码做的。

二、场景一:@Transactional 在入口方法上

java 复制代码
@Service
public class OrderService {
    
    @Transactional  // 放在入口方法上
    public void placeOrder(Order order) {
        repository.save(order);          // ✅ 在事务里
        inventoryService.deduct(order);  // ✅ 在事务里
        sendNotification(order);         // ✅ 在事务里,虽然是同类调用
    }
    
    // 同类的另一个方法,没有 @Transactional
    public void sendNotification(Order order) {
        notificationClient.send(order);
    }
}

调用链:

scss 复制代码
你的代码 → 代理.placeOrder()
                ↓
           代理开启事务
                ↓
           真实对象.placeOrder() 开始执行
                ↓
           repository.save(order)        ← 在事务里 ✅
                ↓
           inventoryService.deduct()     ← 在事务里 ✅
                ↓
           this.sendNotification()       ← 还在事务里 ✅
                ↓
           方法执行完毕,回到代理
                ↓
           代理提交事务(或回滚)

结论:入口方法有 @Transactional → 代理开启事务 → 内部所有代码(包括同类调用)都在事务里。

三、场景二:@Transactional 在被调用的方法上

java 复制代码
@Service
public class OrderService {
    
    // 入口方法:没有 @Transactional
    public void placeOrder(Order order) {
        repository.save(order);     // 没有事务
        createOrder(order);         // 调用同类方法
    }
    
    // 被调用的方法:有 @Transactional
    @Transactional
    public void createOrder(Order order) {
        repository.save(order);         // 事务会生效吗?
        inventoryService.deduct(order); // 事务会生效吗?
    }
}

调用链:

kotlin 复制代码
你的代码 → 代理.placeOrder()
                ↓
           代理发现 placeOrder 没有 @Transactional
           → 不开启事务
                ↓
           真实对象.placeOrder() 开始执行
                ↓
           repository.save(order)        ← 没有事务 ❌
                ↓
           this.createOrder()
                ↑
           this 是谁?是真实对象本身,不是代理
           直接调用真实对象的方法,没有经过代理
                ↓
           @Transactional 被忽略 ❌
           repository.save(order)        ← 没有事务 ❌
           inventoryService.deduct()     ← 没有事务 ❌

结论:入口方法没有 @Transactional → 代理不开事务 → 内部同类调用 this.createOrder() 绕过了代理 → @Transactional 被忽略。

四、画一张对比图

less 复制代码
场景一:@Transactional 在入口方法上(生效 ✅)

  你的代码
      ↓
  代理.placeOrder()          ← 代理开启事务
      ↓
  真实对象.placeOrder()
      ├── repository.save()     ← 在事务里 ✅
      ├── this.sendNotification() ← 在事务里 ✅
      ↓
  代理                        ← 代理提交/回滚事务


场景二:@Transactional 在被调用方法上(不生效 ❌)

  你的代码
      ↓
  代理.placeOrder()          ← 没有 @Transactional,不开事务
      ↓
  真实对象.placeOrder()
      ├── repository.save()     ← 没有事务 ❌
      ├── this.createOrder()    ← 没经过代理,@Transactional 被忽略 ❌
      │       ├── repository.save()         ← 没有事务 ❌
      │       └── inventoryService.deduct() ← 没有事务 ❌

五、为什么是这样?根本原因

this 关键字。

java 复制代码
public void placeOrder(Order order) {
    this.createOrder(order);  // this 指向当前对象(真实对象),不是代理
}

Java 的 this 永远指向当前正在运行的对象实例。在 placeOrder() 方法内部,this 就是真实对象本身。调用 this.createOrder() 就是直接调用真实对象的方法,没有任何代理介入。

代理只有在外部调用时才能介入:

java 复制代码
// 外部调用:经过代理 ✅
orderService.createOrder(order);  // orderService 是代理对象

// 同类内部调用:不经过代理 ❌
this.createOrder(order);          // this 是真实对象

六、怎么解决

方案一:把方法拆到不同的类里(推荐)

java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private OrderCreator orderCreator;  // 注入的是代理对象
    
    public void placeOrder(Order order) {
        repository.save(order);
        orderCreator.createOrder(order);  // 通过代理调用,事务生效 ✅
    }
}

@Service
public class OrderCreator {
    
    @Transactional
    public void createOrder(Order order) {
        repository.save(order);
        inventoryService.deduct(order);
    }
}

方案二:注入自己(能用但不推荐)

java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private OrderService self;  // 注入的是代理对象
    
    public void placeOrder(Order order) {
        repository.save(order);
        self.createOrder(order);  // 通过代理调用,事务生效 ✅
    }
    
    @Transactional
    public void createOrder(Order order) {
        repository.save(order);
        inventoryService.deduct(order);
    }
}

七、回答具体问题

@Transactional 放在入口方法上,同类内部调用会被事务包含吗?

会。 事务在代理调用入口方法时就开启了,方法返回后才提交或回滚。中间不管调用了多少个同类方法,都在这一个事务里。

为什么加在被调用方法上就不生效?

因为同类内部调用是 this.createOrder()this 是真实对象,不是代理。调用没有经过代理,代理无法开启事务,@Transactional 注解被忽略了。

事务不生效的是被调用的方法吗?

不生效的是这次调用。 方法本身有 @Transactional 注解,但因为调用没经过代理,注解没有被处理。如果从外部直接调用这个方法,事务是会生效的。

事务应该把中间的逻辑都包含吧?

对。前提是入口方法上的调用经过了代理。 经过代理的调用,从代理开启事务到代理提交/回滚,中间的所有代码都在事务里。

八、小结

less 复制代码
@Transactional 的生效条件:
  1. 方法必须是 public 的
  2. 调用必须经过代理对象(不能是同类内部 this 调用)
  3. 异常必须是 RuntimeException(默认只回滚运行时异常)

同类调用事务不生效的根本原因:
  this 指向真实对象,不经过代理,代理无法介入

最佳实践:
  @Transactional 放在入口方法上
  需要事务的逻辑放在入口方法里,或拆到单独的类中

回顾与整合

两个问题的共同本质

这两个问题看似无关,但背后指向同一个核心:Spring 的代理机制。

kotlin 复制代码
问题一(多实现切换):
  代码依赖接口,不依赖实现。
  Spring 容器管理实现的选择,通过配置或引入不同的包来切换。
  控制权在外部(配置文件、pom.xml),不在代码里。

问题二(事务同类调用):
  @Transactional 基于代理。
  代理拦截方法调用,插入事务逻辑。
  同类内部 this 调用绕过了代理,事务无法生效。

代理模式是 Spring AOP 的基础,理解了代理,就理解了 Spring 中很多"看起来奇怪"的行为。

现在回答开头的问题

问题 A:两个实现类时,Spring 怎么处理?怎么控制用哪个?

只有一个实现时直接注入。多个实现时 Spring 报错,需要你通过 @Primary@Qualifier@ConditionalOnProperty@Profile 或引入不同包来指定。真正的不改代码切换靠配置或依赖管理。

问题 B:placeOrder() 没有 @Transactional,内部调用有 @Transactional 的 createOrder(),事务生效吗?

不生效。因为 this.createOrder() 没有经过代理。如果把 @Transactional 移到 placeOrder() 上,内部所有调用(包括同类调用)都会被事务包含。

相关推荐
彩票管理中心秘书长2 小时前
MySQL 数据库高级与网络管理操作命令大全
后端
Gopher_HBo2 小时前
CompletableFuture函数原理
后端
香山上的麻雀10082 小时前
由 Rust 开发的能大幅降低LLM token消耗的高性能 CLI 代理工具 rtk
开发语言·后端·rust
神奇小汤圆2 小时前
Java vs Go:哈希冲突解决之道,为什么一个用红黑树,一个用桶?
后端
神奇小汤圆2 小时前
得物二面:Redis 中某个 Key 访问量特别大怎么办?我:Redis 能顶得住... 生瓜蛋子 生瓜蛋子
后端
掘金者阿豪2 小时前
Spring Data JPA 接入金仓数据库:少写代码,多干活
前端·后端
Moment2 小时前
AI 时代,为什么全栈项目越来越离不开 Monorepo 和 TypeScript
前端·javascript·后端
神奇小汤圆2 小时前
字节一面凉了!被问接口超时频繁,线程池该怎么优化?面试官:你管这叫高并发优化?
后端
Jenlybein3 小时前
用 uv 替代 conda,速度飙升(从 0 到 1 开始使用 uv)
后端·python·算法