Spring MVC中@Valid @NotBlank产品更新校验失效问题排查
问题根因
所有异常表现(仅编辑场景触发、能手动调用getName拿到值、校验报name为null、调试看到ByteBuddyInterceptor代理对象)都是两个机制叠加导致的:
- 第一,Service层查询用的
productRepository.getOne(id)返回的是Hibernate未初始化的懒加载代理对象,而非真实的Product实体实例。这类代理通过ByteBuddyInterceptor拦截所有方法调用,字段值不会直接存在代理自身的内存空间,而是暂存在拦截器内部。 - 第二,Controller类标注了
@SessionAttributes("product"),POST请求触发时,Spring MVC会优先从Session中取出之前GET请求存入的代理对象,作为@ModelAttribute的表单绑定目标,而非创建全新的真实实例接收表单参数。 - 具体的异常逻辑:
- 表单提交的
name值会被Spring调用代理的setName()方法暂存在拦截器内部,不会写入代理持有的父类实体的原始字段内存位置。 - 手动调用
product.getName()时,代理的拦截器会优先返回拦截器内部暂存的表单值,所以你能拿到正确的产品名。 - 而
@Valid触发的Bean Validation校验流程,是直接通过反射读取父类实体的原始字段值,不会触发代理的方法拦截逻辑,此时读到的字段值仍是初始的null,就会报@NotBlank的校验错误,提示被拒绝的值为null。
- 表单提交的
- 新建产品流程完全没有上述代理对象、Session复用绑定的环节,使用的是Spring创建的真实Product实例,字段读写逻辑统一,所以校验完全正常。
可落地的修复方案
- 替换Repository的查询方法,从根源避免返回懒加载代理:
把productRepository.getOne(id)替换为productRepository.findById(id).orElseThrow(EntityNotFoundException::new),这个方法返回的是从数据库加载完成的真实实体对象,没有懒加载拦截逻辑,字段的反射读取、方法调用取值结果完全一致,不会再出现校验逻辑和业务读取逻辑取值不一致的问题。 - 避免将JPA代理对象直接暴露给Web层的表单绑定流程:
编辑页加载时,将查询到的Product实体转换为独立的编辑表单DTO,把DTO存入Session作为模型属性,表单绑定、参数校验全流程都操作这个DTO;校验通过后再将DTO的值拷贝回真实的实体实例执行更新操作,从架构层面切断持久化对象和Web绑定逻辑的耦合。 - 绕开Session代理的绑定逻辑:
在POST请求方法中,根据请求传入的id从数据库查出真实的Product实体,通过WebDataBinder将表单提交的参数绑定到这个真实实体上,不再复用Session中存储的代理对象作为绑定目标。
内容的提问来源于stack exchange,提问作者Erfan Ahmed
相关产品推荐
相关产品推荐

