这篇文章在讲什么
在使用 Spring 的过程中,有两个问题经常让人困惑:
问题一:一个接口有多个实现类时,Spring 怎么知道注入哪个?怎么做到不改代码就切换实现?
问题二:@Transactional 加在入口方法上,同类内部调用会被事务包含吗?为什么加在被调用方法上就不生效?
这两个问题看似无关,但它们背后指向同一个核心机制:Spring 的代理模式。 理解了代理,两个问题都能回答。
在开始之前,先问你两个问题
问题 A:假设你的项目里同时存在 JdbcOrderRepository 和 MongoOrderRepository,
两个都实现了 OrderRepository 接口,Spring 会怎么处理?
你怎么控制用哪个?切换实现需要改 Java 代码吗?
问题 B:假设 placeOrder() 方法没有 @Transactional,
但它内部调用了有 @Transactional 的 createOrder(),
createOrder() 里的事务会生效吗?为什么?
带着问题往下读。
第一部分:一个接口多个实现——Spring 怎么选择
一、最简单的场景:一个接口,一个实现
// 接口
public interface OrderRepository {
void save(Order order);
}
// 唯一的实现
@Component
public class JdbcOrderRepository implements OrderRepository {
public void save(Order order) {
System.out.println("用 JDBC 保存到 MySQL");
}
}
@Service
public class OrderService {
@Autowired
private OrderRepository repository; // 只有一个实现,直接注入
}
只有一个实现,没有歧义,Spring 直接注入。 不需要任何额外配置。
二、问题出现了:两个实现类
@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 保存");
}
}
@Service
public class OrderService {
@Autowired
private OrderRepository repository; // 两个实现,注入哪个?
}
直接运行会报错:
NoUniqueBeanDefinitionException:
No qualifying bean of type 'OrderRepository' available:
expected single matching bean but found 2: jdbcOrderRepository, mongoOrderRepository
Spring 不会替你做选择题——发现有两个匹配的 Bean,不知道选哪个,直接报错。
三、解决方式一:@Primary(指定默认优先级)
@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 保存");
}
}
@Service
public class OrderService {
@Autowired
private OrderRepository repository; // 注入 MongoOrderRepository
}
但这种方式还是写死在代码里了。换实现就要改代码(删掉 @Primary 或换到另一个类上)。
四、解决方式二:@Qualifier(精确指定名称)
@Component("jdbcRepo")
public class JdbcOrderRepository implements OrderRepository {
// ...
}
@Component("mongoRepo")
public class MongoOrderRepository implements OrderRepository {
// ...
}
@Service
public class OrderService {
@Autowired
@Qualifier("jdbcRepo") // 明确指定要哪个
private OrderRepository repository;
}
同样写死在代码里了。换实现要改 @Qualifier 的值。
五、真正不改代码的切换方式
方式一:通过配置文件切换(@ConditionalOnProperty)
@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 保存");
}
}
@Service
public class OrderService {
@Autowired
private OrderRepository repository; // 注入哪个取决于配置文件
}
# application.properties
repository.type=jdbc # 用 MySQL
# repository.type=mongodb # 换这一行就切到 MongoDB
改配置文件,不改 Java 代码。 这就是 Spring Boot 自动配置的思路。
方式二:通过环境切换(@Profile)
@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 保存");
}
}
# application.properties
spring.profiles.active=mysql # 切换这一行
方式三:通过引入不同的包切换(最贴近实际业务)
场景:你做了一个支付系统,需要支持微信支付和支付宝支付。
项目结构:
payment-api ← 定义接口(纯接口,没有实现)
payment-wechat ← 微信支付实现(独立的 JAR 包)
payment-alipay ← 支付宝支付实现(独立的 JAR 包)
payment-app ← 主应用(只依赖接口)
接口模块(payment-api):
public interface PaymentService {
PayResult pay(PayRequest request);
}
微信支付模块(payment-wechat):
@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):
@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);
}
}
主应用只依赖接口,不依赖任何实现:
@Service
public class OrderService {
@Autowired
private PaymentService paymentService; // 注入哪个取决于引入了哪个包
public void placeOrder(Order order) {
paymentService.pay(order.toPayRequest());
}
}
pom.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>
-->
背后的原理:
引入 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 代码一行不动。
六、小结
多个实现类时 Spring 怎么选择?
只有一个实现 → 直接注入,不需要额外配置
多个实现 → 报错,需要你指定
怎么不改代码切换?
@ConditionalOnProperty → 通过配置文件切换
@Profile → 通过环境切换
@ConditionalOnClass → 通过引入不同的包切换(Starter 思路)
核心思想:
代码依赖接口,不依赖实现。
切换实现时,改的是配置或依赖,不是 Java 代码。
这就是 IoC 的价值——控制权交给外部。
第二部分:@Transactional 同类调用——为什么事务不生效
一、先理解 @Transactional 背后的机制
@Transactional 不是魔法,它基于 AOP 动态代理。
当你写:
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
repository.save(order);
inventoryService.deduct(order);
}
}
Spring 启动时做的事情:
1. 发现 OrderService 有 @Transactional 方法
2. 不直接创建 OrderService 实例
3. 创建一个代理对象(Proxy),包装 OrderService
4. 把代理对象注册到容器里
容器里存的不是 OrderService 本身,而是 OrderService 的代理对象:
┌─────────────────────────────┐
│ Spring 容器 │
│ │
│ orderService = Proxy ──────→ OrderService(真实对象)
│ │
└─────────────────────────────┘
当外部代码调用 orderService.createOrder() 时:
你调用的是代理对象的 createOrder()
↓
代理对象先开启事务
↓
代理对象调用真实对象的 createOrder()
↓
真实对象执行业务逻辑(repository.save、inventoryService.deduct)
↓
如果成功 → 代理对象提交事务
如果抛异常 → 代理对象回滚事务
事务的开启和提交/回滚是代理做的,不是你的代码做的。
二、场景一:@Transactional 在入口方法上
@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);
}
}
调用链:
你的代码 → 代理.placeOrder()
↓
代理开启事务
↓
真实对象.placeOrder() 开始执行
↓
repository.save(order) ← 在事务里 ✅
↓
inventoryService.deduct() ← 在事务里 ✅
↓
this.sendNotification() ← 还在事务里 ✅
↓
方法执行完毕,回到代理
↓
代理提交事务(或回滚)
结论:入口方法有 @Transactional → 代理开启事务 → 内部所有代码(包括同类调用)都在事务里。
三、场景二:@Transactional 在被调用的方法上
@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); // 事务会生效吗?
}
}
调用链:
你的代码 → 代理.placeOrder()
↓
代理发现 placeOrder 没有 @Transactional
→ 不开启事务
↓
真实对象.placeOrder() 开始执行
↓
repository.save(order) ← 没有事务 ❌
↓
this.createOrder()
↑
this 是谁?是真实对象本身,不是代理
直接调用真实对象的方法,没有经过代理
↓
@Transactional 被忽略 ❌
repository.save(order) ← 没有事务 ❌
inventoryService.deduct() ← 没有事务 ❌
结论:入口方法没有 @Transactional → 代理不开事务 → 内部同类调用 this.createOrder() 绕过了代理 → @Transactional 被忽略。
四、画一张对比图
场景一:@Transactional 在入口方法上(生效 ✅)
你的代码
↓
代理.placeOrder() ← 代理开启事务
↓
真实对象.placeOrder()
├── repository.save() ← 在事务里 ✅
├── this.sendNotification() ← 在事务里 ✅
↓
代理 ← 代理提交/回滚事务
场景二:@Transactional 在被调用方法上(不生效 ❌)
你的代码
↓
代理.placeOrder() ← 没有 @Transactional,不开事务
↓
真实对象.placeOrder()
├── repository.save() ← 没有事务 ❌
├── this.createOrder() ← 没经过代理,@Transactional 被忽略 ❌
│ ├── repository.save() ← 没有事务 ❌
│ └── inventoryService.deduct() ← 没有事务 ❌
五、为什么是这样?根本原因
this 关键字。
public void placeOrder(Order order) {
this.createOrder(order); // this 指向当前对象(真实对象),不是代理
}
Java 的 this 永远指向当前正在运行的对象实例。在 placeOrder() 方法内部,this 就是真实对象本身。调用 this.createOrder() 就是直接调用真实对象的方法,没有任何代理介入。
代理只有在外部调用时才能介入:
// 外部调用:经过代理 ✅
orderService.createOrder(order); // orderService 是代理对象
// 同类内部调用:不经过代理 ❌
this.createOrder(order); // this 是真实对象
六、怎么解决
方案一:把方法拆到不同的类里(推荐)
@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);
}
}
方案二:注入自己(能用但不推荐)
@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 注解,但因为调用没经过代理,注解没有被处理。如果从外部直接调用这个方法,事务是会生效的。
事务应该把中间的逻辑都包含吧?
对。前提是入口方法上的调用经过了代理。 经过代理的调用,从代理开启事务到代理提交/回滚,中间的所有代码都在事务里。
八、小结
@Transactional 的生效条件:
1. 方法必须是 public 的
2. 调用必须经过代理对象(不能是同类内部 this 调用)
3. 异常必须是 RuntimeException(默认只回滚运行时异常)
同类调用事务不生效的根本原因:
this 指向真实对象,不经过代理,代理无法介入
最佳实践:
@Transactional 放在入口方法上
需要事务的逻辑放在入口方法里,或拆到单独的类中
回顾与整合
两个问题的共同本质
这两个问题看似无关,但背后指向同一个核心:Spring 的代理机制。
问题一(多实现切换):
代码依赖接口,不依赖实现。
Spring 容器管理实现的选择,通过配置或引入不同的包来切换。
控制权在外部(配置文件、pom.xml),不在代码里。
问题二(事务同类调用):
@Transactional 基于代理。
代理拦截方法调用,插入事务逻辑。
同类内部 this 调用绕过了代理,事务无法生效。
代理模式是 Spring AOP 的基础,理解了代理,就理解了 Spring 中很多"看起来奇怪"的行为。
现在回答开头的问题
问题 A:两个实现类时,Spring 怎么处理?怎么控制用哪个?
只有一个实现时直接注入。多个实现时 Spring 报错,需要你通过 @Primary、@Qualifier、@ConditionalOnProperty、@Profile 或引入不同包来指定。真正的不改代码切换靠配置或依赖管理。
问题 B:placeOrder() 没有 @Transactional,内部调用有 @Transactional 的 createOrder(),事务生效吗?
不生效。因为 this.createOrder() 没有经过代理。如果把 @Transactional 移到 placeOrder() 上,内部所有调用(包括同类调用)都会被事务包含。