spring boot项目18:请求参数校验

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对数据校验提供了相应的支持,可以大大简化数据校验过程,除了对请求参数进行检查,还可以对应用中方法的参数进行检查。

目录

试验1:硬编码

试验2:DTO + javax.validation.Valid注解

试验3:DTO + org.springframework.validation.annotation.Validated注解

试验4:org.springframework.validation.annotation.Validated注解 到 Controller

试验5:POST请求的参数校验

试验6:返回参数校验失败信息给调用方

方式1:拦截异常

方式2:使用BindingResult

试验1:硬编码

	@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---

试验5:POST请求的参数校验

其实和前面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]] ]

试验6:返回参数校验失败信息给调用方

在前面的试验中,返回给调用方的信息是400、500等,没有提示到底出了什么错误。

本节介绍两种方式来将具体的参数错误信息返回给调用方

本节仅处理 试验2、3 和 试验5 的异常(org.springframework.validation.BindException、org.springframework.web.bind.MethodArgumentNotValidException)。

检查发现,MethodArgumentNotValidException 继承了 BindException:

方式1:拦截异常

拦截参数校验中的异常,再将异常的信息返回给调用方。

第一次尝试:返回信息太多,不符合预期

// 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"
}

方式2:使用BindingResult

前面使用拦截异常控制了返回的请求,其中,返回的信息来自异常的一个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框架是如何把 入参和参数做绑定的,官文中也有详细的介绍。

》》》全文完《《《

参考文档

1、SpringBoot 参数校验的方法

作者: 木白的菜园

2、Spring基础系列-参数校验

作者: 唯一浩哥

3、

原文地址:https://www.cnblogs.com/luo630/p/15337347.html