Spring事务自调用失效问题:Spring 默认使用代理(proxy)来实现事务拦截:只有通过代理对象的调用才会触发事务增强
我们来写一个完整可运行的 Spring Boot + MySQL Demo,验证
在 ServiceA.methodA() 中直接调用 methodB()(同类自调用),即使
methodB()上有@Transactional,事务也不会生效。
🧱 一、环境假设
- Spring Boot 版本:
2.7.x或3.x均可 - MySQL 已启动(比如:
localhost:3306/test) - 表结构自建或由程序初始化
- 你可以直接通过 Postman/浏览器 访问
http://localhost:8080/test/selfCall验证效果
🧩 二、项目结构
src/main/java/com/example/txdemo/├── TxDemoApplication.java # 启动类├── controller/TestController.java # 控制层入口├── service/DemoService.java # 核心业务逻辑,包含methodA与methodB└── resources/application.yml
🧰 三、依赖(pom.xml)
<project xmlns="http://maven.apache.org/POM/4.0.0" ...><modelVersion>4.0.0</modelVersion><groupId>com.example</groupId><artifactId>txdemo</artifactId><version>0.0.1-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.10</version></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency></dependencies><properties><java.version>11</java.version></properties>
</project>
⚙️ 四、配置文件(application.yml)
spring:datasource:url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8username: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver# 显示SQL日志jpa:show-sql: truelogging:level:org.springframework.jdbc.core.JdbcTemplate: DEBUG
🧠 五、建表 SQL
CREATE TABLE IF NOT EXISTS person (id BIGINT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(100)
);
🧩 六、启动类(TxDemoApplication.java)
package com.example.txdemo;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class TxDemoApplication {public static void main(String[] args) {SpringApplication.run(TxDemoApplication.class, args);}
}
🧭 七、Service 逻辑(核心验证点)
package com.example.txdemo.service;import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service
public class DemoService {private final JdbcTemplate jdbc;public DemoService(JdbcTemplate jdbc) {this.jdbc = jdbc;}// 方法A:不带事务,内部调用方法Bpublic void methodA() {System.out.println("====> 进入 methodA()");try {// 同类内部调用 -> 不经过代理methodB();} catch (Exception e) {System.out.println("methodA 捕获异常: " + e.getMessage());}System.out.println("<==== 退出 methodA()");}// 方法B:带事务注解@Transactionalpublic void methodB() {System.out.println("====> 进入 methodB() [事务标注生效?]");jdbc.update("INSERT INTO person(name) VALUES (?)", "内部调用事务测试");System.out.println("插入完成,马上抛异常测试事务回滚...");throw new RuntimeException("测试事务回滚异常");}// 用于通过代理验证(控制器直接调用)@Transactionalpublic void methodBFromController() {System.out.println("====> 进入 methodBFromController() [事务标注生效]");jdbc.update("INSERT INTO person(name) VALUES (?)", "Controller直接调用事务测试");System.out.println("插入完成,马上抛异常测试事务回滚...");throw new RuntimeException("测试事务回滚异常");}public int count() {return jdbc.queryForObject("SELECT COUNT(*) FROM person", Integer.class);}public void clear() {jdbc.update("DELETE FROM person");}
}
🌐 八、Controller(TestController.java)
package com.example.txdemo.controller;import com.example.txdemo.service.DemoService;
import org.springframework.web.bind.annotation.*;@RestController
@RequestMapping("/test")
public class TestController {private final DemoService demoService;public TestController(DemoService demoService) {this.demoService = demoService;}// 1️⃣ 验证【内部调用】事务不生效@GetMapping("/selfCall")public String testSelfCall() {demoService.clear();try {demoService.methodA(); // A 内部调用 B(事务不会生效)} catch (Exception ignored) {}int count = demoService.count();return "执行完毕(内部调用),表中记录数:" + count;}// 2️⃣ 验证【通过代理调用】事务生效@GetMapping("/proxyCall")public String testProxyCall() {demoService.clear();try {demoService.methodBFromController(); // Controller -> 代理 -> Service 方法(事务生效)} catch (Exception ignored) {}int count = demoService.count();return "执行完毕(代理调用),表中记录数:" + count;}
}
🚀 九、运行 & 验证
启动项目后访问:
✅ 情况 1:内部自调用(事务不生效)
GET http://localhost:8080/test/selfCall
控制台输出:
====> 进入 methodA()
====> 进入 methodB() [事务标注生效?]
插入完成,马上抛异常测试事务回滚...
methodA 捕获异常: 测试事务回滚异常
<==== 退出 methodA()
返回结果:
执行完毕(内部调用),表中记录数:1
👉 插入提交成功,说明事务没有生效(因为 B 的事务未被代理拦截)。
✅ 情况 2:通过代理调用(事务生效)
GET http://localhost:8080/test/proxyCall
控制台输出:
====> 进入 methodBFromController() [事务标注生效]
插入完成,马上抛异常测试事务回滚...
返回结果:
执行完毕(代理调用),表中记录数:0
👉 插入回滚成功,说明事务确实生效。
✅ 十、结论
| 场景 | 调用方式 | 是否经过代理 | 事务是否生效 | 表中记录 |
|---|---|---|---|---|
| methodA → this.methodB() | 同类内部调用 | ❌ 否 | ❌ 不生效 | ✅ 插入成功 |
| Controller → DemoService.methodB() | 代理调用 | ✅ 是 | ✅ 生效 | ❌ 回滚成功 |
将带事务的方法(B)拆分到另一个独立的 @Service 中,通过 Spring 容器注入并调用
将带事务的方法(B)拆分到另一个独立的 @Service 中,通过 Spring 容器注入并调用。
这样就会通过 Spring AOP 代理对象 调用,从而让 @Transactional 生效。
✅ 一、实现目标
我们现在的目标是:
| 场景 | 调用方式 | 是否通过代理 | 事务生效 | 结果 |
|---|---|---|---|---|
| Controller → ServiceA.methodA() → 调用 ServiceB.methodB() | ✅ 是 | ✅ 生效 | 插入会回滚 |
🧩 二、项目结构
src/main/java/com/example/txdemo/├── TxDemoApplication.java├── controller/TestController.java├── service/ServiceA.java # 外层逻辑,不带事务├── service/ServiceB.java # 内层逻辑,带事务└── resources/application.yml
🧠 三、ServiceB(带事务的方法)
package com.example.txdemo.service;import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service
public class ServiceB {private final JdbcTemplate jdbc;public ServiceB(JdbcTemplate jdbc) {this.jdbc = jdbc;}@Transactionalpublic void methodB() {System.out.println("====> 进入 ServiceB.methodB() [事务生效]");jdbc.update("INSERT INTO person(name) VALUES (?)", "事务正常生效");System.out.println("插入完成,马上抛异常测试回滚...");throw new RuntimeException("测试事务回滚异常");}public int count() {return jdbc.queryForObject("SELECT COUNT(*) FROM person", Integer.class);}public void clear() {jdbc.update("DELETE FROM person");}
}
四、ServiceA(调用方)
package com.example.txdemo.service;import org.springframework.stereotype.Service;@Service
public class ServiceA {private final ServiceB serviceB;public ServiceA(ServiceB serviceB) {this.serviceB = serviceB;}public void methodA() {System.out.println("====> 进入 ServiceA.methodA()");try {// 调用另一个 bean 的方法,会经过代理serviceB.methodB();} catch (Exception e) {System.out.println("ServiceA 捕获异常: " + e.getMessage());}System.out.println("<==== 退出 ServiceA.methodA()");}
}
🌐 五、Controller
package com.example.txdemo.controller;import com.example.txdemo.service.ServiceA;
import com.example.txdemo.service.ServiceB;
import org.springframework.web.bind.annotation.*;@RestController
@RequestMapping("/test2")
public class TestController {private final ServiceA serviceA;private final ServiceB serviceB;public TestController(ServiceA serviceA, ServiceB serviceB) {this.serviceA = serviceA;this.serviceB = serviceB;}// 测试拆分Service后事务是否生效@GetMapping("/crossService")public String crossServiceCall() {serviceB.clear();serviceA.methodA(); // A -> B(带事务)int count = serviceB.count();return "执行完毕(跨Service调用),表中记录数:" + count;}
}
⚙️ 六、配置文件(application.yml)
spring:datasource:url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8username: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverlogging:level:org.springframework.jdbc.core.JdbcTemplate: DEBUG
🚀 七、验证执行
启动项目后访问:
GET http://localhost:8080/test2/crossService
控制台输出:
====> 进入 ServiceA.methodA()
====> 进入 ServiceB.methodB() [事务生效]
插入完成,马上抛异常测试回滚...
ServiceA 捕获异常: 测试事务回滚异常
<==== 退出 ServiceA.methodA()
返回结果:
执行完毕(跨Service调用),表中记录数:0
👉 表明:
ServiceB.methodB()的事务确实生效;- 抛出的异常导致回滚;
- 表中记录未插入成功(回滚成功)。
✅ 八、对比总结
| 场景 | 调用方式 | 事务生效 | 表记录结果 |
|---|---|---|---|
| A 内部直接调用 B(同类) | ❌ 未经过代理 | ❌ 不生效 | ✅ 插入成功 |
| A 调用 B(不同 Service) | ✅ 经过代理 | ✅ 生效 | ❌ 回滚成功 |
💡 九、建议与实践经验
- 建议将带事务的业务逻辑拆分到单独的 Service 中,让调用经过 Spring 代理。
- @Transactional 一般放在 public 方法上(AOP 代理只拦截 public)。
- 避免 private 方法加事务,或同类自调用导致事务失效。
- 如果确实必须在同类内部调用,也可以启用
AopContext.currentProxy()+exposeProxy=true,但不推荐生产环境使用。
