1、创建约束目标 常见数据验证中,非空、长度等已经有内置的验证器,而对于枚举类型数据值的合法性约束缺失一个合适的方式,选择通过自定义验证器+注解约束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public enum CaseMode { UPPER(0 ), LOWER(1 ), ; private final int code; CaseMode(int code) { this .code = code; } public int getCode () { return code; } public static boolean isValid (int other) { for (CaseMode cas : CaseMode.values()) { if (cas.code == other) { return true ; } } return false ; } }
2、定义约束注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import cn.probiecoder.springjavademo.validator.CaseValidator;import jakarta.validation.Constraint;import jakarta.validation.Payload;import java.lang.annotation.*;@Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = CaseValidator.class) @Documented public @interface ValidCase { String message () default "{com.mycompany.constraints.checkcase}" ; Class<?>[] groups() default {}; Class<? extends Payload >[] payload() default {}; }
message:必须,定义默认消息模板,约束条件验证失败的时候,通过此属性来输出错误信息
groups:必须,指定约束条件属于哪些校验组,可以使用字符串或者一个空的无定义的interface
,建议采用 interface
;
validator.validate()
不指定group
时,
payload:必须,Bean Validation API
的使用者可以通过此属性来给约束条件指定严重级别. 这个属性并不被API自身所使用,或者增加额外的元数据信息
3、定义约束校验器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import cn.probiecoder.springjavademo.annotations.ValidCase;import cn.probiecoder.springjavademo.enums.CaseMode;import jakarta.validation.ConstraintValidator;import jakarta.validation.ConstraintValidatorContext;public class CaseValidator implements ConstraintValidator <ValidCase, Integer> { @Override public void initialize (ValidCase constraintAnnotation) { } @Override public boolean isValid (Integer value, ConstraintValidatorContext context) { if (value == null ) { return true ; } return CaseMode.isValid(value); } }
实现 ConstraintValidator
接口,继承 isValid
定义验证规则
initialize
方法为接口默认实现方法,如果不需要在验证前进行初始化,可以不实现
isValid
验证方法,验证逻辑需要在这里实现
对于null
值,Bean Validation
推荐按照合法的值进行处理,如果值非空通过 @NotNull
或者 @NotBlank
验证。那么如果需要缺省值有什么好方法呢?
4、示例 4.1 所有属性验证采用默认group 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import cn.probiecoder.springjavademo.annotations.ValidCase;import jakarta.validation.constraints.Min;import jakarta.validation.constraints.NotBlank;import jakarta.validation.constraints.Size;public class CaseModeDTO { @NotBlank private String a; @Negative private Integer b; @Size(min=1, max = 10) private String c; @Min(3) private String d; @ValidCase private Integer mode; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 try (var factory = Validation.buildDefaultValidatorFactory()) { var validator = factory.getValidator(); var dto = new CaseModeDTO (); dto.setA("a" ); dto.setB(-3 ); dto.setC("CCC" ); dto.setD(2 ); dto.setMode(CaseMode.LOWER.getCode()); var violations = validator.validate(dto); violations.forEach(System.out::println); } 属性D要求最小为3 ,设置为2 ,不符合验证规则,消息输出 must be greater than or equal to 3
4.2 属性指定不同 group,验证默认group(不指定group) 1 2 3 4 5 6 7 8 9 10 11 12 public class CaseModeDTO { @NotBlank private String a; @Negative(groups = InsertChecks.class) private Integer b; @Size(min=1, max = 10, groups = InsertChecks.class) private String c; @Min(value = 3, groups = UpdateChecks.class) private Integer d; @ValidCase(groups = UpdateChecks.class) private Integer mode; }
1 2 3 4 5 6 7 8 9 10 11 try (var factory = Validation.buildDefaultValidatorFactory()) { var validator = factory.getValidator(); var dto = new CaseModeDTO (); dto.setB(3 ); dto.setC("CCC" ); dto.setD(4 ); dto.setMode(2 ); var violations = validator.validate(dto); violations.forEach(System.out::println); }
输出结果:
1 2 3 4 ConstraintViolationImpl{interpolatedMessage='must be less than 0' , propertyPath=b, rootBeanClass=class cn .probiecoder.springjavademo.dto.CaseModeDTO, messageTemplate='{jakarta.validation.constraints.Negative.message}' } ConstraintViolationImpl{interpolatedMessage='must not be blank' , propertyPath=a, rootBeanClass=class cn .probiecoder.springjavademo.dto.CaseModeDTO, messageTemplate='{jakarta.validation.constraints.NotBlank.message}' }
验证时不指定group
,自定义的验证器未校验,内置的验证器被执行。
4.3 属性c和mode指定group,验证指定group 1 2 3 4 5 6 7 8 9 10 11 12 public class CaseModeDTO { @NotBlank private String a; @Negative(groups = InsertChecks.class) private Integer b; @Size(min=1, max = 10, groups = UpdateChecks.class) private String c; @Min(value = 3, groups = UpdateChecks.class) private Integer d; @ValidCase(groups = UpdateChecks.class) private Integer mode; }
1 2 3 4 5 6 7 8 9 10 try (var factory = Validation.buildDefaultValidatorFactory()) { var validator = factory.getValidator(); var dto = new CaseModeDTO (); dto.setC("CCC" ); dto.setD(4 ); dto.setMode(2 ); var violations = validator.validate(dto, UpdateChecks.class); violations.forEach(System.out::println); }
输出结果与期望相同:只验证了 group=UpdateChecks.class
,mode
不符合枚举值,被拦截,返回配置的默认消息
1 ConstraintViolationImpl{interpolatedMessage='Invalid case mode', propertyPath=mode, rootBeanClass=class cn.probiecoder.springjavademo.dto.CaseModeDTO, messageTemplate='Invalid case mode'}
5、错误消息配置 5.1 在配置message时,通过default直接指定固定的错误消息 1 String message () default "case mode is invalid" ;
配置简单,灵活性不强,需要国际化时不能很好处理
5.2 通过消息模板配置 通过 {}
包括的内容会作为消息模板索引进行检索,默认的解析器(MessageInterpolator
)解析顺序如下:
先在类路径下查找名称为ValidationMessages.properties
的ResourceBundle
,然后将占位符和这个文件中定义的resource进行匹配
匹配Hibernate Validator自带的位于/org/hibernate/validator/ValidationMessages.properties
的ResourceBundle
1 String message () default "{jakarta.validation.constraints.Size.message}" ;
5.3 Spring Boot 国际化消息配置 5.3.1 全局异常拦截处理 Spring Boot
默认错误处理器(DefaultHandlerExceptionResolver
)会按照400 Bad Request
处理错误异常,但是无具体错误消息,对于客户端来说无实际意义,服务端需要将错误信息进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package cn.probiecoder.springjavademo.errorhandler;import jakarta.validation.ConstraintViolation;import jakarta.validation.ConstraintViolationException;import org.springframework.context.support.DefaultMessageSourceResolvable;import org.springframework.http.HttpStatus;import org.springframework.validation.BindException;import org.springframework.web.bind.MethodArgumentNotValidException;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseStatus;import org.springframework.web.bind.annotation.RestControllerAdvice;import java.util.stream.Collectors;@RestControllerAdvice public class GlobalExceptionHandler { @ResponseStatus(HttpStatus.OK) @ExceptionHandler(BindException.class) public String bindExceptionHandler (BindException e) { String message = e.getBindingResult().getAllErrors().stream() .map(DefaultMessageSourceResolvable::getDefaultMessage) .collect(Collectors.joining()); return "{\"errors\":\"" + message + "\"}" ; } @ResponseStatus(HttpStatus.OK) @ExceptionHandler(MethodArgumentNotValidException.class) public String methodArgumentNotValidExceptionHandler (MethodArgumentNotValidException e) { String message = e.getBindingResult().getAllErrors().stream() .map(DefaultMessageSourceResolvable::getDefaultMessage) .collect(Collectors.joining()); return "{\"errors\":\"" + message + "\"}" ; } @ResponseStatus(HttpStatus.OK) @ExceptionHandler(ConstraintViolationException.class) public String constraintViolationExceptionHandler (ConstraintViolationException e) { String message = e.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining()); return "{\"errors\":\"" + message + "\"}" ; } }
5.3.2 自定义验证器Validator指定文件位置和命名 此处需要覆盖 MessageResource
来指定具体的message
文件,hibernate
默认在类路径下查找ValidationMessages.properties
文件,如果不需要指定其他message
文件或不和业务错误message
统一,此处也可以不覆写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package cn.probiecoder.springjavademo.config;import jakarta.validation.Validator;import org.springframework.context.MessageSource;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.support.ReloadableResourceBundleMessageSource;import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;import java.nio.charset.StandardCharsets;@Configuration public class MessageHandlerConfig { @Bean public MessageSource messageSource () { ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource (); messageSource.setBasename("classpath:messages" ); messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name()); return messageSource; } @Bean public Validator getValidator () throws Exception { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean (); validator.setValidationMessageSource(messageSource()); return validator; } @Bean public MethodValidationPostProcessor validationPostProcessor () throws Exception { MethodValidationPostProcessor processor = new MethodValidationPostProcessor (); processor.setValidator(getValidator()); return processor; } }
5.3.3 指定全局验证 此处一定需要进行全局异常拦截处理 ConstraintViolationException
,否则Spring Boot
默认返回400
且无具体的错误原因
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package cn.probiecoder.springjavademo.validator;import jakarta.annotation.Resource;import jakarta.validation.ConstraintViolation;import jakarta.validation.ConstraintViolationException;import jakarta.validation.Validator;import jakarta.validation.constraints.NotNull;import jakarta.validation.groups.Default;import org.springframework.stereotype.Component;import java.util.Set;@Component public class AppValidator { @Resource private Validator validator; public <T> void validate (@NotNull T object) { this .validate(object, Default.class); } public <T> void validate (@NotNull T object, @NotNull Class<?>... groups) { Set<ConstraintViolation<T>> violations = this .validator.validate(object, groups); if (!violations.isEmpty()) { throw new ConstraintViolationException (violations); } } }
5.3.4 验证 1 2 3 4 appValidator.validate(caseModeDTO); appValidator.validate(caseModeDTO, UpdateChecks.class);
参考:
1、https://juejin.cn/post/6979165353481863182
2、https://gist.github.com/aoudiamoncef/9eeece142d1ef0faa4d06216a41282a2
3、https://docs.jboss.org/hibernate/validator/4.2/reference/zh-CN/html/