不推荐的校验
通过if
判断一个个的判断必须的参数,这样的写法很臃肿。
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("addUser")
public ResponseEntity<String> add(@Valid @RequestBody UserParam userParam) {
if (StringUtils.isBlank(userParam.getUserName())){
ResponseEntity.badRequest().body("userName不能为空");
}
if (StringUtils.isBlank(userParam.getEmail())){
ResponseEntity.badRequest().body("email不能为空");
}
if (StringUtils.isBlank(userParam.getNickName())){
ResponseEntity.badRequest().body("nickName不能为空");
}
//……
return ResponseEntity.ok("success");
}
}
优雅的校验
Java开者在Java API规范 (JSR303) 定义了Bean校验的标准 validation-api ,但没有提供实现。
hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等。
Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。
接下来,我们以springboot项目为例,介绍Spring Validation的使用。
- 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
- 在入参映射的实体vo上添加注解
@Data
public class UserParam implements Serializable {
private static final long serialVersionUID = 1L;
@NotEmpty(message = "userName不能为空")
private String userName;
@NotEmpty(message = "email不能为空")
@Email(message = "email格式异常,请检查")
private String email;
@NotEmpty(message = "nickName不能为空")
@Length(min = 1, max = 10, message = "nick name should be 1-10")
private String nickName;
@NotNull(message = "sex不能为空")
@Range(min = 0, max = 1, message = "sex 值是 0-1")
private int sex;
}
- 编写controller
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("addUser")
public ResponseEntity<String> add(@Valid @RequestBody UserParam userParam, BindingResult bindingResult) {
//如果验证失败。取出
if (bindingResult.hasErrors()) {
List<ObjectError> errors = bindingResult.getAllErrors();
errors.forEach(p -> {
FieldError fieldError = (FieldError) p;
log.error("Invalid Parameter : object - {},field - {},errorMessage - {}", fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage());
});
return ResponseEntity.badRequest().body(JSONUtil.toJsonStr(errors));
}
return ResponseEntity.ok("success");
}
}
在要验证的参数类类型前添加@Valid
表示开启验证。若果验证不通过,我们可以通过BindingResult
取出异常返回给请求方。如果不在最后一项声明BindingResult,会被无情地抛错。
参数异常统一处理
通过上方可以看出,这种方式可以很优雅的解决了验证参数这一操作。但是controller中的方法中,我们每个方法都要编写固定的取出异常的代码,这样不太好,接下来我们通过全局异常进行优化。
- 在上方的基础之上添加全局异常类
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// 1、处理 form data方式调用接口校验失败抛出的异常
@ExceptionHandler(BindException.class)
public ResultBody bindExceptionHandler(BindException e) {
List<FieldError> fieldErrors = e.getFieldErrors();
List<String> collect = fieldErrors.stream().map(o -> o.getDefaultMessage()).collect(Collectors.toList());
return new ResultBody("500","验证错误",collect);
}
// 2、处理 json 请求体调用接口校验失败抛出的异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultBody handleError(MethodArgumentNotValidException e){
log.info("GlobalExceptionHandler-handleError-e:{}",e);
BindingResult bindingResult = e.getBindingResult();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
List<String> collect = fieldErrors.stream().map(o -> o.getDefaultMessage()).collect(Collectors.toList());
return new ResultBody("500","验证错误",collect);
}
}
- 调整controller
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("addUser")
public ResponseEntity<String> add(@Valid @RequestBody UserParam userParam) {
return ResponseEntity.ok("success");
}
}
删除BindingResult
参数和内部取值逻辑,统一放到全局异常中进行处理。
分组校验
那么假设Controller有两个请求一个是添加数据,一个是修改用户数据的,需要在UserParam
类上,添加一个userId
(修改时需要),而在添加时不需要的,那么我们可以通过分组校验,去控制修改时,不能为空。
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("addUser")
public ResponseEntity<String> addUser(@RequestBody UserParam userParam) {
return ResponseEntity.ok("success");
}
@PostMapping("editUser")
public ResponseEntity<String> editUser(@RequestBody UserParam userParam) {
return ResponseEntity.ok("success");
}
}
- 先定义两个分组
//添加分组
public interface AddValidationGroup {
}
//修改分组
public interface EditValidationGroup {
}
- 在UserParam添加userId
@Data
public class UserParam implements Serializable {
@NotEmpty(message = "userId不能为空",groups = EditValidationGroup.class)
private String userId;
//……
}
- Controller中不同请求使用不同分组
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("addUser")
public ResponseEntity<String> addUser(@Validated(AddValidationGroup.class) @RequestBody UserParam userParam) {
return ResponseEntity.ok("success");
}
@PostMapping("editUser")
public ResponseEntity<String> editUser(@Validated(EditValidationGroup.class) @RequestBody UserParam userParam) {
return ResponseEntity.ok("success");
}
}
:::warning
这里的开启验证注解一定要使用@Validated
而不是@Valid
:::
测试结果:
请求editUser返回:
{"code":"500","msg":"验证错误","data":["userId不能为空"]}
请求addUser返回:
success
递归校验
如果在A
类中引入了B
类,而B
类中的属性也需要校验,这就是递归校验。其实很简单,我们只需要在A
类中声明的B
类就可以自动进行递归校验。
public class A{
@Valid
public B b;
}
public class B{
@NotEmpty(message = "id不能为空")
private String id;
}
自定义校验
Spring Validation允许用户自定义校验,实现方式也很简单,分两步:
- 自定义校验注解
- 编写校验实现
- 编写注解
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {HaveNoBlankValidator.class})// 标明由哪个类执行校验逻辑
public @interface HaveNoBlank {
// 校验出错时默认返回的消息
String message() default "字符串中不能含有空格";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
/**
* 同一个元素上指定多个该注解时使用
*/
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface List {
NotBlank[] value();
}
}
- 编写注解实现
public class HaveNoBlankValidator implements ConstraintValidator<HaveNoBlank, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// null 不做检验
if (value == null) {
return true;
}
// 空 校验失败
if (value.contains(" ")) {
return false;
}
// 校验成功
return true;
}
}
- 使用定义的
HaveNoBlank
注解
只需要在相应字段添加即可,使用起来和内置的注解并无任何差异
@Data
public class UserParam implements Serializable {
private static final long serialVersionUID = 1L;
@HaveNoBlank(message = "userName不能为空")
private String userName;
//……
}
- 测试
@SpringBootTest
public class ValidationTest {
@Test
public void addUser(){
UserParam userParam = new UserParam();
userParam.setUserName(" ");
userParam.setEmail("test@qq.coms");
userParam.setNickName("test");
userParam.setSex(1);
String bodyStr = JSONUtil.toJsonStr(userParam);
System.out.println(bodyStr);
String result = HttpUtil.post("http://localhost:8001/user/addUser", bodyStr);
System.out.println(result);
}
}
常用注解
JSR303/JSR-349 : JSR303是一项标准,只提供规范不提供实现,规定一些校验规范即校验注解。JSR-349是其的升级版本,添加了一些新特性 。
@AssertFalse | 被注释的元素只能为false |
---|---|
@AssertTrue | 被注释的元素只能为true |
@DecimalMax | 被注释的元素必须小于或等于{value} |
@DecimalMin | 被注释的元素必须大于或等于{value} |
@Digits | 被注释的元素数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内) |
被注释的元素不是一个合法的电子邮件地址 | |
@Future | 被注释的元素需要是一个将来的时间 |
@FutureOrPresent | 被注释的元素需要是一个将来或现在的时间 |
@Max | 被注释的元素最大不能超过{value} |
@Min | 被注释的元素最小不能小于{value} |
@Negative | 被注释的元素必须是负数 |
@NegativeOrZero | 被注释的元素必须是负数或零 |
@NotBlank | 被注释的元素不能为空 |
@NotEmpty | 被注释的元素不能为空 |
@NotNull | 被注释的元素不能为null |
@Null | 被注释的元素必须为null |
@Past | 被注释的元素需要是一个过去的时间 |
@PastOrPresent | 被注释的元素需要是一个过去或现在的时间 |
@Pattern | 被注释的元素需要匹配正则表达式”{regexp}” |
@Positive | 被注释的元素必须是正数 |
@PositiveOrZero | 被注释的元素必须是正数或零 |
@Size | 被注释的元素个数必须在{min}和{max}之间 |
hibernate validation :hibernate validation是对这个规范的实现,并增加了一些其他校验注解,如@Email,@Length,@Range等等
@CreditCardNumber | 被注释的元素不合法的信用卡号码 |
---|---|
@Currency | 被注释的元素不合法的货币 (必须是{value}其中之一) |
@EAN | 被注释的元素不合法的{type}条形码 |
被注释的元素不是一个合法的电子邮件地址 (已过期) | |
@Length | 被注释的元素长度需要在{min}和{max}之间 |
@CodePointLength | 被注释的元素长度需要在{min}和{max}之间 |
@LuhnCheck | 被注释的元素${validatedValue}的校验码不合法, Luhn模10校验和不匹配 |
@Mod10Check | 被注释的元素${validatedValue}的校验码不合法, 模10校验和不匹配 |
@Mod11Check | 被注释的元素${validatedValue}的校验码不合法, 模11校验和不匹配 |
@ModCheck | 被注释的元素${validatedValue}的校验码不合法, ${modType}校验和不匹配 (已过期) |
@NotBlank | 被注释的元素不能为空 (已过期) |
@NotEmpty | 被注释的元素不能为空 (已过期) |
@ParametersScriptAssert | 被注释的元素执行脚本表达式”{script}”没有返回期望结果 |
@Range | 被注释的元素需要在{min}和{max}之间 |
@SafeHtml | 被注释的元素可能有不安全的HTML内容 |
@ScriptAssert | 被注释的元素执行脚本表达式”{script}”没有返回期望结果 |
@URL | 被注释的元素需要是一个合法的URL |
spring validation :spring validation对hibernate validation进行了二次封装,在springmvc模块中添加了自动校验,并将校验信息封装进了特定的类中
@Validate和@Valid什么区别
在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:
分组
- @Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,这个网上也有资料,不详述。@Valid:作为标准JSR-303规范,还没有吸收分组的功能。
注解地方
- @Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
- @Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上
嵌套类型
- 比如本文例子中的address是user的一个嵌套属性, 只能用@Valid
公用代码
controller接收的入参实体
@Data
public class UserParam implements Serializable {
private static final long serialVersionUID = 1L;
private String userName;
private String email;
private String phone;
private int sex;
}
统一返回VO
public class ResultBody {
private String code;
private String msg;
private Object data;
public ResultBody(String code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public ResultBody() {
}
public ResultBody(String code,String msg) {
this.code = code;
this.msg = msg;
this.data = null;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
使用的第三方模块
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>