如何在SpringBoot中做参数校验

前言

参数校验是开发中非常重要,也是非常繁琐的一件事情,不得不做,但是确无聊至极,而且和业务代码混杂在一起,维护起来十分麻烦。
JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation,用于对 Java Bean 中的字段的值进行验证。Hibernate Validator 则是Hibdernate提供的一种对该规范的实现。
使用spring-boot-starter-web的话,已经包含了hibernate-validator包的依赖,给开发人员提供了极大的便利。

pom.xml

<dependencies><dependency><groupId>org.springframework.bootgroupId><artifactId>spring-boot-starter-webartifactId>dependency>
dependencies>

校验规则

javax.validation.constraints规范中定义了很多校验规则:

标签说明
@AssertFalse只能为false
@AssertTrue只能为true
@DecimalMax必须小于或等于{value}
@DecimalMin必须大于或等于{value}
@Digits数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
@Email一个合法的电子邮件地址
@Future需要是一个将来的时间
@FutureOrPresent需要是一个将来或现在的时间
@Max最大不能超过{value}
@Min最小不能小于{value}
@Negative必须是负数
@NegativeOrZero必须是负数或零
@NotBlank不能为空字符
@NotEmpty不能为空元素
@NotNull不能为null
@Null必须为null
@Past需要是一个过去的时间
@PastOrPresent需要是一个过去或现在的时间
@Pattern需要匹配正则表达式"{regexp}"
@Positive必须是正数
@PositiveOrZero必须是正数或零
@Size元素个数必须在{min}和{max}之间

除了官方定义的规则,hibernate-validator自身也特有一套校验规则:

标签说明
@CreditCardNumber信用卡号码
@Currency货币格式
@EAN条形码
@Length字符长度需要在{min}和{max}之间
@ParametersScriptAssert执行脚本表达式"{script}"返回期望结果
@Range需要在{min}和{max}之间
@SafeHtml安全的HTML内容
@ScriptAssert可以用一些脚本来进行校验
@URL合法的URL
@DurationMax指定最大时间长度
@DurationMin指定最小时间长度

那么,规则是有了,怎么用呢?下面开始进入使用方法的讲解。

Controller层参数校验

应该大部分的参数校验都在对web请求过来的参数进行判断,在进入业务之前,这里是参数校验的重中之重。

VO类入参校验

如果是接口开发的话,一般来说大多数是以json格式接收的,然后用一个VO对象进行包装映射。
还有一种常见的是表单提交,同样也用一个VO对象进行参数包装。
它们的写法区别很简单,前者需要用@RequestBody修饰,后者不需要,而VO类的写法是没有任何区别的。

DemoVO.java

import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;@Data
public class DemoVO {@NotBlank(message="ID不能为空")private String id;@NotNull(message="Count不能为空")private Integer count;
}

DemoController.java

import javax.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("demo")
public class DemoController {@PostMapping("test")public void test(@RequestBody @Valid DemoVO demo) {}
}

/demo/test POST 一个空的JSON
则返回以下错误提示:

{"timestamp": "2021-03-02T08:56:06.301+0000","status": 400,"error": "Bad Request","errors": [{"codes": ["NotNull.demoVO.count","NotNull.count","NotNull.java.lang.Integer","NotNull"],"arguments": [{"codes": ["demoVO.count","count"],"arguments": null,"defaultMessage": "count","code": "count"}],"defaultMessage": "Count不能为空","objectName": "demoVO","field": "count","rejectedValue": null,"bindingFailure": false,"code": "NotNull"},{"codes": ["NotBlank.demoVO.id","NotBlank.id","NotBlank.java.lang.String","NotBlank"],"arguments": [{"codes": ["demoVO.id","id"],"arguments": null,"defaultMessage": "id","code": "id"}],"defaultMessage": "ID不能为空","objectName": "demoVO","field": "id","rejectedValue": null,"bindingFailure": false,"code": "NotBlank"}],"message": "Validation failed for object='demoVO'. Error count: 2","path": "/demo/test"
}

错误处理

针对以上不友好的错误提示,有两种方法处理:

内部处理

在方法中传入BindingResult 对象,在方法内部处理校验错误:

    @PostMapping("test")public void test(@RequestBody @Valid DemoVO demo, BindingResult result) {if (result.hasErrors()) {for (ObjectError error : result.getAllErrors()) {System.out.println(error.getDefaultMessage());}}}
全局处理

参数的校验失败会引发MethodArgumentNotValidException异常,所以可以使用@RestControllerAdvice来全局捕获这个异常,然后返回统一的格式。

Violation.java (定义错误描述类)

@Data
@AllArgsConstructor
public class Violation {private String fieldName;private String message;
}

ParamValidateHandlingControllerAdvice.java (定义通用异常拦截)

@RestControllerAdvice
public class ParamValidateHandlingControllerAdvice {@ExceptionHandler(MethodArgumentNotValidException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)List<Violation> onMethodArgumentNotValidException(MethodArgumentNotValidException e) {List<Violation> violations = new ArrayList<>();for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {violations.add(new Violation(fieldError.getField(), fieldError.getDefaultMessage()));}return violations;}
}

当再次请求一个空的json对象:{}时,将得到以下结果(http状态码为400)

[{"fieldName": "count","message": "Count不能为空"},{"fieldName": "id","message": "ID不能为空"}
]

参数校验分组

在实际开发中,不同的接口如果用相同的VO接收参数,但是校验的逻辑不同的时候,就可以用参数分组来实现:
GroupDemoVO.java

@Data
public class GroupDemoVO {interface Update {}@NotBlank(message = "ID不能为空", groups = {Update.class})private String id;@NotNull(message = "Count不能为空")private Integer count;
}

GroupDemoController.java

import org.springframework.validation.annotation.Validated;
import javax.validation.groups.Default;@RestController
@RequestMapping("demo")
public class GroupDemoController {@PostMapping("create")public void create(@RequestBody @Validated GroupDemoVO demo) {}@PostMapping("update")public void update(@RequestBody @Validated({Default.class, GroupDemoVO.Update.class} GroupDemoVO demo) {}
}

上面的代码表示调用create接口时,只使用未分组的校验规则:

[{"fieldName": "count","message": "Count不能为空"}
]

而调用update时,同时使用未分组 + Update分组的所有校验规则:

[{"fieldName": "count","message": "Count不能为空"},{"fieldName": "id","message": "ID不能为空"}
]

需要注意的是,方法参数列表中的@Valid要换成@Validated, 前者为javax校验框架提供,后者为Spring提供,它是前者的扩展,支持 group分组校验的写法,所以为了校验统一,尽量使用 @Validated

嵌套校验

上面的例子中,需校验的字段全部封装在一个类中,层级都是1层,那如果类中有需要验证的其他类呢,这就用到了嵌套校验,需要在嵌套的那个对象声明上加上@Valid标签:

DemoNestSubVO.java

@Data
public static class DemoNestSubVO {@NotBlankprivate String id;@NotNullprivate Integer count;
}

DemoNestParentVO.java

@Data
public class DemoNestParentVO {@Validprivate DemoNestSubVO sub;
}

GroupDemoController.java

@RestController
@RequestMapping("demo")
public class GroupDemoController {@PostMapping("nestParent")public void nestParent(@RequestBody @Validated DemoNestParentVO parentVO) {}
}

/demo/nestParent 发送一个JSON请求:{"sub": {}},将得到以下结果:

[{"fieldName": "sub.count","message": "不能为null"},{"fieldName": "sub.id","message": "不能为空"}
]

嵌套校验,也支持List等集合类型的包装:
DemoNestParentVO.java

@Data
public class DemoNestListVO {@Validprivate List<DemoNestSubVO> subList;
}

GroupDemoController.java


@RestController
@RequestMapping("demo")
public class GroupDemoController {@PostMapping("nestList")public void nestList(@RequestBody @Validated DemoNestListVO listVO) {}
}

/demo/nestList 发送一个JSON请求:{“subList”: [{“count”:1}, {“id”:“1”}]},subList有两个对象,一个只有id,一个只有count,将得到以下结果:

[{"fieldName": "subList[1].count","message": "不能为null"},{"fieldName": "subList[0].id","message": "不能为空"}
]

@Valid 与 @Validated

上面的例子中,@Valid@Validated有时候可以通用,有时候必须用其中一个,可能会绝对困惑,这里做一下总结:

项目@Valid@Validated
javax.validationorg.springframework.validation.annotation
作用范围参数、方法、构造器、字段参数、方法、类
支持分组是(作用于参数)
支持嵌套是(作用于字段)

校验模式

hibernate的校验模式有两种,默认是普通模式,会校验所有的字段,如果只要有一个校验失败就返回结果,则需使用快速失败返回模式:
ValidatorConfiguration.java

@Configuration
public class ValidatorConfiguration {@Beanpublic Validator validator() {ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure().failFast(true).buildValidatorFactory();Validator validator = validatorFactory.getValidator();return validator;}
}

再次运行,则返回一条校验错误消息:

[{"fieldName": "count","message": "Count不能为空"}
]

RequestParam参数校验

当表单提交的参数很少,常常会省略掉VO类的创建,直接用方法参数来接收,这时候的参数校验,需要在类定义上加上@Validated标签。

@RestController
@RequestMapping("demo")
@Validated
public class DemoController {@GetMapping("getOne")public void getOne(@RequestParam @NotBlank(message = "ID不能为空") String id) {}
}

当请求/demo/getOne的时候,由于@RequestParam默认是要求必须传入的,所以如果不传id, 会触发MissingServletRequestParameterException, 然后返回以下内容:

{"timestamp": "2021-03-03T02:10:16.342+0000","status": 400,"error": "Bad Request","message": "Required String parameter 'id' is not present","path": "/demo/getOne"
}

而当请求/demo/getOne?id的时候,就会触发参数验证了,由于id的值未传入,这里会触发ConstraintViolationException异常,并返回以下内容:

{"timestamp": "2021-03-03T02:14:49.552+0000","status": 500,"error": "Internal Server Error","message": "getOne.id: ID不能为空","path": "/demo/getOne"
}

需要注意的是这里是500异常,而不是400,参照之前的异常处理,这里也可以统一返回格式:

@RestControllerAdvice
public class ParamValidateHandlingControllerAdvice {@ExceptionHandler(ConstraintViolationException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)List<Violation> onConstraintViolationException(ConstraintViolationException e) {List<Violation> violations = new ArrayList<>();for (ConstraintViolation fieldError : e.getConstraintViolations()) {violations.add(new Violation(fieldError.getPropertyPath().toString(), fieldError.getMessage()));}return violations;}
}

这时候再请求/demo/getOne?id, 结果返回:

[{"fieldName": "getOne.id","message": "ID不能为空"}
]

那如何处理@RequestParam时候的异常呢,有3种方法:

1.修改@RequestParam的必须传入属性:@RequestParam(required = false)

2.直接把@RequestParam去掉

3.自定义统一异常处理

@RestControllerAdvice
public static class ParamValidateHandlingControllerAdvice {@ExceptionHandler(MissingServletRequestParameterException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)List<Violation> onConstraintViolationException(MissingServletRequestParameterException e) {List<Violation> violations = new ArrayList<>();violations.add(new Violation(e.getParameterName(), e.getMessage()));return violations;}
}
[{"fieldName": "id","message": "Required String parameter 'id' is not present"}
]

Service层参数校验

在上面的介绍种,已经将hibernate-validator的基本用法都讲完了,实际上不光在Controller层,只要是spring管理的bean中,都可以直接使用这种自动化的校验,需要做的仅仅是在类上加上@Validated注解,其他用法和上面的Controller一模一样。

@Service
@Validated
public class DemoService {public void getOne(@NotBlank String id) {}public void create(@Valid DemoVO demo) {}
}

程序触发校验逻辑

有些场景下可能需要程序触发校验逻辑,这时候可以创建一个Validator对象来进行手动验证:

@Service
public class DemoService {public void test() {DemoVO demo = new DemoVO();ValidatorFactory factory = Validation.buildDefaultValidatorFactory();Validator validator = factory.getValidator();Set<ConstraintViolation<DemoVO>> violations = validator.validate(demo);if (!violations.isEmpty()) {throw new ConstraintViolationException(violations);}}
}

在Spring环境中,Validator对象已经存在于容器中,所以可以通过自动注入的方式获取到Validator对象,上面的代码可以写成:

@Service
public class DemoService {@AutowiredValidator validator;public void test() {DemoVO demo = new DemoVO();Set<ConstraintViolation<DemoVO>> violations = validator.validate(demo);if (!violations.isEmpty()) {throw new ConstraintViolationException(violations);}}
}

自定义消息

上面这些校验规则的标签默认的错误消息是来自hibernate-validatator包下面的资源里:
在这里插入图片描述
如果要自定义这些消息,除了在使用的时候,指定message属性外,还可以在项目的/resources目录下创建ValidationMessages.properties自定义消息文件来实现, 比如:

javax.validation.constraints.NotBlank.message = 请输入参数

则提示消息变为:

[{"fieldName": "getOne.id","message": "请输入参数"}
]

自定义校验规则

虽然官方已经提供了很多常用的校验规则,但是千变万化的业务场景中,肯定有很多其他的检查逻辑,比如自定义一个IP地址的校验规则:
IpAddress.java

@Target({ FIELD })
@Retention(RUNTIME)
@Constraint(validatedBy = IpAddressValidator.class)
@Documented
public @interface IpAddress {String message() default "{IpAddress.invalid}";Class<?>[] groups() default { };Class<? extends Payload>[] payload() default { };}

IpAddressValidator.java

class IpAddressValidator implements ConstraintValidator<IpAddress, String> {@Overridepublic boolean isValid(String value, ConstraintValidatorContext context) {Pattern pattern = Pattern.compile("^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$");Matcher matcher = pattern.matcher(value);try {if (!matcher.matches()) {return false;} else {for (int i = 1; i <= 4; i++) {int octet = Integer.valueOf(matcher.group(i));if (octet > 255) {return false;}}return true;}} catch (Exception e) {return false;}}
}

某些场景下规则的标签和规则的校验逻辑不是在一个包下面的,这个时候可以通过配置Validator的方式来实现自定义映射,首先将validatedBy的值去除:
IpAddress.java

@Target({ FIELD })
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@Documented
public @interface IpAddress {String message() default "{IpAddress.invalid}";Class<?>[] groups() default { };Class<? extends Payload>[] payload() default { };}

ValidatorConfiguration.java

@Configuration
public static class ValidatorConfiguration {@Beanpublic Validator validator() {HibernateValidatorConfiguration configure = Validation.byProvider(HibernateValidator.class).configure();ConstraintMapping constraintMapping = configure.createConstraintMapping();constraintMapping.constraintDefinition(IpAddress.class).validatedBy(IpAddressValidator.class);ValidatorFactory validatorFactory = configure.failFast(true).addMapping(constraintMapping).buildValidatorFactory();Validator validator = validatorFactory.getValidator();return validator;}
}

还有一种方式是通过XML的形式来配置,在项目的/resources/META-INF/目录中分别建两个XML:

validation.xml


<validation-configxmlns="http://xmlns.jcp.org/xml/ns/validation/configuration"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/validation/configurationhttp://xmlns.jcp.org/xml/ns/validation/configuration/validation-configuration-2.0.xsd"version="2.0"><constraint-mapping>META-INF/constraint-mappings.xmlconstraint-mapping><property name="hibernate.validator.fail_fast">trueproperty>
validation-config>

constraint-mappings.xml


<constraint-mappingsxmlns="http://xmlns.jcp.org/xml/ns/validation/mapping"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/validation/mappinghttp://xmlns.jcp.org/xml/ns/validation/mapping/validation-mapping-2.0.xsd"version="2.0"><constraint-definition annotation="mypackage.IpAddress"><validated-by include-existing-validators="false"><value>mypackage.IpAddressValidatorvalue>validated-by>constraint-definition>
constraint-mappings>

注意:XML配置麻烦的地方就在于启动的时候会检验XML的正确性,这与hibernate-validator的版本有很大关系,上面是6.x版本中的DTD定义,如果是7.x版本的话,需要改成下面的定义:

validation.xml


<validation-configxmlns="https://jakarta.ee/xml/ns/validation/configuration"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="https://jakarta.ee/xml/ns/validation/configuration https://jakarta.ee/xml/ns/validation/validation-configuration-3.0.xsd"version="3.0"><constraint-mapping>META-INF/constraint-mappings.xmlconstraint-mapping><property name="hibernate.validator.fail_fast">trueproperty>
validation-config>

constraint-mappings.xml


<constraint-mappingsxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="https://jakarta.ee/xml/ns/validation/mapping https://jakarta.ee/xml/ns/validation/validation-mapping-3.0.xsd"xmlns="https://jakarta.ee/xml/ns/validation/mapping"version="3.0"><constraint-definition annotation="mypackage.IpAddress"><validated-by include-existing-validators="false"><value>mypackage.IpAddressValidatorvalue>validated-by>constraint-definition>
constraint-mappings>

至于每个版本的详细文档,可以到 https://docs.jboss.org/hibernate/validator 查找对应的目录。
在这里插入图片描述

总结

1.用类包装参数时,需要在对象前面加@Valid@Validated 修饰,后者支持分组校验,所以推荐统一使用@Validated;而使用参数列表校验时,需要在类定义上加@Validated修饰;如果一个字段需要嵌套校验,则这个字段加上@Valid注解。

2.不仅Controller层可以参数校验,其他Spring容器中的组件也都可以,只需在类上加@Validated修饰

3.可以通过统一异常处理类,来接收不同参数异常的统一返回:

  • @RequestParam修饰的参数未传入,抛MissingServletRequestParameterException异常,返回400错误
  • Spring MVC Controller层的方法经过类包装的参数验证未通过,抛MethodArgumentNotValidException异常,返回400错误
  • 自动校验(类上有@Validated修饰)校验未通过, 抛ConstraintViolationException异常, 返回500错误
  • 程序触发的校验,本身不会发生异常,但是可以和上面一样主动抛出ConstraintViolationException异常, 返回500错误

4.可以通过自定义校验规则来扩展修饰标签,可以通过程序或XML的形式对标签和规则处理类进行绑定

5.通过设置failFast(false)hibernate.validator.fail_fast=true的方式,使出现第一个校验失败的时候,就返回错误消息

参考

  • https://reflectoring.io/bean-validation-with-spring-boot
  • https://www.cnblogs.com/mooba/p/11276062.html
  • https://www.cnblogs.com/mr-yang-localhost/p/7812038.html


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部