参数校验是开发中非常重要,也是非常繁琐的一件事情,不得不做,但是确无聊至极,而且和业务代码混杂在一起,维护起来十分麻烦。
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}位小数范围内) |
一个合法的电子邮件地址 | |
@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 | 指定最小时间长度 |
那么,规则是有了,怎么用呢?下面开始进入使用方法的讲解。
应该大部分的参数校验都在对web请求过来的参数进行判断,在进入业务之前,这里是参数校验的重中之重。
如果是接口开发的话,一般来说大多数是以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 {
@NotBlank
private String id;
@NotNull
private Integer count;
}
DemoNestParentVO.java
@Data
public class DemoNestParentVO {
@Valid
private 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 {
@Valid
private 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 |
---|---|---|
包 | javax.validation | org.springframework.validation.annotation |
作用范围 | 参数、方法、构造器、字段 | 参数、方法、类 |
支持分组 | 否 | 是(作用于参数) |
支持嵌套 | 是(作用于字段) | 否 |
hibernate的校验模式有两种,默认是普通模式,会校验所有的字段,如果只要有一个校验失败就返回结果,则需使用快速失败返回模式:
ValidatorConfiguration.java
@Configuration
public class ValidatorConfiguration {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
}
再次运行,则返回一条校验错误消息:
[
{
"fieldName": "count",
"message": "Count不能为空"
}
]
当表单提交的参数很少,常常会省略掉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"
}
]
在上面的介绍种,已经将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 {
@Autowired
Validator 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> {
@Override
public 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 {
@Bean
public 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-config
xmlns="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/configuration
http://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-mappings
xmlns="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/mapping
http://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-config
xmlns="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-mappings
xmlns: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错误MethodArgumentNotValidException
异常,返回400错误@Validated
修饰)校验未通过, 抛ConstraintViolationException
异常, 返回500错误ConstraintViolationException
异常, 返回500错误4.可以通过自定义校验规则来扩展修饰标签,可以通过程序或XML的形式对标签和规则处理类进行绑定
5.通过设置failFast(false)
或hibernate.validator.fail_fast=true
的方式,使出现第一个校验失败的时候,就返回错误消息