官网:Jakarta Bean Validation specification
@Validation是一套帮助我们继续对传输的参数进行数据校验的注解,通过配置Validation可以很轻松的完成对数据的约束。
@Validated作用在类、方法和参数上
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
Class<?>[] value() default {};
}
maven依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-validationartifactId>
dependency>
gradle依赖:
implementation("org.springframework.boot:spring-boot-starter-validation")
package spring.validation.advice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import spring.validation.entity.Result;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author liuyang
* 创建时间: 2022-05-23 9:04
*/
@RestControllerAdvice
public class GlobalExceptionAdvice {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionAdvice.class);
@ExceptionHandler(value = Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result handleRuleException(Exception e) {
LOGGER.error(e.getMessage(), e);
Result result = Result.failure("999999", e.toString());
return result;
}
/**
* spring 封装的参数验证异常,处理 json 请求体调用接口校验失败抛出的异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result handle(MethodArgumentNotValidException e) {
LOGGER.error(e.getMessage(), e);
//获取参数校验错误集合
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
//格式化以提供友好的错误提示
String data = String.format("参数校验错误(%s):%s", fieldErrors.size(),
fieldErrors.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(";")));
//参数校验失败响应失败个数及原因
return Result.failure("888888", "校验失败", data);
}
/**
* 请求参数校验(PathVariables、RequestParameters、RequestHeader等)校验失败
*/
@ExceptionHandler(ServletRequestBindingException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result constraintViolationExceptionHandler(ServletRequestBindingException e) {
LOGGER.error(e.getMessage(), e);
return Result.failure("888887", "校验失败," + e.getMessage());
}
}
通过源码分析:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Valid {
}
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
Class<?>[] value() default {};
}
@Valid:没有分组的功能。
@Valid:可以用在方法、构造函数、方法参数和**成员属性(字段)**上
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制
@Validated:可以用在类型、方法和方法参数上。但是**不能用在成员属性(字段)**上
两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能,如果需要嵌套验证,则字段上必须增加 @Valid 注解。
比如我们现在有个实体叫做Item:
public class Item {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
@NotNull(message = "props不能为空")
@Size(min = 1, message = "至少要有一个属性")
private List<Property> properties;
}
Item带有很多属性,属性里面有属性id,属性值id,属性名和属性值,如下所示:
public class Property {
@NotNull(message = "pid不能为空")
@Min(value = 1, message = "pid必须为正整数")
private Long pid;
@NotNull(message = "name不能为空")
private String name;
}
属性这个实体也有自己的验证机制,比如属性和属性值id不能为空,属性名和属性值不能为空等。
现在我们有个ItemController接受一个Item的入参,想要对Item进行验证,如下所示:
@RestController
public class ItemController {
@RequestMapping("/item/add")
public void addItem(@Validated Item item) {
//doSomething();
}
}
在上图中,如果Item实体的props属性不额外加注释,只有@NotNull和@Size,无论入参采用@Validated还是@Valid验证,Spring Validation框架只会对Item的id和props做非空和数量验证,不会对props字段里的Prop实体进行字段验证,也就是**@Validated和@Valid**加在方法参数前,都不会自动对参数进行嵌套验证。也就是说如果传的List中有Prop的pid为空或者是负数,入参验证不会检测出来。
为了能够进行嵌套验证,必须手动在Item实体的props字段上明确指出这个字段里面的实体也要进行验证。由于@Validated不能用在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且@Valid类注解上也说明了它支持嵌套验证功能,那么我们能够推断出:@Valid加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证。
我们修改Item类如下所示:
public class Item {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
/**
* 嵌套验证必须用@Valid
*/
@Valid
@NotNull(message = "properties不能为空")
@Size(min = 1, message = "至少要有一个属性")
private List<Property> properties;
}
然后我们在ItemController的addItem函数上再使用@Validated或者@Valid,就能对Item的入参进行嵌套验证。此时Item里面的props如果含有Prop的相应字段为空的情况,Spring Validation框架就会检测出来。
Default
接口,建议新建分组加上 extend javax.validation.groups.Default
groups
属性指定分组@Validated
注解添加激活(或使用)的分组类package javax.validation.groups;
/**
* Default Jakarta Bean Validation group.
*
* Unless a list of groups is explicitly defined:
*
* - constraints belong to the {@code Default} group
* - validation applies to the {@code Default} group
*
* Most structural constraints should belong to the default group.
*
* @author Emmanuel Bernard
*/
public interface Default {
}
在编写Custom
分组接口时,如果继承了 Default
,下面两个写法就是等效的:
@Validated({Custom.class})
@Validated({Custom.class, Default.class})
ConstraintValidator
,实现isValid方法来判断校验是否成功校验两个变量至少存在一个(如父母亲手机号至少填一个)
接口定义:
@PostMapping("/insertChildInfo")
public ChildInfo insertChildInfo(@Validated @RequestBody ChildInfo info) {
return info;
}
自定义被@AsserTrue标记的方法(private、public均可),内部自定义实现逻辑即可。
package spring.validation.entity;
import lombok.Getter;
import lombok.Setter;
import org.springframework.util.StringUtils;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
/**
* @author liuyang
* 创建时间: 2022-06-19 17:09
*/
@Getter
@Setter
public class ChildInfo {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private int id;
@NotNull(message = "name不能为空")
private String name;
private String fatherPhone;
private String motherPhone;
@AssertTrue(message = "method,父母亲手机号至少填写一个")
private boolean isHasPhoneNum() {
return !StringUtils.isEmpty(fatherPhone) || !StringUtils.isEmpty(motherPhone);
}
}
可以通过@ScriptAssert自定义JavaScript脚本来实现,默认脚本上下文用_this
表示
package spring.validation.entity;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.ScriptAssert;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
/**
* @author liuyang
* 创建时间: 2022-06-19 17:09
*/
@Getter
@Setter
//@ScriptAssert(lang = "javascript", script = "_this.fatherPhone != null || _this.motherPhone != null",
// message = "javascript, fatherPhone 和 motherPhone 不能同时为空", reportOn = "fatherPhone")
//@ScriptAssert(lang = "javascript", alias = "_", script = "_.fatherPhone != null && _.motherPhone != null", message = "", reportOn = "fatherPhone")
@ScriptAssert(lang = "javascript", alias = "_", script = "!org.springframework.util.StringUtils.isEmpty(_.fatherPhone) || !org.springframework.util.StringUtils.isEmpty(_.motherPhone)",
message = "javascript2, fatherPhone 和 motherPhone 不能同时为空", reportOn = "fatherPhone")
public class ChildInfo2 {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private int id;
@NotNull(message = "name不能为空")
private String name;
private String fatherPhone;
private String motherPhone;
}
使用 _this :
使用StringUtils:
通过自定义注解及校验方法实现
package spring.validation.annotations;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 至少存在一个(一个或多个都存在)
*
* @author liuyang
* 创建时间: 2022-06-19 17:55
*/
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = OneOfValidator.class)
@Documented
public @interface OneOf {
String message() default "{one.of.message}";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
String[] value();
}
package spring.validation.annotations;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Optional;
/**
* 自定义校验方法
*
* @author liuyang
* 创建时间: 2022-06-19 17:56
*/
public class OneOfValidator implements ConstraintValidator {
private String[] fields;
@Override
public void initialize(OneOf annotation) {
this.fields = annotation.value();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(value);
int matches = countNumberOfMatches(wrapper);
if (matches == 0) {
setValidationErrorMessage(context);
return false;
}
return true;
}
/**
* 获取值不为空的field数量
*/
private int countNumberOfMatches(BeanWrapper wrapper) {
int matches = 0;
for (String field : fields) {
Object value = wrapper.getPropertyValue(field);
boolean isPresent = detectOptionalValue(value);
if (!StringUtils.isEmpty(value) && isPresent) {
matches++;
}
}
return matches;
}
private boolean detectOptionalValue(Object value) {
if (value instanceof Optional) {
return ((Optional) value).isPresent();
}
return true;
}
/**
* 设置校验失败信息
*/
private void setValidationErrorMessage(ConstraintValidatorContext context) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
.addPropertyNode(fields[0]).addConstraintViolation();
}
}