
在开发基于jakarta bean validation的自定义验证逻辑时,我们通常会定义一个注解(如@validsmstextlength)及其对应的constraintvalidator实现(如smstextlengthvalidator)。这个验证器负责对特定业务规则进行校验,并在不满足条件时通过constraintvalidatorcontext.buildconstraintviolationwithtemplate().addconstraintviolation()添加自定义错误消息。
然而,一个常见的问题是,当自定义验证器触发并添加了其特定的错误消息后,BindingResult中却出现了两条错误:
这种双重错误报告导致验证结果冗余,可能给前端展示或后端处理带来困扰。
示例场景:
考虑一个SmsMessageDto对象,它包含text和encoding字段,并使用@ValidSmsTextLength进行验证。
// SmsMessageDto.java
@ValidSmsTextLength(groups = { PostGroup.class, PatchGroup.class, PostMessageCampaignGroup.class })
@JsonDeserialize(as = SmsMessageDto.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({"text", "encoding", "messagePartCount", "length"})
public class SmsMessageDto extends AbstractRestDto implements OneOfMessage {
@NotEmpty(message = "SMS_TEXT_NULL_OR_EMPTY", groups = { PostGroup.class, PatchGroup.class })
@JsonProperty("text")
private String text = null;
@ValidParameterByEnum(enumValid = EncodingEnum.class, message = "INVALID_ENCODING_ENUM", groups = {PostGroup.class, PostMessageCampaignGroup.class})
@JsonProperty("encoding")
private EncodingEnum encoding = EncodingEnum.GSM7;
// ... 其他字段和方法
}
// ValidSmsTextLength.java
@Constraint(validatedBy = SmsTextLengthValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidSmsTextLength {
String message() default "DEFAULT_SMS_TEXT_LENGTH_MESSAGE"; // 默认错误消息
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// SmsTextLengthValidator.java (原始实现)
public class SmsTextLengthValidator implements ConstraintValidator<ValidSmsTextLength, SmsMessageDto> {
private static final String TEXT = "text";
@Override
public boolean isValid(SmsMessageDto smsMessageDto, ConstraintValidatorContext constraintValidatorContext) {
EncodingEnum encodingEnum = smsMessageDto.getEncoding();
if (smsMessageDto.getText() != null && EncodingEnum.GSM7.equals(encodingEnum) && smsMessageDto.getText().length() > 1530) {
constraintValidatorContext
.buildConstraintViolationWithTemplate("SMS_TEXT_LENGTH_GSM7_ERROR") // 自定义错误
.addPropertyNode(TEXT)
.addConstraintViolation();
return false;
} else if (smsMessageDto.getText() != null && EncodingEnum.UNICODE.equals(encodingEnum) && smsMessageDto.getText().length() > 670) {
constraintValidatorContext
.buildConstraintViolationWithTemplate("SMS_TEXT_LENGTH_UNICODE_ERROR") // 自定义错误
.addPropertyNode(TEXT)
.addConstraintViolation();
return false;
}
return true;
}
}当SmsMessageDto的text字段满足GSM7编码且长度超过1530时,BindingResult将包含两个错误:一个默认的DEFAULT_SMS_TEXT_LENGTH_MESSAGE错误,作用于body字段;另一个是自定义的SMS_TEXT_LENGTH_GSM7_ERROR错误,作用于body.text字段。
造成这种重复错误的原因在于Bean Validation框架的默认行为。当一个自定义ConstraintValidator被触发时,框架会首先生成一个基于注解message()属性的默认错误。如果isValid()方法内部又通过ConstraintValidatorContext手动添加了新的错误,那么这两个错误都会被收集。
要解决这个问题,我们需要明确告诉验证框架,当前验证器将完全负责错误消息的生成,不再需要自动添加默认错误。这可以通过调用ConstraintValidatorContext接口提供的disableDefaultConstraintViolation()方法来实现。
方法说明:
constraintValidatorContext.disableDefaultConstraintViolation(): 此方法指示验证框架禁用当前验证器关联的默认约束违规。一旦调用,验证器将不再自动生成基于注解message()属性的错误。之后,所有的错误报告都必须通过buildConstraintViolationWithTemplate()方法手动添加。
将disableDefaultConstraintViolation()方法添加到isValid()方法的开头,确保在任何自定义错误被添加之前,默认错误生成机制就被禁用。
// SmsTextLengthValidator.java (优化后,仅解决默认错误问题)
public class SmsTextLengthValidator implements ConstraintValidator<ValidSmsTextLength, SmsMessageDto> {
private static final String TEXT = "text";
@Override
public boolean isValid(SmsMessageDto smsMessageDto, ConstraintValidatorContext constraintValidatorContext) {
// 禁用默认错误消息,确保只报告自定义错误
constraintValidatorContext.disableDefaultConstraintViolation();
EncodingEnum encodingEnum = smsMessageDto.getEncoding();
if (smsMessageDto.getText() != null && EncodingEnum.GSM7.equals(encodingEnum) && smsMessageDto.getText().length() > 1530) {
constraintValidatorContext
.buildConstraintViolationWithTemplate("SMS_TEXT_LENGTH_GSM7_ERROR")
.addPropertyNode(TEXT)
.addConstraintViolation();
return false;
} else if (smsMessageDto.getText() != null && EncodingEnum.UNICODE.equals(encodingEnum) && smsMessageDto.getText().length() > 670) {
constraintValidatorContext
.buildConstraintViolationWithTemplate("SMS_TEXT_LENGTH_UNICODE_ERROR")
.addPropertyNode(TEXT)
.addConstraintViolation();
return false;
}
return true;
}
}通过上述修改,当SmsTextLengthValidator执行时,如果触发了自定义错误条件,BindingResult将只包含由buildConstraintViolationWithTemplate()生成的特定错误,而不会再出现默认错误消息。
除了解决重复错误的问题,自定义ConstraintValidator还需要考虑一个重要的健壮性问题:被验证的对象(在本例中是smsMessageDto)在isValid()方法执行时,可能尚未通过@NotNull等基础注解的验证,因此它本身可能是null。
如果smsMessageDto为null,直接调用smsMessageDto.getEncoding()或smsMessageDto.getText()将会导致NullPointerException。为了避免这种情况,我们应该在isValid()方法的开头添加一个null检查。
最佳实践:添加null检查
// 示例:在isValid方法开头添加null检查
public class SmsTextLengthValidator implements ConstraintValidator<ValidSmsTextLength, SmsMessageDto> {
// ...
@Override
public boolean isValid(SmsMessageDto smsMessageDto, ConstraintValidatorContext constraintValidatorContext) {
// 1. 处理null对象情况
// 如果对象为null,通常由@NotNull等更基础的注解处理。
// 当前验证器不应重复处理null情况,返回true表示它不对null负责。
if (smsMessageDto == null) {
return true;
}
// 2. 禁用默认错误消息
constraintValidatorContext.disableDefaultConstraintViolation();
// 3. 执行具体的验证逻辑
// ... (原有的验证逻辑)
return true;
}
}通过这种方式,SmsTextLengthValidator将专注于其特定的业务逻辑验证,而基础的非空验证则由@NotNull等注解负责,从而提高了验证器的健壮性和职责分离。
结合禁用默认错误和处理空对象的最佳实践,最终的SmsTextLengthValidator实现如下:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
// 假设EncodingEnum和SmsMessageDto已在项目中定义
public class SmsTextLengthValidator implements ConstraintValidator<ValidSmsTextLength, SmsMessageDto> {
private static final String TEXT = "text";
@Override
public boolean isValid(SmsMessageDto smsMessageDto, ConstraintValidatorContext constraintValidatorContext) {
// 1. 处理null对象情况:如果对象为null,通常由@NotNull等注解处理。
// 当前验证器不应重复处理null情况,返回true表示它不对null负责。
if (smsMessageDto == null) {
return true;
}
// 2. 禁用默认错误消息:确保只报告自定义错误,避免默认消息的干扰。
constraintValidatorContext.disableDefaultConstraintViolation();
EncodingEnum encodingEnum = smsMessageDto.getEncoding();
String text = smsMessageDto.getText();
// 3. 执行具体的验证逻辑
// 同时考虑text字段本身可能为null的情况,避免NullPointerException
if (text != null) {
if (EncodingEnum.GSM7.equals(encodingEnum) && text.length() > 1530) {
constraintValidatorContext
.buildConstraintViolationWithTemplate("SMS_TEXT_LENGTH_GSM7_ERROR")
.addPropertyNode(TEXT)
.addConstraintViolation();
return false;
} else if (EncodingEnum.UNICODE.equals(encodingEnum) && text.length() > 670) {
constraintValidatorContext
.buildConstraintViolationWithTemplate("SMS_TEXT_LENGTH_UNICODE_ERROR")
.addPropertyNode(TEXT)
.addConstraintViolation();
return false;
}
}
// 如果所有条件都通过,则验证成功
return true;
}
}遵循这些最佳实践,可以构建出更加健壮、清晰且专业的Jakarta Bean Validation自定义验证逻辑。
以上就是解决ConstraintValidator重复报告默认与自定义验证错误的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号