1、概述
在做开发的时候后端做参数校验检验必不可少,java提出了 Bean Validation 规范,并且经理了JSR303、JSR349、JSR380 三次标准。
Bean Validation 只提供规范,不提供具体的实现。
目前大多数开发中我们使用 Hibernate Validator 这个实现。
在spring 项目中,Spring Validation 提供了对 Bean Validation 的内置封装支持,可以使用 @Validated 注解,实现声明式校验,而无需直接调用 Bean Validation 提供的 API 方法。而在实现原理上,也是基于 Spring AOP 拦截,实现校验相关的操作
2、注解
2.1、 Bean Validation 中内置的 constraint
Constraint | 详细信息 |
| 被注释的元素必须为 |
| 被注释的元素必须不为 |
| 被注释的元素必须为 |
| 被注释的元素必须为 |
| 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
| 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
| 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
| 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
| 被注释的元素的大小必须在指定的范围内 |
| 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
| 被注释的元素必须是一个过去的日期 |
| 被注释的元素必须是一个将来的日期 |
| 被注释的元素必须符合指定的正则表达式 |
@NotBlank | 只能用于字符串不为 |
@NotEmpty | 集合对象的元素不为 0 ,即集合不为空,也可以用于字符串不为 |
2.2、Hibernate Validator 附加的 constraint
Constraint | 详细信息 |
| 被注释的元素必须是电子邮箱地址 |
| 被注释的字符串的大小必须在指定的范围内 |
| 被注释的字符串的必须非空 |
| 被注释的元素必须在合适的范围内 |
2.3、@Valid 和 @Validated
@Valid
注解, 是 Bean Validation
@Validated
是 Spring Validation 定义,可以添加在类、方法参数、普通方法上,表示它们需要进行约束校验。同时,@Validated
有 value
属性,支持分组校验。
Spring Validation 仅对 @Validated
注解,实现声明式校验。
相比较来说,使用@Valid注解的地方,多了==【成员变量】==,【构造方法】和【方法返回】,在有嵌套对象的使用只能使用@Valid
//User.java
public class User {
private String id;
@Valid
private Resume resume;
}
//Resume.java
public class Resume {
@NotBlank
private String contact;
}
3、实践
3.1pom.xml中引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
3.2、UserAddDTO
@Data
public class UserAddDTO {
/**
* 账号
*/
@NotEmpty(message = "登录账号不能为空")
@Length(min = 5, max = 16, message = "账号长度为 5-16 位")
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
private String username;
/**
* 密码
*/
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
}
3.3、UserContoller
@Slf4j
@RestController
@RequestMapping("/users")
@Validated
public class UserContoller {
@GetMapping("/get")
public void get(@RequestParam("id") @Min(value = 1L, message = "编号必须大于 0") Integer id) {
log.info("[get][id: {}]", id);
}
@PostMapping("/add")
public void add(@Valid @RequestBody UserAddDTO addDTO) {
log.info("[add][addDTO: {}]", addDTO);
}
}
- 对于get(id)方法
抛出的异常是javax.validation.ConstraintViolationException
- 对于add(@Valid @RequestBody UserAddDTO addDTO)方法
抛出的异常时org.springframework.web.bind.MethodArgumentNotValidException
4、校验异常–全局处理
从上面可以看出返回的数据不友好,我们可以根据全局异常处理,进行友好提示
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理 ServiceException 异常
*/
@ResponseBody
@ExceptionHandler(value = ServiceException.class)
public Result serviceExceptionHandler(HttpServletRequest req, ServiceException ex) {
log.debug("[serviceExceptionHandler]", ex);
// 包装 Result 结果
return Result.error(ex.getCode(), ex.getMessage());
}
// GlobalExceptionHandler.java
//处理参数校验ConstraintViolationException异常
@ResponseBody
@ExceptionHandler(value = ConstraintViolationException.class)
public Result constraintViolationExceptionHandler(HttpServletRequest req, ConstraintViolationException ex) {
log.debug("[constraintViolationExceptionHandler]", ex);
// 拼接错误
StringBuilder detailMessage = new StringBuilder();
for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) {
// 使用 ; 分隔多个错误
if (detailMessage.length() > 0) {
detailMessage.append(";");
}
// 拼接内容到其中
detailMessage.append(constraintViolation.getMessage());
}
// 包装 Result 结果
return Result.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
}
//处理参数校验BindException异常
@ResponseBody
@ExceptionHandler(value = BindException.class)
public Result bindExceptionHandler(HttpServletRequest req, BindException ex) {
log.info("[bindExceptionHandler]", ex);
// 拼接错误
StringBuilder detailMessage = new StringBuilder();
for (ObjectError objectError : ex.getAllErrors()) {
// 使用 ; 分隔多个错误
if (detailMessage.length() > 0) {
detailMessage.append(";");
}
// 拼接内容到其中
detailMessage.append(objectError.getDefaultMessage());
}
// 包装 Result 结果
return Result.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
}
//处理参数校验MethodArgumentNotValidException异常
@ResponseBody
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result methodArgumentNotValidExceptionHandle(HttpServletRequest req, MethodArgumentNotValidException ex) {
log.info("[methodArgumentNotValidExceptionHandle]", ex);
// 拼接错误
StringBuilder detailMessage = new StringBuilder();
for (ObjectError objectError : ex.getBindingResult().getAllErrors()) {
// 使用 ; 分隔多个错误
if (detailMessage.length() > 0) {
detailMessage.append(";");
}
// 拼接内容到其中
detailMessage.append(objectError.getDefaultMessage());
}
// 包装 Result 结果
return Result.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
}
/**
* 处理其它 Exception 异常
*/
@ResponseBody
@ExceptionHandler(value = Exception.class)
public Result exceptionHandler(HttpServletRequest req, Exception e) {
// 记录异常日志
log.error("[exceptionHandler]", e);
// 返回 ERROR Result
return Result.error(ServiceExceptionEnum.SYSTEM_ERROR.getCode(),
ServiceExceptionEnum.SYSTEM_ERROR.getMessage());
}
}
参数校验异常处理是一下三个:
//处理参数校验ConstraintViolationException异常
@ResponseBody
@ExceptionHandler(value = ConstraintViolationException.class)
public Result constraintViolationExceptionHandler(HttpServletRequest req, ConstraintViolationException ex) {
log.debug("[constraintViolationExceptionHandler]", ex);
// 拼接错误
StringBuilder detailMessage = new StringBuilder();
for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) {
// 使用 ; 分隔多个错误
if (detailMessage.length() > 0) {
detailMessage.append(";");
}
// 拼接内容到其中
detailMessage.append(constraintViolation.getMessage());
}
// 包装 Result 结果
return Result.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
}
//处理参数校验BindException异常
@ResponseBody
@ExceptionHandler(value = BindException.class)
public Result bindExceptionHandler(HttpServletRequest req, BindException ex) {
log.info("[bindExceptionHandler]", ex);
// 拼接错误
StringBuilder detailMessage = new StringBuilder();
for (ObjectError objectError : ex.getAllErrors()) {
// 使用 ; 分隔多个错误
if (detailMessage.length() > 0) {
detailMessage.append(";");
}
// 拼接内容到其中
detailMessage.append(objectError.getDefaultMessage());
}
// 包装 Result 结果
return Result.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
}
//处理参数校验MethodArgumentNotValidException异常
@ResponseBody
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result methodArgumentNotValidExceptionHandle(HttpServletRequest req, MethodArgumentNotValidException ex) {
log.info("[methodArgumentNotValidExceptionHandle]", ex);
// 拼接错误
StringBuilder detailMessage = new StringBuilder();
for (ObjectError objectError : ex.getBindingResult().getAllErrors()) {
// 使用 ; 分隔多个错误
if (detailMessage.length() > 0) {
detailMessage.append(";");
}
// 拼接内容到其中
detailMessage.append(objectError.getDefaultMessage());
}
// 包装 Result 结果
return Result.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
}
- get(id)方法
- add(@Valid @RequestBody UserAddDTO addDTO)
5、自定义约束
很多时候无论是Bean Validation 定义的约束,还是 Hibernate Validator都无法满足我们的开发需求,所以我们需要自定义约束
自定义约束的步骤:
1、编写自定义约束的注解;
2、编写自定义的校验器 ConstraintValidator 。
5.1、IntArrayValuable
public interface IntArrayValuable {
/**
* @return int 数组
*/
int[] array();
}
5.2、GenderEnum
@Getter
@AllArgsConstructor
public enum GenderEnum implements IntArrayValuable {
MALE(1, "男"),
FEMALE(2, "女");
/**
* 性别值
*/
private final Integer value;
/**
* 性别名
*/
private final String name;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(GenderEnum::getValue).toArray();
@Override
public int[] array() {
return ARRAYS;
}
}
5.3、@InEnum
就是第一步编写注解:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = InEnumValidator.class)
public @interface InEnum {
/**
* @return 实现 IntArrayValuable 接口的
*/
Class<? extends IntArrayValuable> value();
/**
* @return 提示内容
*/
String message() default "必须在指定范围 {value}";
/**
* @return 分组
*/
Class<?>[] groups() default {};
/**
* @return Payload 数组
*/
Class<? extends Payload>[] payload() default {};
}
5.4、InEnumValidator 自定义的校验器
就是第二步
public class InEnumValidator implements ConstraintValidator<InEnum, Integer> {
/**
* 值数组
*/
private Set<Integer> values;
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
// 校验通过
if (values.contains(value)) {
return true;
}
//校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
.replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句
return false;
}
@Override
public void initialize(InEnum annotation) {
IntArrayValuable[] values = annotation.value().getEnumConstants();
if (values.length == 0) {
this.values = new HashSet<>();
} else {
this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toSet());
}
}
}
5.5使用
@Data
public class UserAddDTO {
/**
* 账号
*/
@NotEmpty(message = "登录账号不能为空")
@Length(min = 5, max = 16, message = "账号长度为 5-16 位")
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
private String username;
/**
* 密码
*/
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
/**
* 性别
*/
@NotNull(message = "性别不能为空")
@InEnum(value = GenderEnum.class, message = "性别必须是 {value}")
private Integer gender;
}
@Slf4j
@Validated
@RestController
@RequestMapping("/users")
public class UserContoller {
@GetMapping("/get")
public void get(@RequestParam("id") @Min(value = 1L, message = "编号必须大于 0") Integer id) {
log.info("[get][id: {}]", id);
}
@PostMapping("/add")
public void add(@Valid @RequestBody UserAddDTO addDTO) {
log.info("[add][addDTO: {}]", addDTO);
}
}