@validate或@valid注解進行數據校驗的解決方案
我們在對外提供接口的時候,為了提高安全性,我們需要在后端做數據的校驗。實際上,Java 早在 2009 年就提出了 Bean Validation 規范,該規范定義的是一個運行時的數據驗證框架,在驗證之后驗證的錯誤信息會被馬上返回。並且已經歷經 JSR303、JSR349、JSR380 三次標准的置頂,發展到了 2.0 。下面即將要介紹的是該數據驗證的規范,以及相應的技術框架日常使用。
JSR規范提案
JSR:Java Specification Requests的縮寫,意思是Java 規范提案。是指向JCP(Java Community Process)提出新增一個標准化技術規范的正式請求。任何人都可以提交JSR,以向Java平台增添新的API和服務,JSR已成為Java界的一個重要標准。
本文介紹的Bean Validation 就是出自JSR303,JSR349,以及JSR380 規范提案。該規范從JSR 303 發展到 JSR 380,目前最新規范是Bean Validation 2.0。
相信有小伙伴想去看下到底是個啥。規范提案地址:https://jcp.org/en/jsr/summary?id=bean+validation
需要注意的是,規范提案只是提供了規范,並沒有提供具體的實現。具體實現框架有默認的javax.validation.api,以及hibernate-validator。目前絕大多使用hibernate-validator。
依賴引入
要使用注解進行校驗,需要引入如下兩個依賴:
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.20.Final</version>
</dependency>
據我發現: 2.2.9.RELEASE 版的 spring-boot-starter-web 中引入了 spring-boot-starter-validation ,而 spring-boot-starter-validation 中又引入了 hibernate-validator ,所以如果引入了 spring-boot-starter-web 或者 spring-boot-starter-validation 都已經默認引入了 hibernate-validator 可以不用再引。
其它版本還未研究過,在使用的時候可以自己研究一下。
JSR303定義的校驗類型
空檢查
@Null 驗證對象是否為null
@NotNull 驗證對象是否不為null, 無法查檢長度為0的字符串
@NotBlank 檢查約束字符串是不是Null還有被Trim的長度是否大於0,只對字符串,且會去掉前后空格.
@NotEmpty 檢查約束元素是否為NULL或者是EMPTY.
Booelan檢查
@AssertTrue 驗證 Boolean 對象是否為 true
@AssertFalse 驗證 Boolean 對象是否為 false
長度檢查
@Size(min=, max=) 驗證對象(Array,Collection,Map,String)長度是否在給定的范圍之內
@Length(min=, max=) 驗證注解的元素值長度在min和max區間內
日期檢查
@Past 驗證 Date 和 Calendar 對象是否在當前時間之前
@Future 驗證 Date 和 Calendar 對象是否在當前時間之后
@Pattern 驗證 String 對象是否符合正則表達式的規則
數值檢查,建議使用在Stirng,Integer類型,不建議使用在int類型上,因為表單值為“”時無法轉換為int,但可以轉換為Stirng為"",Integer為null
@Min 驗證 Number 和 String 對象是否大等於指定的值
@Max 驗證 Number 和 String 對象是否小等於指定的值
@DecimalMax 被標注的值必須不大於約束中指定的最大值. 這個約束的參數是一個通過BigDecimal定義的最大值的字符串表示.小數存在精度
@DecimalMin 被標注的值必須不小於約束中指定的最小值. 這個約束的參數是一個通過BigDecimal定義的最小值的字符串表示.小數存在精度
@Digits 驗證 Number 和 String 的構成是否合法
@Digits(integer=,fraction=) 驗證字符串是否是符合指定格式的數字,interger指定整數精度,fraction指定小數精度。
@Range(min=, max=) 驗證注解的元素值在最小值和最大值之間
@Range(min=10000,max=50000,message="range.bean.wage")
private BigDecimal wage;
@Valid 遞歸的對關聯對象進行校驗, 如果關聯對象是個集合或者數組,那么對其中的元素進行遞歸校驗,如果是一個map,則對其中的值部分進行校驗.(是否進行遞歸驗證)
@CreditCardNumber信用卡驗證
@Email 驗證是否是郵件地址,如果為null,不進行驗證,算通過驗證。
@ScriptAssert(lang= ,script=, alias=)
@URL(protocol=,host=, port=,regexp=, flags=)
@Valid和@Validated的區別
@Valid注解是javax提供的,遵循標准 JSR-303 規范,所屬包為: javax.validation.Valid
配合BindingResult可以直接提供參數驗證結果。
@Validated是@Valid的一次封裝,是Spring提供的校驗機制使用,遵循 Spring’s JSR-303 規范(是標准 JSR-303 的一個變種),所屬包為: org.springframework.validation.annotation.Validated
@Validation對@Valid進行了二次封裝,在基本使用上並沒有區別,但在分組、注解位置、嵌套驗證等功能上有所不同,這里主要就這幾種情況進行說明。
注解位置
@Validated:可以用在類型、方法和方法參數上。但是不能用在成員屬性(字段)上
@Valid:可以用在方法、構造函數、方法參數和成員屬性(字段)上
兩者是否能用於成員屬性(字段)上直接影響能否提供嵌套驗證的功能。
分組
先定義分組接口(接口什么都不需要,空的就可以):
public interface Insert {
}
public interface Update {
}
在需要校驗的bean上加上分組注解:
@NotBlank(groups = {Update.class}, message = "ID不能為空")
private String id;
@NotBlank(groups = {Insert.class, Update.class}, message = "名稱不能為空")
@Size(groups = {Insert.class, Update.class}, max = 32, message = "名稱最大長度為32")
private String name;
根據需要,在Controller處理請求中加入 @Validated 並引入需要校驗的分組(未引入分組則都校驗)
@PostMapping("/insert")
public int insert(@RequestBody @Validated({Insert.class}) HospitalRequest request) {
return hospitalService.insert(request);
}
@PostMapping("/update")
public int update(@RequestBody @Validated({Update.class}) HospitalRequest request) {
return hospitalService.update(request);
}
在進行insert的時候不會對id進行校驗
嵌套驗證
嵌套驗證就是類嵌套類的驗證,比如我要在集合上加一個@NotNull的注解,要求該集合中的每一個對象都被驗證,如果只用@Validated與@Valid是不會驗證的。我們要用@Validated配合@Valid來進行驗證。
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class HospitalRequest {
/**
* ID
*/
@NotBlank(groups = {Update.class}, message = "ID不能為空")
private String id;
/**
* 名稱
*/
@NotBlank(groups = {Insert.class, Update.class}, message = "名稱不能為空")
@Size(groups = {Insert.class, Update.class}, max = 32, message = "名稱最大長度為32")
private String name;
/**
* 科室
*/
@NotBlank(message = "departmentList不能為空")
@Size(min = 1, message = "至少要有一個屬性")
private List<Department> departmentList;
}
例如我想讓departmentList中的每一個元素都按照我規定的JSR-303校驗進行驗證。
那么我在controller中不管用@Validated還是@Valid都是不能驗證的。
只需要在前面加上@Validated注解
@PostMapping("/add")
public void add(@RequestBody @Validated HospitalRequest request) {
add();
}
然后把@Valid放到需要驗證的集合上就可以了:
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class HospitalRequest {
/**
* ID
*/
@NotBlank(groups = {Update.class}, message = "ID不能為空")
private String id;
/**
* 名稱
*/
@NotBlank(groups = {Insert.class, Update.class}, message = "名稱不能為空")
@Size(groups = {Insert.class, Update.class}, max = 32, message = "名稱最大長度為32")
private String name;
/**
* 科室
*/
@Valid //嵌套驗證必須用@Valid
@NotBlank(groups = {Insert.class, Update.class}, message = "departmentList不能為空")
@Size(groups = {Insert.class, Update.class}, min = 1, message = "至少要有一個屬性")
private List<Department> departmentList;
}
使用BindingResult接收校驗結果信息
使用注解進行校驗的時候,我們可以通過BindingResult來收集校驗結果信息,具體操作如下:
Controller中,在@Valid或@Validated修飾的參數后跟上BindingResult參數(@Valid或@Validated 和 BindingResult 是一 一對應的,如果有多個@Valid或@Validated,那么每個@Valid或@Validated后面都需要添加BindingResult用於接收bean中的校驗信息)
@PostMapping("/insert")
public int insert(@RequestBody @Validated HospitalRequest request, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
List<String> collect = bindingResult.getFieldErrors().stream().map(
DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.toList());
StringBuilder errorMsg = new StringBuilder();
for (String s : collect) {
errorMsg.append(s);
errorMsg.append(",");
}
errorMsg = new StringBuilder(errorMsg.substring(0, errorMsg.length() - 1));
log.error("校驗未通過:{}", errorMsg.toString());
Assert.state(Boolean.FALSE, errorMsg.toString());
}
return hospitalService.insert(request);
}
這樣就可以接收到校驗的結果信息,可以根據校驗的結果信息進行一系列操作,如打印錯誤信息、拋出指定異常等。
統一異常處理
在日常開發中,我們可能需要讓校驗返回指定的信息或對象,這時我們就可以進行統一異常處理:
package com.app.config;
import com.framework.common.domain.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
/**
* 參數校驗異常處理
*/
@Slf4j
@RestControllerAdvice
public class BadRequestExceptionHandler {
/**
* 校驗錯誤攔截處理
*
* @param exception 錯誤信息集合
* @return ErrorResponse 錯誤響應,當HTTP響應狀態碼不為200時,使用該響應返回
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
private ErrorResponse validateRequestException(MethodArgumentNotValidException exception) {
BindingResult bindingResult = exception.getBindingResult();
StringBuilder errorMsg = new StringBuilder();
if (bindingResult.hasErrors()) {
List<ObjectError> errors = bindingResult.getAllErrors();
for (ObjectError objectError : errors) {
FieldError fieldError = (FieldError) objectError;
if (log.isDebugEnabled()) {
log.error("Data check failure : object: {},field: {},errorMessage: {}",
fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage());
}
errorMsg.append(objectError.getDefaultMessage());
errorMsg.append(",");
}
errorMsg = new StringBuilder(errorMsg.substring(0, errorMsg.length() - 1));
}
return new ErrorResponse("ILLEGAL_ARGUMENT_ERROR", errorMsg.toString());
}
}
返回的自定義響應體如下:
package com.framework.common.domain;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 錯誤響應,當HTTP響應狀態碼不為200時,使用該響應返回
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@AllArgsConstructor
@NoArgsConstructor
@Data
public class ErrorResponse {
/**
* 錯誤碼
*/
private String code;
/**
* 錯誤信息
*/
private String message;
}