SpringBoot中BeanValidation資料校驗與優雅處理詳解
阿新 • • 發佈:2020-11-16
[toc]
## 本篇要點
> JDK1.8、SpringBoot2.3.4release
- 說明後端引數校驗的必要性。
- 介紹**如何使用validator進行引數校驗**。
- 介紹@Valid和@Validated的區別。
- 介紹**如何自定義約束註解**。
- 關於Bean Validation的前世今生,建議閱讀文章:[ 不吹不擂,第一篇就能提升你對Bean Validation資料校驗的認知](https://www.yourbatman.cn/x2y/55d56c0b.html),介紹十分詳細。
## 後端引數校驗的必要性
在開發中,從表現層到持久化層,資料校驗都是一項邏輯差不多,但容易出錯的任務,
前端框架往往會採取一些檢查引數的手段,比如校驗並提示資訊,那麼,既然前端已經存在校驗手段,後端的校驗是否還有必要,是否多餘了呢?
並不是,正常情況下,引數確實會經過前端校驗傳向後端,但**如果後端不做校驗,一旦通過特殊手段越過前端的檢測,系統就會出現安全漏洞。**
## 不使用Validator的引數處理邏輯
既然是引數校驗,很簡單呀,用幾個`if/else`直接搞定:
```java
@PostMapping("/form")
public String form(@RequestBody Person person) {
if (person.getName() == null) {
return "姓名不能為null";
}
if (person.getName().length() < 6 || person.getName().length() > 12) {
return "姓名長度必須在6 - 12之間";
}
if (person.getAge() == null) {
return "年齡不能為null";
}
if (person.getAge() < 20) {
return "年齡最小需要20";
}
// service ..
return "註冊成功!";
}
```
寫法乾脆,但`if/else`太多,過於臃腫,更何況這只是區區一個介面的兩個引數而已,要是需要更多引數校驗,甚至更多方法都需要這要的校驗,這程式碼量可想而知。於是,這種做法顯然是不可取的,我們可以利用下面這種更加優雅的引數處理方式。
## Validator框架提供的便利
> **Validating data** is a common task that occurs throughout all application layers, from the presentation to the persistence layer. **Often the same validation** logic is implemented in each layer which is time consuming and error-prone.
如果依照下圖的架構,對每個層級都進行類似的校驗,未免過於冗雜。
![](https://img2020.cnblogs.com/blog/1771072/202011/1771072-20201116130324684-639762727.png)
> Jakarta Bean Validation 2.0 - **defines a metadata model and API for entity and method validation**. The default metadata source are **annotations**, with the ability to override and extend the meta-data through the use of XML.
>
> The API is not tied to a specific application tier nor programming model. It is specifically not tied to either web or persistence tier, and is available for both server-side application programming, as well as rich client Swing application developers.
`Jakarta Bean Validation2.0`定義了一個元資料模型,為實體和方法提供了資料驗證的API,預設將註解作為源,可以通過XML擴充套件源。
![](https://img2020.cnblogs.com/blog/1771072/202011/1771072-20201116130330887-563455677.png)
## SpringBoot自動配置ValidationAutoConfiguration
`Hibernate Validator`是` Jakarta Bean Validation`的參考實現。
在SpringBoot中,只要類路徑上存在JSR-303的實現,如`Hibernate Validator`,就會自動開啟Bean Validation驗證功能,這裡我們只要引入`spring-boot-starter-validation`的依賴,就能完成所需。
```xml
org.springframework.boot
spring-boot-starter-validation
```
目的其實是為了引入如下依賴:
```xml
org.glassfish
jakarta.el
3.0.3
compile
org.hibernate.validator
hibernate-validator
6.1.5.Final
compile
```
SpringBoot對BeanValidation的支援的自動裝配定義在`org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration`類中,提供了預設的`LocalValidatorFactoryBean`和支援方法級別的攔截器`MethodValidationPostProcessor`。
```java
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean(Validator.class)
public static LocalValidatorFactoryBean defaultValidator() {
//ValidatorFactory
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
return factoryBean;
}
// 支援Aop,MethodValidationInterceptor方法級別的攔截器
@Bean
@ConditionalOnMissingBean
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
@Lazy Validator validator) {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
processor.setProxyTargetClass(proxyTargetClass);
// factory.getValidator(); 通過factoryBean獲取了Validator例項,並設定
processor.setValidator(validator);
return processor;
}
}
```
## Validator+BindingResult優雅處理
> 預設已經引入相關依賴。
### 為實體類定義約束註解
```java
/**
* 實體類欄位加上javax.validation.constraints定義的註解
* @author Summerday
*/
@Data
@ToString
public class Person {
private Integer id;
@NotNull
@Size(min = 6,max = 12)
private String name;
@NotNull
@Min(20)
private Integer age;
}
```
### 使用@Valid或@Validated註解
@Valid和@Validated在Controller層做方法引數校驗時功能相近,具體區別可以往後面看。
```java
@RestController
public class ValidateController {
@PostMapping("/person")
public Map validatePerson(@Validated @RequestBody Person person, BindingResult result) {
Map map = new HashMap<>();
// 如果有引數校驗失敗,會將錯誤資訊封裝成物件組裝在BindingResult裡
if (result.hasErrors()) {
List res = new ArrayList<>();
result.getFieldErrors().forEach(error -> {
String field = error.getField();
Object value = error.getRejectedValue();
String msg = error.getDefaultMessage();
res.add(String.format("錯誤欄位 -> %s 錯誤值 -> %s 原因 -> %s", field, value, msg));
});
map.put("msg", res);
return map;
}
map.put("msg", "success");
System.out.println(person);
return map;
}
}
```
### 傳送Post請求,偽造不合法資料
這裡使用IDEA提供的HTTP Client工具傳送請求。
```json
POST http://localhost:8081/person
Content-Type: application/json
{
"name": "hyh",
"age": 10
}
```
響應資訊如下:
```json
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 14 Nov 2020 15:58:17 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"msg": [
"錯誤欄位 -> name 錯誤值 -> hyh 原因 -> 個數必須在6和12之間",
"錯誤欄位 -> age 錯誤值 -> 10 原因 -> 最小不能小於20"
]
}
Response code: 200; Time: 393ms; Content length: 92 bytes
```
## Validator + 全域性異常處理
在介面方法中利用BindingResult處理校驗資料過程中的資訊是一個可行方案,但在介面眾多的情況下,就顯得有些冗餘,我們可以利用全域性異常處理,捕捉丟擲的`MethodArgumentNotValidException`異常,並進行相應的處理。
### 定義全域性異常處理
```java
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* If the bean validation is failed, it will trigger a MethodArgumentNotValidException.
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity