You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot修改BindingResult后仍返回全部校验错误的问题

问题:Spring Boot校验优先级控制异常

练习Spring Boot校验功能时遇到如下问题:
需求是:带@NotEmpty注解的字段,若该注解触发(比如字段为空),则仅显示@NotEmpty的错误信息;只有当@NotEmpty未触发时,才显示该字段其他校验注解(如@Length、@Pattern)的错误。但提交空表单时,username这类字段会同时显示@NotEmpty和@Length的错误提示,不符合预期。

实体类代码

public class Student {
@NotEmpty
@NotNull
@Length(min = 3, max = 10)
private String username;
@NotEmpty
@Pattern(regexp = "((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%]).{6,20})")
private String password;
@NotEmpty
@Email
private String email;
@URL
private String website;
@Min(18)
@Max(30)
private int age;

public Student() {
    super();
}

public Student(String username, String password, String email, String website, int age) {
    super();
    this.username = username;
    this.password = password;
    this.email = email;
    this.website = website;
    this.age = age;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}

public String getWebsite() {
    return website;
}

public void setWebsite(String website) {
    this.website = website;
}

public String getEmail() {
    return email;
}

public void setEmail(String email) {
    this.email = email;
}

public String getUsername() {
    return username;
}

public void setUsername(String username) {
    this.username = username;
}

public String getPassword() {
    return password;
}

public void setPassword(String password) {
    this.password = password;
}

@Override
public String toString() {
    return "Student [username=" + username + ", password=" + password + ", email=" + email + ", website=" + website
            + ", age=" + age + "]";
}

}

Controller代码

@Controller
@RequestMapping({ "validation", "" })
public class ValidationController {     
    @RequestMapping(value = { "index1", "/" }, method = RequestMethod.GET)
    public String index(ModelMap modelMap) {                        
        modelMap.put("student", new Student());
        return "/validation/index";
    }
    
    @RequestMapping(value = { "resgister" }, method = RequestMethod.POST)
    public String resgister(ModelMap modelMap, RedirectAttributes redirectAttributes, @ModelAttribute("student") @Valid Student student, BindingResult bindingResult) {
        if(bindingResult.hasErrors()) {
            var errors = Helper.customErrorMessageOrder(bindingResult.getFieldErrors());
            bindingResult = new BeanPropertyBindingResult(student, "student");
            for(var e : errors) {
                bindingResult.addError(e);
            }               
            return "/validation/index";
        }
        redirectAttributes.addFlashAttribute("message", "Success");
        return "redirect:/validation/index1";
    }

}

自定义错误处理方法(Helper类)

public static List<FieldError> customErrorMessageOrder(List<FieldError> listErrors){
        var notEmptyListErrors = listErrors
                                    .stream()
                                    .filter(e -> e.getCode().equals("NotEmpty"))
                                    .collect(Collectors.toList());  
        if(notEmptyListErrors.size()==0) {
            return listErrors;
        }
        
        var alterListErrors = listErrors.stream().collect(Collectors.toList());
        var newListErrors = notEmptyListErrors;
        
        for(var e : notEmptyListErrors) {
            alterListErrors.removeIf(e1->e1.getField().equals(e.getField()));
        }
        
        newListErrors.addAll(alterListErrors);
        
        return newListErrors;
}

前端Thymeleaf表单代码

<form th:action="@{/validation/resgister}" method="post" th:object="${student}">
        <table border="1">
            <tr>
                <td>Username</td>
                <td><input type="text" th:field="*{username}"></td>
                <td th:if="${#fields.hasErrors('username')}">
                    <span th:errors="*{username}"></span>
                </td>
            </tr>       
            
            <tr>
                <td>Password</td>
                <td><input type="text" th:field="*{password}"></td>
                <td th:if="${#fields.hasErrors('password')}">
                    <span th:errors="*{password}"></span>
                </td>
            </tr>   

            <tr>
                <td>Email</td>
                <td><input type="text" th:field="*{email}"></td>
                <td th:if="${#fields.hasErrors('email')}">
                    <span th:errors="*{email}"></span>
                </td>
            </tr>
            
            <tr>
                <td>Website</td>
                <td><input type="text" th:field="*{website}"></td>
                <td th:if="${#fields.hasErrors('website')}">
                    <span th:errors="*{website}"></span>
                </td>
            </tr>   

            <tr>
                <td>Age</td>
                <td><input type="text" th:field="*{age}"></td>
                <td th:if="${#fields.hasErrors('age')}">
                    <span th:errors="*{age}"></span>
                </td>
            </tr>

            <tr>
                <td colspan="2" style="text-align: center;"><input type="submit" value="submit"></td>
            </tr>
        </table>
</form>

问题详情

已尝试在Controller中重新构建BindingResult,打印显示处理后的错误列表是正确的,但前端仍会显示该字段的所有错误信息,比如username字段同时出现@NotEmpty和@Length的提示,不符合需求。


解决方案

核心问题分析

你重新创建了BeanPropertyBindingResult对象,但这个新对象并没有被Spring MVC传递到视图中——方法内部重新赋值的bindingResult变量只是局部变量,Spring MVC使用的是方法参数中传入的原始BindingResult实例。

修正Controller逻辑

不要重新创建BindingResult,直接修改原始实例:清除所有错误后,添加过滤后的错误列表。

@RequestMapping(value = { "resgister" }, method = RequestMethod.POST)
public String resgister(ModelMap modelMap, RedirectAttributes redirectAttributes, @ModelAttribute("student") @Valid Student student, BindingResult bindingResult) {
    if(bindingResult.hasErrors()) {
        List<FieldError> filteredErrors = Helper.customErrorMessageOrder(bindingResult.getFieldErrors());
        // 清除原始错误
        bindingResult.getAllErrors().clear();
        // 添加过滤后的错误
        filteredErrors.forEach(bindingResult::addError);
        return "/validation/index";
    }
    redirectAttributes.addFlashAttribute("message", "Success");
    return "redirect:/validation/index1";
}

优化错误过滤方法

简化customErrorMessageOrder方法,直接保留目标错误:

public static List<FieldError> customErrorMessageOrder(List<FieldError> listErrors){
    // 收集所有触发NotEmpty错误的字段名
    Set<String> notEmptyFields = listErrors.stream()
            .filter(e -> "NotEmpty".equals(e.getCode()))
            .map(FieldError::getField)
            .collect(Collectors.toSet());
    
    // 过滤规则:触发NotEmpty的字段只保留NotEmpty错误,其他字段保留所有错误
    return listErrors.stream()
            .filter(e -> !notEmptyFields.contains(e.getField()) || "NotEmpty".equals(e.getCode()))
            .collect(Collectors.toList());
}

这样处理后,当字段触发@NotEmpty时,只会显示该错误;未触发@NotEmpty的字段则正常显示所有校验错误,完全符合需求。

内容的提问来源于stack exchange,提问作者FalAn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 21:07:31