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、

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM