
在使用jakarta bean validation进行数据验证时,我们经常会创建自定义注解和对应的constraintvalidator来实现复杂的业务逻辑验证。例如,针对短信内容长度的验证,我们可能定义一个@validsmstextlength注解,并为其指定一个默认的错误消息:
@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在isValid方法中根据短信编码和内容长度添加自定义的错误消息:
public class SmsTextLengthValidator implements ConstraintValidator<ValidSmsTextLength, SmsMessageDto> {
private static final String TEXT = "text";
@Override
public boolean isValid(SmsMessageDto smsMessageDto, ConstraintValidatorContext constraintValidatorContext) {
// ... 假设 smsMessageDto 不为 null 且属性已初始化
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作为另一个对象(如AbstractMessageDto)的嵌套属性,并且AbstractMessageDto的该属性上标记了@Valid注解时,问题就可能出现。例如:
public class AbstractMessageDto extends AbstractRestResourceDto {
@JsonDeserialize(using = OneOfMessageDtoDeserializer.class)
@Valid // 触发嵌套对象的验证
@NotNull(message = "MESSAGE_NULL", groups = PostGroup.class)
@JsonProperty("body")
protected OneOfMessage body = null; // OneOfMessage 的实现类可以是 SmsMessageDto
}当SmsMessageDto中的内容触发了SmsTextLengthValidator中的自定义错误条件时,BindingResult中会同时出现两条错误:
这种重复的错误信息会造成混淆,降低API响应的清晰度。我们通常只希望在自定义验证器触发时,显示我们精确定义的错误信息。
为了避免BindingResult中出现注解的默认错误消息,我们可以在ConstraintValidator的isValid方法内部,在添加任何自定义错误之前,调用ConstraintValidatorContext的disableDefaultConstraintViolation()方法。
disableDefaultConstraintViolation()方法的作用是阻止Bean Validation自动为当前验证上下文添加基于约束注解message()属性的默认错误。一旦调用,后续添加的任何错误都将是自定义的错误。
下面是修改后的SmsTextLengthValidator示例:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class SmsTextLengthValidator implements ConstraintValidator<ValidSmsTextLength, SmsMessageDto> {
private static final String TEXT = "text";
@Override
public void initialize(ValidSmsTextLength constraintAnnotation) {
// 可选:在此处进行初始化操作,例如获取注解参数
}
@Override
public boolean isValid(SmsMessageDto smsMessageDto, ConstraintValidatorContext constraintValidatorContext) {
// 核心步骤:禁用默认的错误消息
// 必须在添加任何自定义错误之前调用,以确保只报告自定义错误。
constraintValidatorContext.disableDefaultConstraintViolation();
// 假设 smsMessageDto 不为 null,该情况将在下一节讨论
EncodingEnum encodingEnum = smsMessageDto.getEncoding();
String text = smsMessageDto.getText();
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; // 验证通过
}
}通过在isValid方法的开头调用constraintValidatorContext.disableDefaultConstraintViolation(),当验证条件不满足时,BindingResult将只包含由buildConstraintViolationWithTemplate创建的自定义错误,而不会再出现默认的"DEFAULT_SMS_TEXT_LENGTH_MESSAGE"错误。
在自定义ConstraintValidator中,还需要注意一个潜在的NullPointerException风险。尽管在父对象上可能已经有了@NotNull这样的注解来确保SmsMessageDto对象本身不为null,但在某些复杂的验证流程或特定的调用时机下,isValid方法接收到的smsMessageDto参数仍有可能为null。如果此时不进行null检查就直接访问其属性(如smsMessageDto.getEncoding()),就会导致NullPointerException。
这是因为Bean Validation的验证顺序不总是严格按照预期。一个自定义的ConstraintValidator可能在@NotNull验证之前被调用,或者在组合验证场景下,null值可能以某种方式传递进来。
因此,在isValid方法的开始处添加一个null检查是一个良好的实践,可以显著提高验证器的健壮性:
public class SmsTextLengthValidator implements ConstraintValidator<ValidSmsTextLength, SmsMessageDto> {
// ...
@Override
public boolean isValid(SmsMessageDto smsMessageDto, ConstraintValidatorContext constraintValidatorContext) {
// 1. 健壮性检查:处理待验证对象为null的情况
if (smsMessageDto == null) {
// 如果smsMessageDto为null,通常意味着@NotNull等其他验证器会处理此情况。
// 此处返回true,表示该自定义验证器不对null对象强制失败,避免NPE。
// 如果需要,也可以在此处添加一个特定的null错误。
return true;
}
// 2. 核心:禁用默认的错误消息
constraintValidatorContext.disableDefaultConstraintViolation();
EncodingEnum encodingEnum = smsMessageDto.getEncoding();
String text = smsMessageDto.getText();
// 3. 验证逻辑
if (text != null) { // 进一步检查text是否为null,因为@NotEmpty可能已处理
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;
}
}在null检查中返回true的策略是基于“单一职责”原则:如果对象本身是null,这通常是@NotNull或类似注解的职责。自定义验证器更专注于验证非null对象的内部状态。
通过上述改进,我们的ConstraintValidator变得更加精确和健壮:
在设计自定义ConstraintValidator时,始终牢记以下几点:
遵循这些实践,可以帮助我们构建出高效、清晰且易于维护的Jakarta Bean Validation验证逻辑。
以上就是优化ConstraintValidator:避免默认与自定义错误并存的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号