本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
【腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云
上周五,微信群里讨论的热火朝天,原因是被我的一篇文章《https://mp.weixin.qq.com/s/PeCU6_GjRoorxUHyNzZGeA》激起了大家的愤慨!
当时,有一个网友提了一个关于BindingResult使用上的问题,很快就被其它消息淹没了。
根据当时,我和这位网友的交流,我这里整理一下关于 Spring Boot 中 BindingResult 的相关用法,让大家能够轻轻松松从入门到填坑!
文章配图参见https://mp.weixin.qq.com/s/1NBmfVbjniw8M95EetNmAA。
话说在 Spring Boot 开发中,参数校验是必不可少的一环。而提到参数校验,就绕不开 BindingResult 这个“老熟人”。它看似简单,实则暗藏玄机。很多开发者在初次接触时,经常会遇到各种莫名其妙的坑——要么校验没生效,要么突然抛异常,要么页面报错说 BindingResult 不能用……
尤其是一些老项目和个别网友,非常喜欢 BindingResult。但稍有不慎,就会陷入被 BindingResult 支配的恐惧之中。接下来,我们就来彻底讲透 BindingResult 的作用、使用方式、常见 bug 以及最佳实践。
BindingResult 是什么?有什么作用?
BindingResult 我其实用的很少,一方面是我觉得它不够灵活,另一方面是在低代码等潮流的影响下,BindingResult 非常鸡肋。虽说我用的不多,但还是有不少网友跳进它的坑里了,下面我们就从“它是什么?有什么作用?”讲起。
BindingResult 是 Spring 框架中位于org.springframework.validation包下的一个接口,用于封装数据绑定和验证过程中的错误信息。
它的核心作用是:
收集验证错误:当使用@Valid或@Validated对参数进行校验时,如果校验失败,错误信息会被自动封装到 BindingResult 对象中避免异常抛出:正常情况下,如果校验失败且没有 BindingResult,Spring MVC 会直接抛出MethodArgumentNotValidException或BindException。而有了 BindingResult,异常会被“吞掉”,让我们可以在代码中手动处理错误。
说白了,BindingResult 就是一个“错误收集器”,让我们能够优雅地处理参数校验失败的情况。
BindingResult 的基本使用
我们先来看看它的基础用法示例。
第一步,定义实体类并添加校验注解。
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
public class User {
@NotEmpty(message = "用户名不能为空")
private String name;
@Size(min = 6, max = 20, message = "密码长度必须在6-20位之间")
private String password;
// getter/setter 省略
}
第二步,Controller 中使用 BindingResult。
@PostMapping("/user")
public String createUser(@Valid @RequestBody User user, BindingResult bindingResult) {
// 判断是否有校验错误
if (bindingResult.hasErrors()) {
// 获取所有错误信息
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError error : fieldErrors) {
System.out.println("字段:" + error.getField() +
",错误信息:" + error.getDefaultMessage());
}
return "参数校验失败";
}
// 校验通过,执行业务逻辑
return "success";
}
通过上面的代码,可以看出 BindingResult 用起来并不难,但是要用好,就稍微需要掌握一些经验。
核心方法详解
BindingResult 提供了丰富的 API 来获取错误信息。
| 方法 | 说明 |
|---|---|
hasErrors() | 判断是否有错误 |
getFieldErrors() | 获取所有字段错误列表 |
getFieldError(String field) | 获取指定字段的错误 |
getGlobalErrors() | 获取全局错误(对象级错误) |
getAllErrors() | 获取所有错误 |
getErrorCount() | 获取错误总数 |
更多方法和用法,参考官方文档https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/validation/BindingResult.html。
@Valid 和 BindingResult 的绑定
这里要记住一条重要规则:@Valid和BindingResult必须一一对应,且 BindingResult 必须紧跟在被校验的对象后面。
看下面的案例。
// 正确:一一对应,顺序正确
public String method1(@Valid User user, BindingResult userResult,
@Valid Role role, BindingResult roleResult)
// 错误:顺序不对,userResult 会接收到 role 的错误
public String method2(@Valid User user, @Valid Role role,
BindingResult userResult)
// 错误:缺少对应的 BindingResult
public String method3(@Valid User user)
BindingResult 的 6 大常见坑
接下来,我汇总了群友讨论的常见的 6 个大坑,大家看看有没有入坑过的。
文章配图参见https://mp.weixin.qq.com/s/1NBmfVbjniw8M95EetNmAA。
第一个坑,忘记加 @Valid 或 @Validated。
症状为,校验注解完全不生效,无论输入什么都能通过。
经过排查后得知原因是,BindingResult 本身只是一个容器,它需要配合 @Valid 或 @Validated 来触发校验。
针对这种情况的解决方案是确保在被校验的对象前加上 @Valid 或 @Validated。
// 错误:没有 @Valid,bindingResult 永远没有错误
public String create(@RequestBody User user, BindingResult bindingResult)
// 正确
public String create(@Valid @RequestBody User user, BindingResult bindingResult)
第二个常见的坑是,BindingResult 和被校验对象之间隔着其他参数。
遇到的症状是,校验失败时抛出 MethodArgumentNotValidException,而不是进入方法体。
究其原因,Spring 要求 BindingResult 必须紧跟在被校验对象后面,中间不能有其他参数。
// 错误:中间多了 HttpServletRequest
public String create(@Valid User user, HttpServletRequest request,
BindingResult bindingResult)
// 正确
public String create(@Valid User user, BindingResult bindingResult,
HttpServletRequest request)
第三个坑是对简单类型使用 BindingResult。
常见的症状是校验注解加在 String、Integer 等简单类型上,BindingResult 收不到错误。
背后的原因是 Spring MVC 对方法参数使用不同的解析器,简单类型的参数校验不走 Bean Validation 的流程,BindingResult 无法接收它们的校验信息。
// 错误:对 String 参数进行校验,bindingResult 收不到错误
public String save(@NotBlank(message = "名称不能为空") String name,
BindingResult bindingResult)
// 正确:使用包装类或实体对象
public String save(@Valid @RequestBody User user, BindingResult bindingResult)
第 4 个坑是 IllegalStateException,BindingResult 和普通目标对象都不能用作请求属性。
这是社区里被问得最多的异常之一,在 Stack Overflow https://stackoverflow.com/questions/56596671上也有大量讨论。
社区里反馈的症状是页面报错,java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'xxx' available as request attribute。
背后的原因分析后得知,这个异常通常出现在使用 Spring MVC 表单标签(<form:form>、<form:input> 等)时。当渲染 JSP 或 Thymeleaf 页面时,Spring 会在 Model 中查找指定名称的属性对象,但没找到。
有下面 3 种常见场景。
- GET 请求中没有将目标对象放入 Model
- 表单的
modelAttribute属性值与 Model 中的属性名不匹配 - 校验失败后,重定向时没有保留原对象
正确的解决方案如下代码所示。
// GET 请求:确保将空对象放入 Model
@GetMapping("/user/form")
public String showForm(Model model) {
// 关键!
model.addAttribute("user", new User());
return "userForm";
}
// POST 请求:校验失败时,返回的视图中也要有该对象
@PostMapping("/user/save")
public String save(@Valid @ModelAttribute("user") User user,
BindingResult result, Model model) {
if (result.hasErrors()) {
// 保留用户输入
model.addAttribute("user", user);
return "userForm";
}
return "redirect:/user/list";
}
第 5 个坑是,忽略 BindingResult 却期望不抛异常。
通常是方法参数中加了 BindingResult 但从未使用,结果还是抛异常。
这类问题背后的原因也很简单。虽然加上了 BindingResult,但 Spring 仍然会进行校验。如果代码中没有通过hasErrors()检查,并且校验失败,Spring 还是会抛出异常吗?
答案是:不会。
BindingResult 的存在本身就告诉 Spring 不要抛异常,而是把错误放入 BindingResult。但如果不检查,错误会被忽略,可能导致数据继续处理。
所以,遇到这类问题的解决方案是务必检查 hasErrors() 并做出相应处理。
最后一个坑是对嵌套对象校验不生效。
尤其是实体类中包含另一个实体对象,但内部对象的校验注解不生效。
这个原因是 @Valid 不会自动级联校验嵌套对象,需要在嵌套属性上也加上@Valid。
public class Order {
@Valid // 必须加上这个!
private List<OrderItem> items;
@Valid // 必须加上这个!
private User buyer;
}
需要注意的是,随着 Spring 7.x 和 Spring Boot 4.x 的流行。现在@Valid也支持套娃了。具体可以参加我前面写的文章《https://mp.weixin.qq.com/s/IAFHNDk3V3SWI3ESn9Ib_A》。
BindingResult 最佳实践
我这里总结了 3 类最佳实践,供大家参考!
统一异常处理 vs 手动处理
这里整理了两种处理校验错误的方式,各有优劣。
方式一,手动处理(使用 BindingResult)。
@PostMapping("/user")
public Result createUser(@Valid @RequestBody User user,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
// 手动收集错误信息返回
Map<String, String> errors = new HashMap<>();
bindingResult.getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return Result.fail(400, "参数校验失败", errors);
}
return Result.success();
}
方式二,统一异常处理(不使用 BindingResult)。
// Controller 中不加 BindingResult
@PostMapping("/user")
public Result createUser(@Valid @RequestBody User user) {
return Result.success();
}
// 统一异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidationException(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return Result.fail(400, "参数校验失败", errors);
}
}
这里,推荐大家使用#统一异常处理的方式。原因有下面 3 条。
- Controller 代码更简洁,业务逻辑更清晰
- 错误处理逻辑集中,易于维护
- 避免在每个方法中重复编写错误处理代码
合理使用分组校验
当同一个实体在不同场景下有不同的校验规则时,推荐使用分组校验。
// 定义分组接口
public interface CreateGroup {}
public interface UpdateGroup {}
// 实体类中使用分组
public class User {
@NotNull(message = "ID不能为空", groups = {UpdateGroup.class})
private Long id;
@NotBlank(message = "用户名不能为空", groups = {CreateGroup.class, UpdateGroup.class})
private String name;
}
// Controller 中使用
@PostMapping("/user")
public Result create(@Validated(CreateGroup.class) @RequestBody User user) {
// 创建场景
}
@PutMapping("/user")
public Result update(@Validated(UpdateGroup.class) @RequestBody User user) {
// 更新场景
}
自定义校验注解
当内置注解无法满足需求时,可以自定义校验注解,案例代码如下所示。
// 自定义注解
@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 校验器实现
public class PhoneValidator implements ConstraintValidator<Phone, String> {
private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true; // 空值由 @NotBlank 处理
}
return PHONE_PATTERN.matcher(value).matches();
}
}
校验消息国际化
配置国际化消息,让错误提示支持多语言。
多语言配置文件,如resources/ValidationMessages.properties。
user.name.notempty=用户名不能为空
user.password.size=密码长度必须在{min}-{max}位之间
然后在 Java 实体类中使用。
public class User {
@NotEmpty(message = "{user.name.notempty}")
private String name;
@Size(min = 6, max = 20, message = "{user.password.size}")
private String password;
}
总结
BindingResult 是 Spring Boot 参数校验中不可或缺的一环,理解它的工作原理和常见陷阱,能让我们的代码更加健壮。
现在回顾一下本文的核心要点。
- 基本规则:
@Valid/@Validated和 BindingResult 必须一一对应,且紧挨着 - 常见坑点:忘记加校验注解、参数顺序错误、简单类型校验、表单页面缺对象、嵌套校验不生效
- 最佳实践:优先使用统一异常处理、合理运用分组校验、善用自定义注解
以上,期待这篇文章能够帮助到部分网友在使用 BindingResult 时少踩坑。如果有网友遇到过其他有趣的问题,欢迎在评论区留言讨论!

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!
本文原文出处:业余草: » 乱用 Spring Boot 参数校验 BindingResult 的 6 个大坑,我被第 4 个坑了 3 小时