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

Spring Boot自定义字段匹配校验抛出ConstraintViolationException求助

解决Spring Boot持久化阶段触发字段匹配校验异常的问题

看起来你遇到的问题核心在于实体类上的类级校验在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上,校验通过后再将数据复制到实体类中。

  1. 创建用户注册表单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; }
}
  1. 修改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";
}
  1. 移除User实体类上的@FieldsValueMatch注解和con_password字段(因为已经不需要了)。

方案2:通过校验组控制校验触发时机

如果不想创建DTO,可以通过校验组来让@FieldsValueMatch只在表单提交时触发,而不在JPA持久化阶段触发:

  1. 定义一个表单校验组:
package com.joker.SampleAuthenticationWebApp.validator;

public interface FormValidationGroup {
}
  1. 修改User实体上的注解,指定校验组:
@Entity
@Table(name = "auth_user")
@FieldsValueMatch(field = "password", fieldMatch = "con_password", message = "Passwords do not match!", groups = FormValidationGroup.class)
public class User {
    // 实体原有内容不变
}
  1. 修改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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 10:02:55