springboot 与数据验证
一. 简述
在项目开发中,验证参数也是最经常使用的业务需求了。通常在开发的时候都需要根据业务需求,对参数进行必要验证。 当然一堆的 if-else
的验证在日常开发中时常可见。这种方式非常不友好: 1. 代码太长导致阅读不友好,更改需求可能只是简单的修改但是却需要阅读几十到几百行的代码 2. 有时候业务只是一两句但是验证代码却占用了很长时间 JSR-303
是 Java
的验证规范,早期是在 Hibernate
框架中实现的,后面被抽取到 Java
体系。Spring-Boot
使用了 hibernate-validator
验证器,所以也包含了 JSR-303
在里面。当需要进行参数验证的时候,只需要几个注解即可实现复杂的验证。
GitHub:https://github.com/WeidanLi/spring-boot-tutorial 项目示例:web-validate
二. 开发
1. 引入依赖
1 2 3 4 5
| <!-- 引入 web-starter, 已经包含 hibernate-validator 验证器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
|
2. 使用注解开发控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @RestController @Validated public class UserEndpoint {
@PostMapping public void create(@Validated @RequestBody UserDto userDto) { System.out.println("-----> add: " + userDto); }
@GetMapping public UserDto queryByName(@Length(min = 1, max = 8) @RequestParam(value = "name") String name) { System.out.println(name); UserDto userDto = new UserDto(); userDto.setId(100); userDto.setUserName("狗蛋"); userDto.setAge(19); return userDto; }
}
|
PS: 1. 第一个接口使用 DTO
的形式进行验证,验证注解将会被使用在 DTO
上,DTO
如下 2. 第二个接口直接在接口参数进行验证 UserDto:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
public class UserDto {
private Integer id;
@NotBlank private String userName;
@NotNull private Integer age; }
|
3. 请求两个接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| POST http://localhost:8080
HTTP/1.1 400 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Fri, 14 Dec 2018 01:41:46 GMT Connection: close
{ "timestamp": "2018-12-14T01:41:46.244+0000", "status": 400, "error": "Bad Request", "errors": [ ... 这里不做详细演示因为下面要配合控制器监听把这个错误给修改掉 ] }
----------------------------------------------------------
GET http://localhost:8080?name=
HTTP/1.1 500 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Fri, 14 Dec 2018 01:42:55 GMT Connection: close
{ "timestamp": "2018-12-14T01:42:55.654+0000", "status": 500, "error": "Internal Server Error", "message": "queryByName.name: 长度需要在1和8之间", "path": "/" }
Response code: 500; Time: 29ms; Content length: 141 bytes
|
可以看到,Spring
已经对数据按照我们的需求进行了验证,但是返回的错误信息并不是很友好,我们可以使用前面提到的 controller-advice
进行拦截返回
4. 定制返回验证错误信息
从控制台可以看到,验证不通过的时候抛出的异常是 ConstraintViolationException
和 MethodArgumentNotValidException
异常,所以在监听器里只需要对这个异常进行监听,获取 message
属性进行返回即可得知错误的原因。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @ControllerAdvice @RestController public class EndpointAdvice {
@ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Map<String, String> errorParam(ConstraintViolationException e) { Map<String, String> error = new HashMap<>(2); error.put("message", e.getMessage()); return error; }
@ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Map<String, String> errorDto(MethodArgumentNotValidException e) { Map<String, String> error = new HashMap<>(2); StringBuilder message = new StringBuilder(); for (ObjectError allError : e.getBindingResult().getAllErrors()) { message.append(allError.getDefaultMessage()).append("; "); } error.put("message", message.toString()); return error; }
}
|
重新请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| POST http://localhost:8080
HTTP/1.1 400 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Fri, 14 Dec 2018 01:59:57 GMT Connection: close
{ "message": "名字不允许为空; 年龄不允许为空; " }
Response code: 400; Time: 166ms; Content length: 28 bytes
--------------------------------------------------------- GET http://localhost:8080?name=
HTTP/1.1 400 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Fri, 14 Dec 2018 01:48:23 GMT Connection: close
{ "message": "queryByName.name: 长度需要在1和8之间" }
Response code: 400; Time: 151ms; Content length: 42 bytes
|
5. 分组验证
DTO
如果验证不能自由的话,那么DTO
的复用性就变差了,总不能一个接口都要重新写属性相同但是验证不相同的 DTO
吧 所以,JSR-303
的每个验证注解上都有一个 groups
属性,传入 Class
数组(多组验证),用于定义分组验证的接口(但是这个接口没有实际实现,只是以 Java
类来做分组罢了)。 DTO
修改为更新的时候要求 id
不能为空,新增时名字和年龄不能为空。
1 2 3 4 5 6 7 8 9 10 11
| public class UserDto {
@NotNull(groups = Update.class) private Integer id;
@NotBlank(message = "名字不允许为空", groups = Add.class) private String userName;
@NotNull(message = "年龄不允许为空", groups = Add.class) private Integer age; }
|
接口修改,验证器传入分组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @RestController @Validated public class UserEndpoint {
@PostMapping public void create(@Validated({Add.class}) @RequestBody UserDto userDto) { System.out.println("-----> add: " + userDto); }
@PutMapping public void update(@Validated({Update.class}) @RequestBody UserDto userDto) { System.out.println("-----> update: " + userDto); }
}
|
重新请求接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| POST http://localhost:8080
HTTP/1.1 400 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Fri, 14 Dec 2018 02:11:14 GMT Connection: close
{ "message": "年龄不允许为空; 名字不允许为空; " }
Response code: 400; Time: 11ms; Content length: 32 bytes
---------------------------------------------------------
PUT http://localhost:8080
HTTP/1.1 400 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Fri, 14 Dec 2018 02:12:06 GMT Connection: close
{ "message": "不能为null; " }
Response code: 400; Time: 194ms; Content length: 23 bytes
|
6. 注解使用说明
每个注解都拥有两个属性:
- groups 用于定义这个注解要应用于哪些分组的
- message 用于定义不符合要求时要输出的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| 注解
说明
@Null
被注释的元素必须为
@NotNull
被注释的元素必须不为null
@AssertTrue
被注释的元素必须为true
@AssertFalse
被注释的元素必须为false
@Min(value)
被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value)
被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value)
被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value)
被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max=,min=)
被注释的元素的大小必须在指定的范围内
@Digits
被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past
被注释的元素必须是一个过去的日期
@Future
被注释的元素必须是一个将来的日期
@Pattern(regex=,flag=)
被注释的元素必须符合指定的正则表达式
Hibernate
Validator
@NotBlank
验证字符串非null,且长度必须大于0
@Email
被注释的元素必须是电子邮箱地址
@Length(min=,max=)
被注释的字符串的大小必须在指定的范围内
@NotEmpty
被注释的字符串的必须非空
@Range(min=,max=,message=)
被注释的元素必须在合适的范围内
|
7. 开发自定义校验器
很多时候,框架自带的功能并不能或者说不方便实现我们想要的功能的时候,我们就可以开始考虑自己造轮子,开发属于我们所在领域的校验器。 简单来说,开发流程就是以下两个步骤:
- 编写约束注解,填充需要的属性以及官方必须要求的属性;
- 编写校验器,对目标值配合注解上的属性进行验证。
7.1 定义校验注解
为了示范,我定义一个验证目标字符串需要纯中文字符的校验注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
@Constraint(validatedBy = { ChsValidator.class }) @Target(ElementType.FIELD) @Retention(RUNTIME) @Documented public @interface Chs {
String chineseName();
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
String message() default "该字段需要纯中文";
}
|
7.2 定义校验器
校验器需要去实现 ConstraintValidator
接口,传入两个泛型,第一个泛型是注解类型,第二个是目标值的类型。实现两个方法,校验器调用的校验在 isValid
中进行,如果校验通过返回 True
不同过则返回 False
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class ChsValidator implements ConstraintValidator<Chs, String> {
private String reg = "^[\\u0391-\\uFFE5]+[\\w*[\\u0391-\\uFFE5]*]*";
@Override public void initialize(Chs constraintAnnotation) {
}
@Override public boolean isValid(String value, ConstraintValidatorContext context) { if (null == value) { return false; } return value.matches(reg); }
}
|
7.3 测试自定义校验器
现在需要一个控制器接口来测试
1 2 3 4
| @PostMapping("testChsValidator") public void testChsValidator(@Chs @RequestParam String chsName) {
}
|
请求访问测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| POST http://localhost:8080/testChsValidator?chsName=888
HTTP/1.1 400 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Tue, 18 Dec 2018 01:39:40 GMT Connection: close
{ "message": "testChsValidator.chsName: 该字段需要纯中文" }
Response code: 400; Time: 239ms; Content length: 48 bytes
--------------------------------------------------------
POST http://localhost:8080/testChsValidator?chsName=你好
HTTP/1.1 200 Content-Length: 0 Date: Tue, 18 Dec 2018 01:40:11 GMT
<Response body is empty>
Response code: 200; Time: 44ms; Content length: 0 bytes
|
三. 总结
OK,验证器的使用到这里基本完结,一般来说基础的可以适用大部分需求,但是我建议如果某个校验规则用的不多的话,就不大需要定义自定义校验器,如果用的比较多而且是通用的话,那么自定义的注解校验无疑会带来很多便捷。校验器适用的实现是 Hibernate Validate
所以如若需要查询资料可直接查询Hibernate Validate
相关的即可。 Hibernate-Validate