1、引言
我们在对外提供接口时,不管是从实际的业务需求,还是系统的安全性出发,通常都需要对接口的入参做一些合法性验证。
这些验证通常包括非空、字符串长度、数组大小,格式等等,在以前,我们都是通过手动实现类似的代码,于是,代码中充斥着很多if、else、StringUtils.isEmpty、CollectionUtils.isEmpty...类似的代码。
通过手动的方式来实现这些校验逻辑,它确实存在一定的优点,比如:灵活,无论什么样的校验逻辑,都可以直接翻译成代码实现;但同时,它也存在很多缺点:
- 编码工作量大,手动实现方式太繁琐;
- 容易出错;新写的代码越多,越容易出bug,越需要花时间去验证其正确性;
其实,Java生态提供了一套标准JSR-380,它已成为“对象验证”事实上的标准。基于这套标准,我们只需要使用@NotNull、@NotEmpty、@Size、@Range、@Pattern类似的注解,标注需要校验的参数,就可以完成对参数的校验。
Hibernate Validator实现了上述的标准;同时,SpringBoot也无缝集成了Hibernate Validator、自定义验证器、自动验证的功能。让我们在进行参数验证的时候,更加方便,高效。
下文我们将从以下三个方面来介绍参数校验:
- 如何使用Hibernate Validator来校验参数;
- SpringBoot集成Hibernate Validator;
- 参数校验的高阶方式,如:如何自定义验证器、验证分组等等;
2、Hibernate Validator
2.1 如何使用Hibernate Validator来校验参数
(1)添加Maven依赖
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.5.Final</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.0</version>
</dependency>
(2)定义实体类,并对需要校验的属性添加相应的注解
public class User {
@NotBlank(message = "用户名称不能为空")
private String name;
@Email(message = "用户邮箱格式不合法")
private String email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
(3)校验实体的合法性
public class Main {
public static void main(String[] args) {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
User user = new User();
Set<ConstraintViolation<User>> violations = validator.validate(user);
if(violations.size() > 0){
violations.forEach(System.out::println);
}
}
}
2.2 JSR-380支持的注解
JSR-380支持的注解汇总如下
2.2.1 空 or 非空
注解 | 适用对象 | null是否验证通过 | 说明 |
@NotNull | 所有对象 | No | 不是null |
@NotEmpty | CharSequence, Collection, Map, Array | No | 不是null、不是""、size>0 |
@NotBlank | CharSequence | No | 不是null、trim后长度大于0 |
@Null | 所有对象 | Yes | 是null |
2.2.2 布尔值
注解 | 适用对象 | null是否验证通过 | 说明 |
@AssertTrue | boolean | Yes | 必须是true |
@AssertFalse | boolean | Yes | 必须是false |
2.2.3 长度
注解 | 适用对象 | null是否验证通过 | 说明 |
@Size(min=0, max=Integer.MAX_VALUE) | CharSequence, Collection, Map, Array | Yes | 字符串长度、集合size |
2.2.4 数值大小
注解 | 适用对象 | null是否验证通过 | 说明 |
@Positive | BigDecimal, BigInteger, byte, short, int, long, float, double | Yes | 数字>0 |
@PositiveOrZero | BigDecimal, BigInteger, byte, short, int, long, float, double | Yes | 数字>=0 |
@Negative | BigDecimal, BigInteger, byte, short, int, long, float, double | Yes | 数字<0 |
@NegativeOrZero | BigDecimal, BigInteger, byte, short, int, long, float, double | Yes | 数字<=0 |
@Min(value=0L) | BigDecimal, BigInteger, byte, short, int, long | Yes | 数字>=min.value |
@Max(value=0L) | BigDecimal, BigInteger, byte, short, int, long | Yes | 数字<=max.value |
@Range(min=0L, max=Long.MAX_VALUE) | BigDecimal, BigInteger, byte, short, int, long | Yes | range.min<=数字<=range.max |
@DecimalMin(value="") | BigDecimal, BigInteger, CharSequence, byte, short, int, long | Yes | 数字>=decimalMin.value |
@DecimalMax(value="") | BigDecimal, BigInteger, CharSequence, byte, short, int, long | Yes | 数字<=decimalMax.value |
2.2.5 日期、时间
注解 | 适用对象 | null是否验证通过 | 说明 |
@Past | java.util.Date java.util.Calendar java.time.Instant java.time.LocalDate java.time.LocalDateTime java.time.LocalTime java.time.MonthDay java.time.OffsetDateTime java.time.OffsetTime java.time.Year java.time.YearMonth java.time.ZonedDateTime java.time.chrono.HijrahDate java.time.chrono.JapaneseDate java.time.chrono.MinguoDate java.time.chrono.ThaiBuddhistDate | Yes | 时间在当前时间之前 |
@PastOrPresent | 同上 | Yes | 时间在当前时间之前 或者 等于此时 |
@Future | 同上 | Yes | 时间在当前时间之后 |
@FutureOrPresent | 同上 | Yes | 时间在当前时间之后 或者 等于此时 |
2.2.6 格式
注解 | 适用对象 | null是否验证通过 | 说明 |
@Pattern(regexp="", flags={}) | CharSequence | Yes | 匹配正则表达式 |
@Email @Email(regexp=".*", flags={}) | CharSequence | Yes | 匹配邮箱格式 |
@Digts(integer=0, fraction=0) | BigDecimal, BigInteger, CharSequence, byte, short, int, long | Yes | 必须是数字类型,且满足整数位数<=digits.integer, 浮点位数<=digits.fraction |
3、SpringBoot集成Hibernate Validator
3.1 准备工作
(1)首先,我们需要创建一个SpringBoot项目,这个不是本文的重点,大家自行创建即可。
(2)接下来,添加相应的Maven依赖:
<!-- springboot集成validation的starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
(3)定义全局异常处理器:
使用@ControllerAdvice、@ExceptionHandler来对验证框架抛出的异常进行统一处理,并将错误信息拼接后统一返回,具体处理代码如下:
@RestControllerAdvice
@Slf4j
public class ExceptionHandlerAdvice {
/**
* 符号常量
*/
private final String SPLITOR = ", ";
private final String SEPARATOR = ": ";
/**
* RequestBody、Form对象参数 验证不通过时抛出的异常
* 需要在对应的参数上加 @Validated 注解
*
* @param e
* @return
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Response handleValidException(HttpServletRequest request, MethodArgumentNotValidException e) {
// 从异常对象中拿到ObjectError对象
String errorMsg = this.convertFiledErrors(e.getBindingResult().getFieldErrors());
log.warn("{} - 参数不合法,msg: {}", request.getServletPath(), errorMsg);
// 然后提取错误提示信息进行返回
return Response.fail(400, errorMsg);
}
/**
* 简单参数 验证不通过时抛出的异常
* 需要在 Controller 上加 @Validated 注解
*
* @param e
* @return
*/
@ExceptionHandler(value = ConstraintViolationException.class)
public Response handleConstraintViolationException(HttpServletRequest request, ConstraintViolationException e) {
// 从异常对象中拿到ObjectError对象
String errorMsg = this.convertConstraintViolations(e);
log.warn("{} - 参数不合法,msg: {}", request.getServletPath(), errorMsg);
return Response.fail(400, errorMsg);
}
@ExceptionHandler(value = BindException.class)
public Response handleBindException(HttpServletRequest request, BindException e) {
String errorMsg = convertFiledErrors(e.getFieldErrors());
log.warn("{} - 参数不合法,msg: {}", request.getServletPath(), errorMsg);
return Response.fail(400, errorMsg);
}
/**
* 入参反序列化为具体入参时产生的异常
*
* @param request
* @param e
* @return
*/
@ExceptionHandler(value = HttpMessageNotReadableException.class)
public Response handleBindException(HttpServletRequest request, HttpMessageNotReadableException e) {
log.warn("{} - 参数不合法,msg: {}", request.getServletPath(), "入参解析失败");
return Response.fail(400, "入参解析失败");
}
/**
* 全局默认异常处理
*
* @param request
* @param t
* @return
*/
@ExceptionHandler(value = Throwable.class)
public Response handleException(HttpServletRequest request, Throwable t) {
log.error("{} - Exception!", request.getServletPath(), t);
return Response.fail(500, "未知错误");
}
/**
* 转换FieldError列表为错误提示信息
*
* @param fieldErrors
* @return
*/
private String convertFiledErrors(List<FieldError> fieldErrors) {
return Optional.ofNullable(fieldErrors)
.map(fieldErrorsInner -> fieldErrorsInner.stream()
.flatMap(fieldError -> Stream.of(fieldError.getField(), SEPARATOR, fieldError.getDefaultMessage(), SPLITOR))
.collect(Collectors.joining()))
.map(msg -> msg.substring(0, msg.length() - SPLITOR.length()))
.orElse(null);
}
/**
* 转换ConstraintViolationException异常为错误提示信息
*
* @param e
* @return
*/
private String convertConstraintViolations(ConstraintViolationException e) {
return Optional.ofNullable(e.getConstraintViolations())
.map(violations -> violations.stream()
.flatMap(constraintViolation -> {
String path = constraintViolation.getPropertyPath().toString();
String errMsg = constraintViolation.getMessage();
return Stream.of(path, SEPARATOR, errMsg, SPLITOR);
}).collect(Collectors.joining())
).map(msg -> msg.substring(0, msg.length() - SPLITOR.length()))
.orElse("");
}
}
3.2 验证简单参数
@RestController
@Validated
public class ValidationController {
/**
* 对于不是封装类型的参数,需要在当前Controller上增加@Validated注解
*
* @param code
* @return
*/
@GetMapping("/valid-simple-param")
public Response valid(@RequestParam(value = "code", required = false)
@NotNull(message = "code不能为null")
@NotBlank(message = "code不能为blank")
@Size(min = 1, max = 6, message = "code的长度必须是1-6个字符") String code) {
return Response.success("World: " + code);
}
}
(1)使用JSR-380提供的注解标注对应的简单参数;(后面介绍的自定义验证注解也可以用在这里)
- @NotNull(message = "code不能为null"):表示code不能为null【@NotBlank其实已经包含了这一约束】;
- @NotBlank(message = "code不能为blank"):表示code不能为null、""、" ";
- @Size(min = 1, max = 6, message = "code的长度必须是1-6个字符"):表示code的长度必须是1-6个字符;
(2)当前Controller需要使用@Validated注解标注;
(3)校验不通过时,会抛出ConstraintViolationException;
3.3 验证RequestBody、Form对象参数
接口定义:
@RestController
public class ValidationController {
@PostMapping("/valid-entity-param")
public Response valid(@RequestBody @Validated ValidRequest request) {
return Response.success();
}
}
- 入参需要使用@Validated注解标注;
- 校验不通过时,会抛出MethodArgumentNotValidException异常;
入参实体定义:
这里列举了一些校验的案例,也添加了相应的注释,可以结合注释理解。
对于一些需要特殊说明的案例,下文会具体展开讲解。
@Data
public class ValidRequest {
@NotNull(message = "数字param1不能为null")
private Long param1;
/**
* @NotNull 字符串 不能为 null
* @NotEmpty 字符串 不能为 空串,如:""
* @NotBlank 字符串去除前后的空格 不能为 空串,如:""、" "
*/
@NotNull(message = "字符串param2不能为null")
@NotEmpty(message = "字符串param2不能为empty")
@NotBlank(message = "字符串param2不能为blank")
private String param2;
@NotNull(message = "字符串param3不能为空")
@Size(min = 6, max = 11, message = "字符串param3长度必须是6-11个字符")
private String param3;
/**
* @Pattern 只能用来校验String类型
*/
@NotNull(message = "type不能为null")
@Pattern(regexp = "[1|2|3]", message = "未知的类型")
private String type;
/**
* 手机号,8位 or 11位
*/
@NotBlank(message = "手机号必填")
@Pattern(regexp = "^\\d{8,11}#34;, message = "手机号不合法")
private String phone;
/**
* @Email 注解 只会在 email != null 时生效;<br/>
* 如果email不能为null,则需要加上 @NotNull 注解 <br/>
*
* @Email 注解中regexp属性,支持自定义email的正则表达式,regexp 表达式只能比默认的正则更严格;
* 参考代码:EmailValidator
*
*/
@NotNull(message = "邮箱不能为空")
@Email(regexp = "^(\\w)+(\\.\\w+)*@163.com#34;, message = "邮箱不合法")
// 该正则表达式无效
// @Email(regexp = "A|B|C|D", message = "邮箱不合法")
private String email;
/**
* yyyy-MM-dd
*/
@NotNull
@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}#34;, message = "生日不合法")
private String birthDateStr;
@Valid
private SubModel subModel;
@Valid
@NotEmpty
private List<SubModel> subModels;
@NotEmpty
private List<String> codes;
}
特殊说明:
(1)@NotNull、@NotEmpty、@NotBlank
@NotNull(message = "数字param1不能为null")
private Long param1;
/**
* @NotNull 字符串 不能为 null
* @NotEmpty 字符串 不能为 空串,如:""
* @NotBlank 字符串去除前后的空格 不能为 空串,如:""、" "
*/
@NotNull(message = "字符串param2不能为null")
@NotEmpty(message = "字符串param2不能为empty")
@NotBlank(message = "字符串param2不能为blank")
private String param2;
@NotEmpty
private List<String> codes;
- 前面2.2.1节已经说明,@NotEmpty、@NotBlank标注的属性,如果字段为null,也是会校验不通过的,因此,不需要再使用@NotNull注解标注。
1)对于一个注解,null是否验证通过,可查看2.2节中的表格
2)后面,对于自定义的注解,null是否验证通过,可以根据实际需求来自行实现。
- @NotEmpty还可用于标注Collection、Map、Array,表示集合不能为null、不能为空集合。
(2)@Pattern
/**
* @Pattern 只能用来校验String类型
*/
@NotNull(message = "type不能为null")
@Pattern(regexp = "[1|2|3]", message = "未知的类型")
private String type;
/**
* 手机号,8位 or 11位
*/
@NotBlank(message = "手机号必填")
@Pattern(regexp = "^\\d{8,11}#34;, message = "手机号不合法")
private String phone;
/**
* yyyy-MM-dd
*/
@NotNull
@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}#34;, message = "生日不合法")
private String birthDateStr;
- regexp属性用来指定字段需要满足的正则表达式;
- @Pattern 注解只能用来标注CharSequence类型的对象【参考 PatternValidator 类】;
如果标注了其他类型的参数,则会抛出如下异常:
这里分享一篇关于正则表达式的干货:
(3)@Email
/**
* @Email 注解 只会在 email != null 时生效;<br/>
* 如果email不能为null,则需要加上 @NotNull 注解 <br/>
*
* @Email 注解中regexp属性,支持自定义email的正则表达式,regexp 表达式只能比默认的正则更严格;
* 参考代码:EmailValidator
*
*/
@NotNull(message = "邮箱不能为空")
@Email(regexp = "^(\\w)+(\\.\\w+)*@163.com#34;, message = "邮箱不合法")
// 该正则表达式无效
// @Email(regexp = "A|B|C|D", message = "邮箱不合法")
private String email;
- @Email 注解只会在 email != null 时生效,如果email不能为null,则需要加上 @NotNull 注解;
- @Email 注解中的regexp属性,支持自定义email的正则表达式,用于定制化的email格式;
- @Email 注解在进行验证时,会先进行一个基本的email格式验证,验证合格的话,才会使用自定义的正则验证。这一点,可从EmailValidator的源码出看到:
(4)@Valid
@Valid
private SubModel subModel;
@Valid
@NotEmpty
private List<SubModel> subModels;
@Data
public class SubModel {
@NotBlank(message = "subParam1必填")
@Size(min = 1, max = 8, message = "subParam1在1-8之间")
private String subParam1;
@Positive(message = "subParam2必须为正数")
private Integer subParam2;
}
- 对于复杂对象的验证,需要在该字段上增加@Valid注解。
- 关于@Valid与@Validated的区别,可参考:@Valid与@Validated区别_wounler的博客-CSDN博客_validate和valid
4、参数校验的高阶方式
4.1 自定义校验注解
当现有的校验注解无法满足我们的业务需求时,我们可以自定义校验注解。
自定义校验注解有两种方式:
(1)将原有的多个校验注解组合成为一个新的校验注解,这样可以继承多个注解的能力;
(2)完全创建一个新的校验注解,来实现自定义的业务校验功能。
4.1.1 自定义组合注解
定义一个校验注解,用于校验手机号的合法性。
@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@Repeatable(PhoneNumber.List.class)
@Pattern(regexp = "^\\d{8,11}#34;, message = "手机号格式不正确")
@ReportAsSingleViolation
public @interface PhoneNumber {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* 语法糖,PhoneNumber 注解可以在某个元素上使用多次
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@interface List {
PhoneNumber[] value();
}
}
(1)自定义组合校验注解,需要使用@Constraint(validatedBy = {})标注;【注意:validatedBy必须设置为{}】
- (2)该功能可以基于@Pattern注解的功能做扩展;
- 使用了@Pattern(regexp = "^\\d{8,11}#34;, message = "手机号格式不正确") 修饰当前注解;
(3)@ReportAsSingleViolation
默认情况下,组合注解中的一个或多个子注解校验失败的情况下,会分别触发子注解各自错误报告,如果想要使用组合注解中定义的错误信息,则添加该注解。添加之后只要组合注解中有至少一个子注解校验失败,则会生成组合注解中定义的错误报告,子注解的错误信息被忽略。
JSR-380中定义的 @Range 注解其实就是一种自定义组合注解,有兴趣可以看下它的源码。
@Range 注解 组合了 @Min 和 @Max 注解的功能
@Documented
@Constraint(validatedBy = { })
@SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Min(0)
@Max(Long.MAX_VALUE)
@ReportAsSingleViolation
public @interface Range {
@OverridesAttribute(constraint = Min.class, name = "value") long min() default 0;
@OverridesAttribute(constraint = Max.class, name = "value") long max() default Long.MAX_VALUE;
String message() default "{org.hibernate.validator.constraints.Range.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
/**
* Defines several {@code @Range} annotations on the same element.
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface List {
Range[] value();
}
}
4.1.2 完全创建新的校验注解
完全创建新的校验注解,一般需要分如下几步:
(1)定义相应的注解,用于标注需要校验的参数;
(2)实现验证器,即具体的校验逻辑;
(3)在定义的注解上,使用元注解@Constraint(validatedBy = {})来关联具体的验证器实现;
下面我们来看一个具体的案例。
需求:文件的类型只能为三种类型:图片(1)、视频(2)、PDF(3)。
@Data
public class File {
private String fileId;
// 文件的类型只能为三种类型:图片(1)、视频(2)、PDF(3)
@DiscreteValue(values = {1, 2, 3}, message = "不支持的类型")
private int type;
}
定义注解类:
@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = {DiscreteValueValidator.class})
public @interface DiscreteValue {
// values 用于指定 字段 可以选择的值
int[] values() default {};
String message() default "未知的值";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
- @Documented、@Target、@Retention 这三个元注解不做过多解释
- @Constraint(validatedBy = {DiscreteValueValidator.class}):用于指定该注解对应的验证处理器注意:validatedBy是个数组,支持设置多个验证处理器;
- values 属性: 用于指定 字段 可以选择的值;
- message 属性:用于指定校验不通过时展示的消息;
- groups 属性:用于指定验证器分组,下文会讲到;
- payload 属性:具体作用暂不清楚,等后面再研究,但定义注解时,一般都需要定于该字段;
定义验证处理器:
public class DiscreteValueValidator implements ConstraintValidator<DiscreteValue, Integer> {
private Set<Integer> valueSet;
/**
* 用于初始化验证处理器,这里将@DiscreteValue中指定的values赋值到valueSet中
*
* @param constraintAnnotation
*/
@Override
public void initialize(DiscreteValue constraintAnnotation) {
this.valueSet = new HashSet<>();
for (int value : constraintAnnotation.values()) {
valueSet.add(value);
}
}
/**
* 真正的验证逻辑,验证valueSet中是否包含方法传入的value
*
* @param value
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return valueSet.contains(value);
}
}
附录中会给出一些自定义Validator的示例。
4.2 验证指定分组
实际开发中,我们肯定给前端同学提供过“创建XXX”和“更新XXX”的接口。
- 对于创建接口,实体中的id属性是必须为null的;
- 对于更新接口,实体中的id属性是必须传的,即不能为null的;
定义待验证的实体类:
@Data
public class GroupValidRequest {
@Null(message = "创建时id必须为null", groups = Create.class)
@NotNull(message = "更新时id必传", groups = Update.class)
private Integer id;
@NotNull(message = "字符串param3不能为空")
@Size(min = 6, max = 11, message = "字符串param3长度必须是6-11个字符")
private String name;
/**
* 创建时的分组
*/
public interface Create {
}
/**
* 更新时的分组
*/
public interface Update {
}
}
分组必须定义成接口
定义http接口:
@RestController
public class ValidationController {
/**
* 创建的请求
*
* @param request
* @return
*/
@PostMapping("/valid-group-request-create")
public Response validCreate(@RequestBody @Validated(GroupValidRequest.Create.class) GroupValidRequest request) {
return Response.success();
}
/**
* 更新的请求
*
* @param request
* @return
*/
@PostMapping("/valid-group-request-update")
public Response validUpdate(@RequestBody @Validated(GroupValidRequest.Update.class) GroupValidRequest request) {
return Response.success();
}
}
附录
更多自定义校验注解示例
待校验的实体类
@Data
public class CustomValidRequest {
@DateFormat
private String birthday;
@DateTimeFormat
private String createdAt;
@PhoneNumber
private String phone;
@DiscreteValue(values = {1, 2, 3}, message = "不支持的类型")
private int type;
@EnumValue(enumClass = TaskTypeEnum.class, message = "任务类型不合法")
private String taskType;
@EnumValue(enumClass = TaskStatusEnum.class, message = "任务状态值不合法")
private Integer taskStatus;
@Ipv4()
private String ip;
}
@DateFormat、@DateTimeFormat
用于校验时间格式
@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = DateFormatValidator.class)
public @interface DateFormat {
/**
* 校验不通过时展示的消息
*
* @return
*/
String message() default "日期格式不合法";
/**
* 按这个格式校验日期字符串
*
* @return
*/
String format() default "yyyy-MM-dd";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@DateFormat
@Constraint(validatedBy = {})
@ReportAsSingleViolation
public @interface DateTimeFormat {
/**
* 校验不通过时展示的消息
* 使用@OverridesAttribute注解 标识 该属性继承DateFormat中的message属性
*
* @return
*/
@OverridesAttribute(constraint = DateFormat.class, name = "message")
String message() default "日期时间格式不合法";
/**
* 按这个格式校验日期时间字符串
* 使用@OverridesAttribute注解 标识 该属性继承DateFormat中的format属性
*
* @return
*/
@OverridesAttribute(constraint = DateFormat.class, name = "format")
String format() default "yyyy-MM-dd HH:mm:ss";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class DateFormatValidator implements ConstraintValidator<DateFormat, String> {
// 格式
private String format;
@Override
public void initialize(DateFormat constraintAnnotation) {
this.format = constraintAnnotation.format();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (!StringUtils.hasText(value)) {
return true;
}
try {
DateTimeFormatter.ofPattern(this.format).parse(value);
return true;
} catch (Throwable t) {
return false;
}
}
}
@EnumValue
用于校验枚举值,验证的枚举类型需要实现BaseEnum接口。
public interface BaseEnum<T> {
T getCode();
String getMessage();
}
@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = {EnumValueValidator.class})
public @interface EnumValue {
Class<? extends BaseEnum> enumClass();
String message() default "未知的值";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
public class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {
private Class<? extends BaseEnum> enumClass;
@Override
public void initialize(EnumValue constraintAnnotation) {
Class<? extends BaseEnum> enumClass = constraintAnnotation.enumClass();
if (!enumClass.isEnum()) {
throw new IllegalArgumentException("enumClass必须为枚举类型");
}
this.enumClass = enumClass;
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (null == value) {
return true;
}
BaseEnum[] enumValues = enumClass.getEnumConstants();
return Stream.of(enumValues).filter(e -> e.isEquals(value)).findFirst().map(v -> true).orElse(false);
}
}
@Ipv4
@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = {Ipv4Validator.class})
@Repeatable(Ipv4.List.class)
public @interface Ipv4 {
String message() default "IP地址格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@interface List {
Ipv4[] value();
}
}
public class Ipv4Validator implements ConstraintValidator<Ipv4, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StringUtils.isEmpty(value)) {
return true;
}
String[] part = value.split("\\.");
if(part.length != 4) {
return false;
}
// 避免 172.001.020.2 的情况
for(int i = 0 ; i < 4 ; i ++){
if (!StringUtils.isNumeric(part[i])
|| Integer.parseInt(part[i], 10) > 255
|| (StringUtils.startsWith(part[i], "0") && !StringUtils.equals(part[i], "0"))) {
return false;
}
}
return true;
}
}
单独使用Hibernate Validator
SpringBoot集成Hibernate Validator的方式,可以帮助我们对HTTP接口的入参进行验证。
然而,有时我们需要校验参数的场景不一定都是HTTP接口,因此,就需要直接使用Hibernate Validator来进行参数验证,这里提供了一个ValidateUtil类,可以帮忙我们手动触发参数的校验。
public class ValidateUtil {
/**
* 快速失败的validator
*/
private static final Validator VALIDATOR_FAIL_FAST = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory()
.getValidator();
/**
* 校验所有属性的validator
*/
private static final Validator VALIDATOR_ALL = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(false)
.buildValidatorFactory()
.getValidator();
/**
* 获取校验结果
*
* @param object
* @param groups
* @param <T>
* @return
*/
public static <T> Set<ConstraintViolation<T>> getValidatorFastResult(T object, Class<?>... groups) {
return VALIDATOR_FAIL_FAST.validate(object, groups);
}
public static <T> void validateFailFast(T object, Class<?>... groups) {
Set<ConstraintViolation<T>> validateResult = getValidatorFastResult(object, groups);
if (validateResult.size() <= 0) {
return;
}
throw new ValidationException(buildValidateMsg(validateResult));
}
public static <T> void validateFailFast(Collection<T> objects, Class<?>... groups) {
for (T object : objects) {
validateFailFast(object, groups);
}
}
/**
* 获取校验结果
*
* @param object
* @param groups
* @param <T>
* @return
*/
public static <T> Set<ConstraintViolation<T>> getValidatorAllResult(T object, Class<?>... groups) {
return VALIDATOR_ALL.validate(object, groups);
}
public static <T> void validateAll(T object, Class<?>... groups) {
Set<ConstraintViolation<T>> validateResult = getValidatorAllResult(object, groups);
if (validateResult.size() <= 0) {
return;
}
throw new ValidationException(buildValidateMsg(validateResult));
}
public static <T> void validateAll(Collection<T> objects, Class<?>... groups) {
for (T object : objects) {
validateAll(object, groups);
}
}
/**
* 构造校验的错误消息
*
* @param validateResult
* @param <T>
* @return
*/
private static <T> String buildValidateMsg(Set<ConstraintViolation<T>> validateResult) {
StringBuilder validateMsg = new StringBuilder();
for (ConstraintViolation<T> violation : validateResult) {
validateMsg.append(violation.getMessage()).append(";\n");
}
return validateMsg.toString();
}
}
参考文档
https://github.com/marqueeluo/spring-boot-validation-demo
Spring基础系列-参数校验:
https://www.cnblogs.com/V1haoge/p/9953744.html
多字段联合逻辑校验: