这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战
在日常api开发中,为了程序的安全性和用户体验,我们都离不开api入参校验,我们可以使用Hibernate Validator
对参数进行校验。当内置验证规则不能满足我们的校验需求,可以通过自定义注解配合Hibernate Validator
较为优雅的实现参数校验。
一、Hibernate Validator
1. 常用校验注解介绍
@AssertFalse
注解的元素必须是Boolean类型,且值为false
@AssertTrue
注解的元素必须是Boolean类型,且值为true
@DecimalMax
注解的元素必须是数字,且值小于等于给定的值
@DecimalMin
注解的元素必须是数字,且值大于等于给定的值
@Digits
注解的元素必须是数字,且值必须是指定的位数
@Max
注解的元素必须是数字,且值小于等于给定的值
@Min
注解的元素必须是数字,且值小于等于给定的值
@Range
注解的元素需在指定范围区间内
@Future
注解的元素必须是将来某个日期
@Past
注解的元素必须是某个过去的日期
@PastOrPresent
注解的元素必须是过去某个或现在日期
@NotNull
注解的元素值不能为null
@NotBlank
注解的元素值有内容(不为null、去除首位空格后长度为0),用于字符串
@Null
注解的元素值为null
@Pattern
注解的元素必须满足给定的正则表达式
@Size
注解的元素必须是String、集合或数组,且长度大小需保证在给定范围之内
@Email
注解的元素需满足Email格式
2. 验证器使用
a. RequestParam参数校验
注意:当校验
RequestParam
的时候,@Validated
注解必须放在控制器类上,否则校验注解不生效。
- 错误示范:
@RestController
@RequestMapping("test")
public class TestController {
@GetMapping("users")
public void userList(@Validated @Min(value = 1, message = "页码不能小于1") @RequestParam(name = "page", defaultValue = "1") Integer page) {
System.out.println(page);
}
}
复制代码
- 正确做法:
@RestController
@RequestMapping("test")
@Validated
public class TestController {
@GetMapping("users")
public void userList(@Min(value = 1, message = "页码不能小于1") @RequestParam(name = "page", defaultValue = "1") Integer page) {
System.out.println(page);
}
}
复制代码
当参数校验不通过的时候,我们在控制台就能看到抛出的异常:
javax.validation.ConstraintViolationException: userList.page: 页码不能小于1
复制代码
实际业务中我们希望结果是这样的 {"code": 400, msg: "页码不能小于1", "data": null}
,有个统一的返回格式。
- 创建一个统一响应类,代码如下:
public class UnifyResponse<T> {
private int code;
private String msg;
private T data;
public UnifyResponse (int code, String message) {
this.code = code;
this.msg = message;
this.request = request;
this.data = null;
}
public UnifyResponse (int code, String message, T data) {
this.code = code;
this.msg = message;
this.request = request;
this.data = null;
}
}
复制代码
- 使用
@ControllerAdvice
注解实现参数校验异常处理:
@ControllerAdvice
public class GlobalExceptionAdvice {
// ConstraintViolationException参数校验异常处理
@ExceptionHandler(ConstraintViolationException.class)
// 响应http状态码为400
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
@ResponseBody
public UnifyResponse handleConstraintException (HttpServletRequest request, ConstraintViolationException e) {
// 获取参数校验的异常信息,注解的message
String errorMsg = e.getConstraintViolations().iterator().next().getMessage();
return new UnifyResponse(400, errorMsg);
}
}
复制代码
- 当参数校验失败时候就会响应
{
"code": 40009,
"msg": "页码不能小于1",
"data": null
}
复制代码
b. RequestBody校验
复杂请求的时候,我们肯能就会创建bean
来接收这些参数,我们可以在每个需要校验的成员变量上使用校验注解。例如有个需求,新增用户,字段分别为username
、password
。
注意:bean校验必须在参数前添加
@Validated
注解,否则验证器不生效。
- 创建
bean
,增加校验规则:username
必填,并且长度2-50字符;password
必填,长度6-20字符。
@Data
class User {
@NotBlank(message = "用户名必填")
@Length(min = 2, max = 20, message = "用户名2-50字")
private String username;
@NotBlank(message = "密码必填")
@Length(min = 6, max = 20, message = "密码长度6-20")
private String password;
}
复制代码
- 控制器
@RestController
@RequestMapping("test")
@Validated
public class TestController {
@PostMapping("users")
public void createUser (@Validated @RequestBody User user) {
System.out.println(user);
}
}
复制代码
- 当参数校验不通过的时候会抛出
org.springframework.web.bind.MethodArgumentNotValidException
异常,我们继续在GlobalExceptionAdvice
中增加对这个异常的处理:
@ControllerAdvice
public class GlobalExceptionAdvice {
...
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
@ResponseBody
public UnifyResponse handleBeanValidation (HttpServletRequest request, MethodArgumentNotValidException e) {
List<ObjectError> errors = e.getBindingResult().getAllErrors();
String message = errors.get(0).getDefaultMessage();
return new UnifyResponse(40009, message);
}
}
复制代码
二、自定义校验注解
当 Hibernate Validator
提供的校验注解不能满足我们的参数校验需求,这时就需要我们自定义校验注解。还是举例上面的新增用户,需要增加一个字段 mobile
,我们自定义一个校验手机号码的注解。
- 新建注解,通过
@Constraint
告诉验证器需要使用哪个验证器。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Constraint(validatedBy = MobileNumberValidator.class)
public @interface MobileNumber {
// 注解参数,校验失败提示信息
String message() default "手机号码不正确";
// 注解参数,是否必填
boolean require() default false;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
复制代码
-
注解验证器实现类
MobileNumberValidator
;需要实现ConstraintValidator
接口,泛型中第一项是校验注解,第二项是需要校验字段的数据类型;实现其中
initialize
、isValid
方法,在initialize
中我们可以拿到注解,获取一些注解中参数。isValid
是实际对参数的校验方法,返回true
则校验通过,反之校验失败。
public class MobileNumberValidator implements ConstraintValidator<MobileNumber, String> {
private boolean require;
@Override
public void initialize(MobileNumber constraintAnnotation) {
this.require = constraintAnnotation.require();
}
@Override
public boolean isValid(String mobile, ConstraintValidatorContext constraintValidatorContext) {
if (this.require) {
return !StringUtils.isBlank(mobile) && this.isMobileNumber(mobile);
} else {
return StringUtils.isBlank(mobile) || this.isMobileNumber(mobile);
}
}
private boolean isMobileNumber (String mobile) {
String regex = "^1[3-9]\\d{9}$";
if (mobile.length() != 11) {
return false;
} else {
Pattern p = Pattern.compile(regex);
Matcher matcher = p.matcher(mobile);
return matcher.matches();
}
}
}
复制代码
-
使用方式,在字段上增加
@MobileNumber
注解,如果必填项则传入require = true
,可以通过message
参数定义参数校验失败提示信息。- bean
@Data
class User {
@NotBlank(message = "用户名必填")
@Length(min = 2, max = 20, message = "用户名2-50字")
private String username;
@NotBlank(message = "密码必填")
@Length(min = 6, max = 20, message = "密码长度6-20")
private String password;
@MobileNumber(require = true, message = "手机号码校验失败")
private String mobile;
}
复制代码
- controller
@RestController
@RequestMapping("test")
@Validated
public class TestController {
@GetMapping("users")
public void userList(
@Min(value = 1, message = "页码不能小于1") @RequestParam(name = "page", defaultValue = "1") Integer page,
@MobileNumber @RequestParam(name = "mobile", defaultValue = "") String mobile) {
System.out.println(page);
System.out.println(mobile);
}
}
复制代码
近期评论