第30章 Web 层与事务异常:参数校验、@Transactional 失效场景全集

第30章 Web 层与事务异常:参数校验、@Transactional 失效场景全集

30.1 参数校验异常速查

异常 触发场景
MethodArgumentNotValidException @RequestBody 配合 @Valid/@Validated 校验 JSON 请求体失败
BindException @ModelAttribute 表单参数绑定+校验失败(MethodArgumentNotValidException 实际上是 BindException 的子类,用于 @RequestBody 场景)
ConstraintViolationException 方法级 @Validated(类上标注)校验 @PathVariable/@RequestParam 时失败,属于 Bean Validation 规范本身抛出的异常,不经过 Spring MVC 的参数绑定流程
HttpMessageNotReadableException 请求体不是合法 JSON,或字段类型与 DTO 定义不匹配导致反序列化失败
MissingServletRequestParameterException 必填的 @RequestParam 参数未传值
MethodArgumentTypeMismatchException 参数类型转换失败(如 @RequestParam Integer id 但传参是非数字字符串)

30.2 事务失效场景全集(Spring 面试和线上排查的高频重灾区)

Spring 的声明式事务基于 AOP 代理实现,凡是"代理机制无法生效"或"业务代码违反了事务传播/异常处理约定"的场景,都会导致事务静默失效(不报错但事务没生效,比参数校验异常更隐蔽,因为没有任何异常提示,只有在数据不一致发生后才会被发现):

场景1:方法内部自调用

java 复制代码
@Service
public class OrderService {
    public void outer() {
        this.inner(); // 内部调用不经过代理对象,事务注解直接失效
    }
    @Transactional
    public void inner() { ... }
}

原因@Transactional 的实现依赖 Spring AOP 生成的代理对象拦截方法调用,this.inner() 这种调用方式是直接在原始对象上调用方法,压根没有经过代理,自然不会触发事务拦截逻辑。

解决方案:

java 复制代码
// 方案1:注入自身的代理对象(需要开启 exposeProxy 或走 ApplicationContext 获取)
@Service
public class OrderService {
    @Autowired
    @Lazy
    private OrderService self; // 注入代理后的自身引用
    public void outer() {
        self.inner(); // 通过代理调用,事务生效
    }
    @Transactional
    public void inner() { ... }
}

// 方案2(更推荐,架构更清晰):把需要事务的方法拆分到另一个 Bean 里
@Service
public class OrderService {
    @Autowired
    private OrderTransactionalService txService;
    public void outer() {
        txService.inner(); // 跨 Bean 调用,天然经过代理
    }
}

场景2:方法访问修饰符不是 public

java 复制代码
@Transactional
private void save() { ... } // Spring AOP(基于 JDK 动态代理或 CGLIB)默认只能拦截 public 方法,此处注解被静默忽略

Spring 的 AOP 代理机制(无论是接口代理还是 CGLIB 子类代理)在处理非 public 方法时都不会应用事务拦截逻辑,且不会有任何警告或报错,这是最容易被忽视的一类事务失效原因。

场景3:异常被自己 catch 掉,没有重新抛出

java 复制代码
@Transactional
public void save() {
    try {
        doSomething();
    } catch (Exception e) {
        log.error("出错了", e); // 异常被吞掉,事务管理器感知不到有异常发生,事务照常提交
    }
}

原理 :Spring 事务的回滚判断逻辑,本质是在代理方法的 AOP 环绕通知里 catch (Throwable ex),判断这个异常是否满足回滚条件,如果满足则调用 TransactionManager.rollback()如果业务代码自己把异常吞掉了,压根不会有异常抛到代理层,代理感知不到任何异常,事务会被正常提交------即使数据库操作实际上因为业务异常导致数据不完整。

场景4:默认只回滚 RuntimeException 和 Error,不回滚受检异常

java 复制代码
@Transactional // 默认 rollbackFor 只包含 RuntimeException 和 Error
public void save() throws IOException {
    doSomething();
    throw new IOException("网络异常"); // 受检异常,默认不会触发回滚!
}

原理:这是 Spring 框架早期设计的一个延续至今的默认行为------设计者认为受检异常代表"可预期的业务异常",调用方应该能够处理并决定是否需要回滚,因此默认不将其纳入自动回滚范围。这个默认行为经常和大多数开发者的直觉不符(直觉上"抛异常了就该回滚"),必须显式声明:

java 复制代码
@Transactional(rollbackFor = Exception.class) // 显式声明所有异常(含受检异常)都触发回滚
public void save() throws IOException { ... }

场景5:事务传播行为配置不当

java 复制代码
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void innerMethod() {
    // 独立的新事务,即使外层事务回滚,这里已提交的内容不受影响
}

常见误区 :以为 REQUIRES_NEW 能保证"内层失败不影响外层",但反过来的场景经常被忽略------外层事务如果最终回滚,不会撤销 REQUIRES_NEW 内层事务已经独立提交的内容,这在需要"部分操作必须持久化留痕(如审计日志),即使主流程失败也不能丢"的场景是合理设计,但如果开发者误用在了本该保持强一致性的场景(比如库存扣减和订单创建),会导致明明主流程失败了,某些子操作却已经不可撤销地生效,造成数据不一致。

场景6:多线程环境下事务不会跨线程传播

java 复制代码
@Transactional
public void process() {
    save(); // 在当前线程的事务中
    new Thread(() -> {
        saveMore(); // 新线程,不在原事务的上下文中,即使原事务回滚,这里的操作也不会被回滚
    }).start();
}

原理 :Spring 事务的上下文(TransactionSynchronizationManager 内部用 ThreadLocal 存储当前事务状态)是绑定在当前线程 上的,另起线程执行的代码天然拿不到父线程的事务上下文,等同于运行在一个全新的、没有事务包裹的环境里(如果 saveMore() 内部方法本身也标注了 @Transactional,会开启一个和主流程完全独立的新事务,而不是加入主流程的事务)。

场景7:数据库引擎本身不支持事务

sql 复制代码
-- MySQL 的 MyISAM 存储引擎不支持事务,即使 Spring 层面配置了 @Transactional,
-- 底层数据库操作也不会有任何回滚能力,这是最容易被忽视、且完全在应用代码层面无法察觉的一类"事务失效"

排查这类问题时,SHOW TABLE STATUSinformation_schema.TABLES 里查看具体表的 ENGINE 字段,如果不是 InnoDB,无论应用层代码写得多规范都无济于事。

30.3 事务失效的排查方法论

java 复制代码
// 开启 Spring 事务相关的 debug 日志,能清楚看到每次事务的开启/提交/回滚时机
logging:
  level:
    org.springframework.transaction: debug
    org.springframework.orm.jpa: debug
csharp 复制代码
Creating new transaction with name [OrderService.save]: ...
Initiating transaction commit
Committing JPA transaction on EntityManager ...

如果日志里显示事务"Creating new transaction",说明 AOP 代理和事务管理器本身是生效的,问题更可能出在回滚条件 (异常类型是否满足 rollbackFor)上;如果压根看不到这行日志,说明代理层面就没有拦截到这次调用,应该按场景1/2 的方向排查(自调用/非 public 方法)。


相关推荐
Java内核笔记1 小时前
BeanRegistrar:Spring Boot 4 最被低估的新特性,彻底改变 Bean 注册方式
java·后端
用户8181870627461 小时前
第28章 RPC 框架异常:Dubbo / gRPC
后端
搬砖小匠1 小时前
SpringBoot 通过自定义注解 + AOP 实现数据字典自动翻译(通用方案)
java·后端
生信星球1 小时前
空间转录组常规分析(大白话教程)
后端
izhaorui2 小时前
【无标题】
后端
长栎2 小时前
AI 写的接口能用,但永远差点意思——这不是 prompt 的问题,是接口设计的品控规则它没装
后端
小p2 小时前
nextjs学习9: Next.js 渲染与缓存
前端·后端
宋哥转AI2 小时前
深入理解 AI Agent 03|RAG评估体系:量化检索增强效果,精准定位系统短板
人工智能·后端·agent
Leo2822 小时前
数据库Executor-Selector排查实践
后端