Java 8
Spring Boot 2.5.3
---
授人以渔
1、Spring Framework官方文档(有PDF下载)
Core文档下的:Chapter 3. Validation, Data Binding, and Type Conversion
2、Spring Boot官方文档(有PDF下载)
章节:4.17. Validation
本文介绍在Spring Boot应用中对请求参数进行校验。
正确的数据校验,可以避免脏数据、非法数据写入系统,也可以阻断一些不正确请求的操作。
Spring框架、Spring Boot对数据校验提供了相应的支持,可以大大简化数据校验过程,除了对请求参数进行检查,还可以对应用中方法的参数进行检查。
目录
试验2:DTO + javax.validation.Valid注解
试验3:DTO + org.springframework.validation.annotation.Validated注解
试验4:org.springframework.validation.annotation.Validated注解 到 Controller
@GetMapping(value="/hello")
public String hello(@RequestParam String name) {
// 校验
if (!StringUtils.hasText(name)) {
// name为空
throw new RuntimeException("name不能为空");
}
final int nameMaxLen = 100;
if (name.length() > nameMaxLen) {
// 最大长度校验
// 抛出异常
throw new RuntimeException("name长度超过" + nameMaxLen);
}
return "Hello, " + name;
}
调用接口 /web/hello(试验Postman),输入触发校验的参数。
得到下面的响应结果:Internal Server Error
{
"timestamp": "2021-09-26T01:59:32.938+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/web/hello"
}
应用日志显示下面的错误:来自博客园
# @RequestParam 的 required 属性 默认为 true导致
Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter 'name' for method parameter type String is not present]
# 代码中校验失败抛出异常
java.lang.RuntimeException: name不能为空
java.lang.RuntimeException: name长度超过100
试验2:DTO + javax.validation.Valid注解
为了方便参数校验,Spring 框架整合了数据校验的功能——不仅仅包含请求参数校验,通过使用注解大大简化参数的校验。
在S.B.应用中,添加下面的依赖包即可使用相关功能:spring-boot-starter-validation
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
注,暂时没有找到不使用DTO即可对Get请求的参数进行校验的方式。
改造接口:
这里的NameDTO前面没有 @RequestParam等注解;来自博客园
使用 javax.validation.Valid 注解;
@GetMapping(value="/hello2")
public String hello2(@Valid NameDTO dto) {
return "Hello, " + dto.getName();
}
添加NameDTO:
// @Data 为 lombok注解
@Data
public class NameDTO {
@NotBlank(message="name不能为空")
@Size(max=100, message="name长度不能超过100")
private String name;
}
调用 接口 /web/hello2,输入触发校验的参数。得到下面的响应:之前因为抛出异常,status是500,现在变为400,更符合参数校验失败状态码了。来自博客园
{
"timestamp": "2021-09-26T02:51:05.988+00:00",
"status": 400,
"error": "Bad Request",
"path": "/web/hello2"
}
应用错误日志如下:出现异常 org.springframework.validation.BindException,和之前的不同了
# 不输入任何参数
.w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'nameDTO' on field 'name': rejected value [null]; codes [NotEmpty.nameDTO.name,NotEmpty.name,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [nameDTO.name,name]; arguments []; default message [name]]; default message [name不能为空]]
# 输入参数 name为空或由空字符组成
Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'nameDTO' on field 'name': rejected value [ ]; codes [NotBlank.nameDTO.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [nameDTO.name,name]; arguments []; default message [name]]; default message [name不能为空]]
# 输入参数 name长度超过100
Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'nameDTO' on field 'name': rejected value [...省略参数值...]; codes [Size.nameDTO.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [nameDTO.name,name]; arguments []; default message [name],100,0]; default message [name长度不能超过100]]
Valid注解 源码:来自博客园
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface Valid {
}
试验3:DTO + org.springframework.validation.annotation.Validated注解
改造接口: 把 试验2 的 @Valid 改为 @Validated
@GetMapping(value="/hello3")
public String hello3(@Validated NameDTO dto) {
return "Hello, " + dto.getName();
}
调用 接口 /web/hello3,输入触发校验的参数。得到下面的响应:和试验2相同
{
"timestamp": "2021-09-26T03:03:20.457+00:00",
"status": 400,
"error": "Bad Request",
"path": "/web/hello3"
}
应用的错误日志:和试验2相同(下面仅展示其中1条)来自博客园
# 其中一条错误日志
Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'nameDTO' on field 'name': rejected value [null]; codes [NotBlank.nameDTO.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [nameDTO.name,name]; arguments []; default message [name]]; default message [name不能为空]]
Validated 注解源码:
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
Class<?>[] value() default {};
}
试验4:org.springframework.validation.annotation.Validated注解 到 Controller
前面提到,没有找到直接在 请求方法的参数中使用校验注解进行校验的方法,现在,找到了!
改造接口:
/web2/ 开头的接口;
@Validated 的位置,在Controller类上,而不是 方法上;
@RestController
@RequestMapping(value="/web2")
@Validated
public class Web2Controller {
@GetMapping(value="/hello")
public String hello(@NotBlank(message="name不能为空") @Size(max=100, message="name长度超过100") String name) {
return "Hello, " + name;
}
}
调用 /web2/hello 接口,输入触发校验的规则。
得到的响应如下:status不是 400,又变成 500了。
{
"timestamp": "2021-09-26T03:18:40.083+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/web2/hello"
}
应用的错误日志:此时的异常 是 javax.validation.ConstraintViolationException
# 输入name长度超过100
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: hello.name: name长度超过100] with root cause
javax.validation.ConstraintViolationException: hello.name: name长度超过100
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120) ~[spring-context-5.3.9.jar:5.3.9]
# 不输入name 或 输入name由空字符组成
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: hello.name: name不能为空] with root cause
javax.validation.ConstraintViolationException: hello.name: name不能为空
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120) ~[spring-context-5.3.9.jar:5.3.9]
在本试验中,没有给参数name加上@RequestParam,因此,不传name时,提示的是 “name不能为空”。
加上@RequestParam会怎样?
# 没有name参数
GET localhost:8080/web2/hello
# 响应 400
{
"timestamp": "2021-09-26T03:43:26.725+00:00",
"status": 400,
"error": "Bad Request",
"path": "/web2/hello"
}
# 应用日志 WARN级别
Resolved [org.springframework.web.bind.MissingServletRequestParameterException:
Required request parameter 'name' for method parameter type String is not present]
加上了是有效的,这样的话,就不影响了——在前面 试验2、3 中使用了 DTO方式,那时是不能加 @RequestParam 注解的,否则要传入的是参数 dto。
Get请求多参数(2个)校验
开发 /web2/hello2 接口:新增参数 age
@GetMapping(value="/hello2")
public String hello2(@RequestParam @NotBlank(message="name不能为空") @Size(max=100, message="name长度超过100") String name,
@RequestParam @Min(value=0, message="age必须大于等于0") @Max(value=150, message="age必须小于等于150") Integer age) {
return "Hello, " + name + ", you are " + age;
}
校验结果:符合预期。
GET请求的参数放入DTO中
试验4这种校验方式 可以使用 @RequestParam注解,但是,在方法签名中给每个参数添加注解显得比较臃肿,将这些参数及校验注解放到DTO中,会让接口显得很清爽。
采用试验4 这种 @Validated 放到Controller类上,如下面这样改造接口,测试失败——未执行校验:
// Web2Controller.java
@GetMapping(value="/hello3")
public String hello3(Name2DTO dto) {
return "Hello, " + dto.getName() + ", you are " + dto.getAge();
}
// Name2DTO.java
@Data
public class Name2DTO {
@NotBlank(message="name不能为空")
@Size(max=100, message="name长度不能超过100")
private String name;
@NotNull(message="age不能为null")
@Min(value=0, message="age必须≥0")
@Max(value=150, message="age必须≤150")
private Integer age;
}
上面的接口未执行校验:
GET localhost:8080/web2/hello3
响应:
Hello, null, you are null
前面试验2、试验3中,给dto参数直接添加 @Validated、@Valid 可以进行校验,这里是否可以呢?
改造:2、3都是可行的——执行了指定的校验,并且响应的status为400,符合预期。
@GetMapping(value="/hello3")
// 1、未做校验
// public String hello3(Name2DTO dto) {
// 2、@Validated 做了校验,返回400
// public String hello3(@Validated Name2DTO dto) {
// 3、@Valid 做了校验,返回400
public String hello3(@Valid Name2DTO dto) {
return "Hello, " + dto.getName() + ", you are " + dto.getAge();
}
疑问:2、3两种方式都可以,两者有什么区别呢?TODO
GET请求试验DTO方式时,没有使用@RequestParam注解,此时,除了请求参数可以放到url中,还可以放到form表单中。
怎么限制——不允许表单方式提交数据——呢?TODO
试验配置 @GetMapping 的 consumes=“text/plain”,但提交Get请求时失败了:
[org.springframework.web.HttpMediaTypeNotSupportedException: Content type '' not supported]
---210926 1221---
其实和前面GET请求的校验一样,只不过,请求参数是DTO形式,并且参数dto使用了@RequestBody注解。
// WebController.java
@PostMapping(value="/hello4")
public String hello4(@RequestBody @Validated Hello4DTO dto) {
return "Hello, " + dto.getName();
}
// Hello4DTO.java
@Data
public class Hello4DTO {
@NotBlank(message="name不能为空")
@Size(max=100, message="name长度不能超过100")
private String name;
}
调用 /web/hello4 接口,使用POST,传入错误的参数触发校验:
产生 org.springframework.web.bind.MethodArgumentNotValidException 异常,这和 试验2、3的GET请求时不同(之前是BindException)
POST localhost:8080/web/hello4
参数:
{
"name": "iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"
}
响应:
{
"timestamp": "2021-09-26T05:11:59.298+00:00",
"status": 400,
"error": "Bad Request",
"path": "/web/hello4"
}
错误日志:
Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public
java.lang.String org.lib.webvalidation.controller.WebController.hello4(org.lib.webvalidation.dto.Hello4DTO):
[Field error in object 'hello4DTO' on field 'name': rejected value [ ]; codes [NotBlank.hello4DTO.name,NotBlank.name,
NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes
[hello4DTO.name,name]; arguments []; default message [name]]; default message [name不能为空]] ]
Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public
java.lang.String org.lib.webvalidation.controller.WebController.hello4(org.lib.webvalidation.dto.Hello4DTO):
[Field error in object 'hello4DTO' on field 'name': rejected value [..省略...]; codes [Size.hello4DTO.name,Size.name,
Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
codes [hello4DTO.name,name]; arguments []; default message [name],100,0]; default message [name长度不能超过100]] ]
在前面的试验中,返回给调用方的信息是400、500等,没有提示到底出了什么错误。
本节介绍两种方式来将具体的参数错误信息返回给调用方。
本节仅处理 试验2、3 和 试验5 的异常(org.springframework.validation.BindException、org.springframework.web.bind.MethodArgumentNotValidException)。
检查发现,MethodArgumentNotValidException 继承了 BindException:
拦截参数校验中的异常,再将异常的信息返回给调用方。
第一次尝试:返回信息太多,不符合预期
// AppExceptionHandler.java
// 方式1:1个注解
//@RestControllerAdvice
// 方式2:2个注解
@ControllerAdvice
@ResponseBody
@Slf4j
public class AppExceptionHandler {
@ExceptionHandler(value = {BindException.class})
public String handleRequestValid(BindException be) {
log.warn("请求参数异常:be={}, {}", be.getClass(), be.getMessage());
return "参数异常:" + be.getMessage();
}
}
测试 GET localhost:8080/web/hello3,响应:
参数异常:org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'nameDTO' on field 'name': rejected value [null]; codes [NotBlank.nameDTO.name,NotBlank.name,
NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
codes [nameDTO.name,name]; arguments []; default message [name]]; default message [name不能为空]
第二次尝试:改造handleRequestValid函数的返回值
@ExceptionHandler(value = {BindException.class})
public String handleRequestValid(BindException be) {
log.warn("请求参数异常:be={}, {}", be.getClass(), be.getMessage());
BindingResult br = be.getBindingResult();
List<ObjectError> oel = br.getAllErrors();
StringBuffer sb = new StringBuffer();
sb.append("参数错误:");
oel.forEach(oe->{
sb.append(oe.getDefaultMessage() + ";");
});
return sb.toString();
}
此时访问 localhost:8080/web/hello3,返回:错误信息总算出来了
参数错误:name不能为空;
访问 localhost:8080/web/hello4:
- 不传 @请求体 时,返回 错误信息,日志发生 HttpMessageNotReadableException 异常:还需要完善异常拦截
{
"timestamp": "2021-09-26T05:51:00.154+00:00",
"status": 400,
"error": "Bad Request",
"path": "/web/hello4"
}
此时的日志错误:
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing:
public java.lang.String org.lib.webvalidation.controller.WebController.hello4(org.lib.webvalidation.dto.Hello4DTO)]
- 传入请求体,但没有任何参数:返回了 参数错误的信息,符合预期
POST localhost:8080/web/hello4
参数:
{
}
响应:
参数错误:name不能为空;
要是存在多个参数存在错误呢?上面的拦截方式会把 所有参数的错误信息返回。
// 增加参数
@Data
public class Hello4DTO {
@NotBlank(message="name不能为空")
@Size(max=100, message="name长度不能超过100")
private String name;
@NotNull(message="age不能为null")
@Range(min=0, max=150, message="age范围:[0,150]")
private Integer age;
}
// 更新接口返回值
@PostMapping(value="/hello4")
public String hello4(@RequestBody @Validated Hello4DTO dto) {
return "Hello, " + dto.getName() + ", you are " + dto.getAge();
}
执行 传入请求体,但没有任何参数,返回:两个参数的错误原因都返回给调用方了
参数错误:age不能为null;name不能为空;
不拦截 HttpMessageNotReadableException等异常
这是由于 @RequestBody 默认的 required=true 导致的——/web/hello4 没有传请求体。
这时要怎么拦截呢?拦截了要返回什么信息呢?
在使用 @RequestParam 时,此时不传参数,会产生下面的异常:MissingServletRequestParameterException
Resolved [org.springframework.web.bind.MissingServletRequestParameterException:
Required request parameter 'name' for method parameter type String is not present]
上面两种异常都要拦截的话,怎么做?
从两者的类继承来看,几乎没有关系,要一个一个处理吗?
public class HttpMessageNotReadableException extends HttpMessageConversionException {
public class MissingServletRequestParameterException extends MissingRequestValueException {
除了上面的 @RequestBody、 @RequestParam会导致异常外,还有其它几个,每一个都要处理吗?代码就太多了。
这种情况可以不处理,不拦截。
已经要求传参数了,可调用方就是不传,发生了错误,就返回错误信息好了:
{
"timestamp": "2021-09-26T06:09:12.663+00:00",
"status": 400,
"error": "Bad Request",
"path": "/web/hello4"
}
前面使用拦截异常控制了返回的请求,其中,返回的信息来自异常的一个BindingResult对象。
也可以在方法中直接使用 BindingResult来返回具体校验信息。
注意,使用方法2时,先注释掉 方法1 的 AppExceptionHandler。
新增接口:/web/hello5,参数中增加 BindingResult bresult
@PostMapping(value="/hello5")
public String hello5(@RequestBody @Validated Hello4DTO dto, BindingResult bresult) {
if (bresult.hasErrors()) {
// 参数校验错误处理:返回所有错误信息
StringBuffer sb = new StringBuffer();
List<ObjectError> oel = bresult.getAllErrors();
sb.append("API中-参数错误:");
oel.forEach(oe->{
sb.append(oe.getDefaultMessage() + ";");
});
return sb.toString();
}
return "Hello, " + dto.getName() + ", you are " + dto.getAge();
}
调用结果:实现了校验,符合预期。
关于BindingResult更多原理性的东西,可以看官文。来自博客园
对了,除了返回所有校验错误信息外,也可以只返回一条错误信息。
方式1、方式2 同时使用时,返回哪个的信息呢?
方式2的!
因为此时参数校验失败的异常已经被处理了,不会抛出到 全局异常拦截层。来自博客园
前面使用了 @NotBlank、@Size 等注解,还有哪些注解可以使用呢?
上面的注解来自 javax.validation包,在前面使用的 @Range注解 则来自 org.hibernate.validator.constraints 包,这个包下有哪些用来做参数校验的注解呢?
除了上面的两个包中的注解,是否还有其它Spring框架自带的注解呢?TODO
是否可以自定义校验注解呢?TODO
是否可以自定义校验规则——非注解方式——呢?TODO
关于这些问题,需要看看官方文档,里面还有更详细的介绍。来自博客园
对于参数校验,还需要知道的是,spring框架是如何把 入参和参数做绑定的,官文中也有详细的介绍。
》》》全文完《《《
参考文档
作者: 木白的菜园
作者: 唯一浩哥
3、