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、