Spring Boot自定义字段匹配校验抛出ConstraintViolationException求助
看起来你遇到的问题核心在于实体类上的类级校验在JPA持久化阶段被重复触发,而此时con_password字段因为@Transient注解的原因,值已经无法被正确获取,导致校验失败抛出ConstraintViolationException。
问题根源分析
你的User实体类上标注了@FieldsValueMatch类级校验注解,同时con_password字段加了@Transient——这个注解告诉JPA该字段不需要映射到数据库,在实体被持久化(比如调用userRepository.save())的过程中,这个字段的值会被清空或者无法被Bean Validation框架获取到。这时候校验器执行时,password有值但con_password为null,自然触发「Passwords do not match!」的约束违例。
而你说提交表单前功能正常,是因为表单提交时的校验是在Controller层对实体进行校验,此时con_password还持有用户输入的值,所以校验能正常通过。
解决方案
方案1:分离表单DTO与实体类(推荐最佳实践)
不要直接用JPA实体类作为表单提交的接收对象,专门创建一个用于表单校验的DTO(数据传输对象),把校验注解放在DTO上,校验通过后再将数据复制到实体类中。
- 创建用户注册表单DTO:
package com.joker.SampleAuthenticationWebApp.dto; import com.joker.SampleAuthenticationWebApp.validator.FieldsValueMatch; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.Email; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; @FieldsValueMatch(field = "password", fieldMatch = "con_password", message = "Passwords do not match!") public class UserRegistrationForm { @NotNull @NotEmpty(message = "First name is compulsory") private String firstname; @NotNull @NotEmpty(message = "Last name is compulsory") private String lastname; @NotNull @NotEmpty(message = "Email is compulsory") @Email private String email; @NotNull private String phone; @NotNull @NotEmpty(message = "Password is compulsory") @Length(min = 5, message = "Password length should be at least 5 characters") private String password; @NotEmpty(message = "Confirm Password field is compulsory") private String con_password; @NotNull private int enabled; // Getters and Setters public String getFirstname() { return firstname; } public void setFirstname(String firstname) { this.firstname = firstname; } public String getLastname() { return lastname; } public void setLastname(String lastname) { this.lastname = lastname; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getCon_password() { return con_password; } public void setCon_password(String con_password) { this.con_password = con_password; } public int getEnabled() { return enabled; } public void setEnabled(int enabled) { this.enabled = enabled; } }
- 修改Controller接收DTO并处理:
@PostMapping("/register") public String registerUser(@Valid @ModelAttribute UserRegistrationForm registrationForm, BindingResult result) { if (result.hasErrors()) { // 校验失败,返回表单页面显示错误 return "register"; } // 将DTO数据复制到User实体 User user = new User(); user.setFirstname(registrationForm.getFirstname()); user.setLastname(registrationForm.getLastname()); user.setEmail(registrationForm.getEmail()); user.setPhone(registrationForm.getPhone()); user.setPassword(registrationForm.getPassword()); user.setEnabled(registrationForm.getEnabled()); // 处理roles字段,根据业务逻辑赋值 // 持久化实体 userRepository.save(user); return "redirect:/login"; }
- 移除
User实体类上的@FieldsValueMatch注解和con_password字段(因为已经不需要了)。
方案2:通过校验组控制校验触发时机
如果不想创建DTO,可以通过校验组来让@FieldsValueMatch只在表单提交时触发,而不在JPA持久化阶段触发:
- 定义一个表单校验组:
package com.joker.SampleAuthenticationWebApp.validator; public interface FormValidationGroup { }
- 修改
User实体上的注解,指定校验组:
@Entity @Table(name = "auth_user") @FieldsValueMatch(field = "password", fieldMatch = "con_password", message = "Passwords do not match!", groups = FormValidationGroup.class) public class User { // 实体原有内容不变 }
- 修改Controller,校验时指定使用
FormValidationGroup:
import org.springframework.validation.annotation.Validated; @PostMapping("/register") public String registerUser(@Validated(FormValidationGroup.class) @ModelAttribute User user, BindingResult result) { if (result.hasErrors()) { return "register"; } userRepository.save(user); return "redirect:/login"; }
默认情况下,JPA在持久化时只会触发Default组的校验,而我们的@FieldsValueMatch属于FormValidationGroup,所以持久化阶段不会执行这个校验,也就不会出现异常了。
额外优化建议
你的校验器中直接忽略了异常,这可能会隐藏其他潜在问题,建议添加日志记录:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class FieldsValueMatchValidator implements ConstraintValidator<FieldsValueMatch, Object> { private static final Logger log = LoggerFactory.getLogger(FieldsValueMatchValidator.class); // ... 原有代码 @Override public boolean isValid(Object value, ConstraintValidatorContext context) { try { // 原有校验逻辑 } catch (final Exception e) { log.error("Failed to validate field match", e); return false; } return true; } }
内容的提问来源于stack exchange,提问作者CodeHealer

