SpringBoot 使用 Validation API 和 全局異常 優(yōu)雅的校驗方法參數(shù)

一、為什么使用 Validation 來驗證參數(shù)
通常我們在使用spring框架編寫接口時,對于部分接口的參數(shù)我們要進行判空或者格式校驗來避免程序出現(xiàn)異常。那是我們一般都是使用if-else逐個對參數(shù)進行校驗。這種方法按邏輯來說也是沒有問題的,同樣也能實現(xiàn)預(yù)期效果。但是,這樣的代碼從可讀性以及美觀程序來看,是非常糟糕的。那么,我們就可以使用@valid注解來幫助我們優(yōu)雅的校驗參數(shù)。
二、如何使用Validation相關(guān)注解進行參數(shù)校驗
①為實體類中的參數(shù)或者對象添加相應(yīng)的注解;②在控制器層進行注解聲明,或者手動調(diào)用校驗方法進行校驗;③對異常進行處理;
三、Validation類的相關(guān)注解及描述
| 驗證注解 | 驗證的數(shù)據(jù)類型 | 說明 |
|---|---|---|
| @AssertFalse | Boolean,boolean | 驗證注解的元素值是false |
| @AssertTrue | Boolean,boolean | 驗證注解的元素值是true |
| @NotNull | 任意類型 | 驗證注解的元素值不是null |
| @Null | 任意類型 | 驗證注解的元素值是null |
| @Min(value=值) | BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存儲的是數(shù)字)子類型 | 驗證注解的元素值大于等于@Min指定的value值 |
| @Max(value=值) | 和@Min要求一樣 | 驗證注解的元素值小于等于@Max指定的value值 |
| @DecimalMin(value=值) | 和@Min要求一樣 | 驗證注解的元素值大于等于@ DecimalMin指定的value值 |
| @DecimalMax(value=值) | 和@Min要求一樣 | 驗證注解的元素值小于等于@ DecimalMax指定的value值 |
| @Digits(integer=整數(shù)位數(shù), fraction=小數(shù)位數(shù)) | 和@Min要求一樣 | 驗證注解的元素值的整數(shù)位數(shù)和小數(shù)位數(shù)上限 |
| @Size(min=下限, max=上限) | 字符串、Collection、Map、數(shù)組等 | 驗證注解的元素值的在min和max(包含)指定區(qū)間之內(nèi),如字符長度、集合大小 |
| @Past | java.util.Date,java.util.Calendar;Joda Time類庫的日期類型 | 驗證注解的元素值(日期類型)比當前時間早 |
| @Future | 與@Past要求一樣 | 驗證注解的元素值(日期類型)比當前時間晚 |
| @NotBlank | CharSequence子類型 | 驗證注解的元素值不為空(不為null、去除首位空格后長度為0),不同于@NotEmpty,@NotBlank只應(yīng)用于字符串且在比較時會去除字符串的首位空格 |
| @Length(min=下限, max=上限) | CharSequence子類型 | 驗證注解的元素值長度在min和max區(qū)間內(nèi) |
| @NotEmpty | CharSequence子類型、Collection、Map、數(shù)組 | 驗證注解的元素值不為null且不為空(字符串長度不為0、集合大小不為0) |
| @Range(min=最小值, max=最大值) | BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子類型和包裝類型 | 驗證注解的元素值在最小值和最大值之間 |
| @Email(regexp=正則表達式,flag=標志的模式) | CharSequence子類型(如String) | 驗證注解的元素值是Email,也可以通過regexp和flag指定自定義的email格式 |
| @Pattern(regexp=正則表達式,flag=標志的模式) | String,任何CharSequence的子類型 | 驗證注解的元素值與指定的正則表達式匹配 |
| @Valid | 任何非原子類型 | 指定遞歸驗證關(guān)聯(lián)的對象如用戶對象中有個地址對象屬性,如果想在驗證用戶對象時一起驗證地址對象的話,在地址對象上加@Valid注解即可級聯(lián)驗證 |
此處只列出Validator提供的大部分驗證約束注解,請參考hibernate validator官方文檔了解其他驗證約束注解和進行自定義的驗證約束注解定義。
四、使用 Validation API 進行參數(shù)效驗步驟
整個過程如下圖所示,用戶訪問接口,然后進行參數(shù)效驗。對于GET請求的參數(shù)可以使用@validated注解配合上面相應(yīng)的注解進行校驗或者按照原先if-else方式進行效驗。而對于POST請求,大部分是以表單數(shù)據(jù)即以實體對象為參數(shù),可以使用@Valid注解方式進行效驗。如果效驗通過,則進入業(yè)務(wù)邏輯,否則拋出異常,交由全局異常處理器進行處理。

五、 Spring Validation的三種校驗方式
第一種:在Controller方法參數(shù)前加@Valid注解——校驗不通過時直接拋異常,get請求直接在平面參數(shù)前添加相應(yīng)的校驗規(guī)則注解,使用這種的話一般結(jié)合統(tǒng)一異常處理進行處理;
第二種:在Controller方法參數(shù)前加@Valid注解,參數(shù)后面定義一個BindingResult類型參數(shù)——執(zhí)行時會將校驗結(jié)果放進bindingResult里面,用戶自行判斷并處理。
/**
* 將校驗結(jié)果放進BindingResult里面,用戶自行判斷并處理
*
* @param userInfo
* @param bindingResult
* @return
*/
@PostMapping("/testBindingResult")
public String testBindingResult(@RequestBody @Valid UserInfo userInfo, BindingResult bindingResult) {
// 參數(shù)校驗
if (bindingResult.hasErrors()) {
String messages = bindingResult.getAllErrors()
.stream()
.map(ObjectError::getDefaultMessage)
.reduce((m1, m2) -> m1 + ";" + m2)
.orElse("參數(shù)輸入有誤!");
//這里可以拋出自定義異常,或者進行其他操作
throw new IllegalArgumentException(messages);
}
return "操作成功!";
}這里我們是直接拋出了異常,如果沒有進行全局異常處理的話,接口將會返回如下信息:

第三種:用戶手動調(diào)用對應(yīng)API執(zhí)行校驗——Validation.buildDefault ValidatorFactory().getValidator().validate(xxx)
這種方法適用于校驗任意一個有valid注解的實體類,并不僅僅是只能校驗接口中的參數(shù);
這里我提取出一個工具類,如下:
import org.springframework.util.CollectionUtils;
import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validation;
import java.util.Set;
/**
* 手動調(diào)用api方法校驗對象
*/
public class MyValidationUtils {
public static void validate(@Valid Object user) {
Set<ConstraintViolation<@Valid Object>> validateSet = Validation.buildDefaultValidatorFactory()
.getValidator()
.validate(user, new Class[0]);
if (!CollectionUtils.isEmpty(validateSet)) {
String messages = validateSet.stream()
.map(ConstraintViolation::getMessage)
.reduce((m1, m2) -> m1 + ";" + m2)
.orElse("參數(shù)輸入有誤!");
throw new IllegalArgumentException(messages);
}
}
}六、springboot項目中實戰(zhàn)演練
spring-boot-starter-web依賴已經(jīng)集成相關(guān)jar,無需額外引入。
1.對實體類的變量進行注解標注
實體類中添加 @Valid 相關(guān)驗證注解,并在注解中添加出錯時的響應(yīng)消息。
User.class
import org.hibernate.validator.constraints.Length;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
@Data
public class User {
@NotBlank(message = "姓名不能為空")
private String username;
@NotBlank(message = "密碼不能為空")
@Length(min = 6, max = 16, message = "密碼長度為6-16位")
private String password;
@Pattern(regexp = "0?(13|14|15|17|18|19)[0-9]{9}", message = "手機號格式不正確")
private String phone;
// 嵌套必須加 @Valid,否則嵌套中的驗證不生效
@Valid
@NotNull(message = "userinfo不能為空")
private UserInfo userInfo;
}如果是嵌套的實體對象,并且也要校驗該對象,則需要在最外層屬性上添加 @Valid 注解。
UserInfo.class
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
@Data
public class UserInfo {
@NotBlank(message = "年齡不為空")
@Max(value = 18, message = "不能超過18歲")
private String age;
@NotBlank(message = "性別不能為空")
private String gender;
}2.創(chuàng)建自定義異常
自定義異常類,方便我們處理手動拋出的異常。
public class ParamaErrorException extends RuntimeException {
public ParamaErrorException() {
}
public ParamaErrorException(String message) {
super(message);
}
}3.自定義響應(yīng)枚舉類
定義一個返回信息的枚舉類,方便我們快速響應(yīng)信息,不必每次都寫返回消息和響應(yīng)碼。
public enum ResultEnum {
SUCCESS(1000, "請求成功"),
PARAMETER_ERROR(1001, "請求參數(shù)有誤!"),
UNKNOWN_ERROR(9999, "未知的錯誤!");
private Integer code;
private String message;
ResultEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}4.自定義響應(yīng)對象類
創(chuàng)建用于返回調(diào)用方的響應(yīng)信息的實體類。
import com.sue.demo.enums.ResultEnum;
import lombok.Data;
@Data
public class ResponseResult {
private Integer code;
private String msg;
public ResponseResult() {
}
public ResponseResult(ResultEnum resultEnum) {
this.code = resultEnum.getCode();
this.msg = resultEnum.getMessage();
}
public ResponseResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}5.添加全局異常處理
全局異常用于處理校驗不通過時拋出的異常,并通過接口返回,同時對其他未知異常進行處理。
import com.sue.demo.controller.ResponseResult;
import com.sue.demo.enums.ResultEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.util.StringUtils;
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.MissingServletRequestParameterException;
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;
@RestControllerAdvice("com.sue.demo.controller")
public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 忽略參數(shù)異常處理器
* @param e 忽略參數(shù)異常
* @return ResponseResult
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseResult parameterMissingExceptionHandler(MissingServletRequestParameterException e) {
logger.error("", e);
return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), "請求參數(shù) " + e.getParameterName() + " 不能為空");
}
/**
* 缺少請求體異常處理器
* @param e 缺少請求體異常
* @return ResponseResult
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseResult parameterBodyMissingExceptionHandler(HttpMessageNotReadableException e) {
logger.error("", e);
return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), "參數(shù)體不能為空");
}
/**
* 參數(shù)效驗異常處理器
* @param e 參數(shù)驗證異常
* @return ResponseInfo
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseResult parameterExceptionHandler(MethodArgumentNotValidException e) {
logger.error("", e);
// 獲取異常信息
BindingResult exceptions = e.getBindingResult();
// 判斷異常中是否有錯誤信息,如果存在就使用異常中的消息,否則使用默認消息
if (exceptions.hasErrors()) {
List<ObjectError> errors = exceptions.getAllErrors();
if (!errors.isEmpty()) {
// 這里列出了全部錯誤參數(shù),按正常邏輯,只需要第一條錯誤即可
FieldError fieldError = (FieldError) errors.get(0);
return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), fieldError.getDefaultMessage());
}
}
return new ResponseResult(ResultEnum.PARAMETER_ERROR);
}
/**
* 自定義參數(shù)錯誤異常處理器
* @param e 自定義參數(shù)
* @return ResponseInfo
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({ParamaErrorException.class})
public ResponseResult paramExceptionHandler(ParamaErrorException e) {
logger.error("", e);
// 判斷異常中是否有錯誤信息,如果存在就使用異常中的消息,否則使用默認消息
if (!StringUtils.isEmpty(e.getMessage())) {
return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), e.getMessage());
}
return new ResponseResult(ResultEnum.PARAMETER_ERROR);
}
/**
* 其他異常
* @param e
* @return
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({Exception.class})
public ResponseResult otherExceptionHandler(Exception e) {
logger.error("其他異常", e);
// 判斷異常中是否有錯誤信息,如果存在就使用異常中的消息,否則使用默認消息
if (!StringUtils.isEmpty(e.getMessage())) {
return new ResponseResult(ResultEnum.UNKNOWN_ERROR.getCode(), e.getMessage());
}
return new ResponseResult(ResultEnum.UNKNOWN_ERROR);
}
}6.接口類中添加相關(guān)注解
處理get請求直接在參數(shù)前添加驗證注解,處理post請求時在對象前添加@Valid注解
TestController.class
import com.sue.demo.entity.User;
import com.sue.demo.entity.UserInfo;
import com.sue.demo.enums.ResultEnum;
import com.sue.demo.exception.ParamaErrorException;
import com.sue.demo.util.MyValidationUtils;
import com.sue.demo.util.ResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.hibernate.validator.constraints.Length;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.List;
@Validated
@RestController
@Api(value = "測試使用validation驗證參數(shù)")
public class TestController {
/**
* 測試get方法,手動if進行判空,校驗失敗時手動拋出自定義異常
* @param username 姓名
* @return ResponseResult
*/
@ApiOperation(value = "測試get方法", notes = "輸入用戶名")
@GetMapping("/testGet")
public ResponseResult testGet(String username) {
if (username == null || "".equals(username)) {
throw new ParamaErrorException("username 不能為空");
}
return new ResponseResult(ResultEnum.SUCCESS);
}
/**
* 使用注解校驗get請求平面參數(shù),需要在Controller類頭部添加@Validated注解,否則不能成功校驗,這種方法不用手動拋出異常
* @param username
* @return
*/
@ApiOperation(value = "測試get方法", notes = "輸入用戶名")
@GetMapping("/testGetByValidated")
public ResponseResult testGetByValidated(@Length(max = 4) @RequestParam("username") String username) {
return new ResponseResult(ResultEnum.SUCCESS);
}
/**
* post方法傳入單個對象進行校驗,在參數(shù)前添加@Valid注解,校驗失敗時會拋出異常并使用全局異常進行處理
* @param userInfo 用戶信息
* @return ResponseResult
*/
@ApiOperation(value = "post方法傳入單個對象", notes = "傳入json對象")
@PostMapping("/testUserInfo")
public ResponseResult testUserInfo(@Valid @RequestBody UserInfo userInfo) {
return new ResponseResult(ResultEnum.SUCCESS);
}
/**
* post方法傳入對象,手動校驗,此時參數(shù)前沒有添加@Valid注解,所以不會自動進行校驗,手動調(diào)用validate方法進行校驗,失敗時會拋出異常
* @param userInfo
* @return ResponseResult
*/
@ApiOperation(value = "post方法傳入對象,手動測試", notes = "單個對象")
@PostMapping("/checkByMethod")
public ResponseResult checkByMethod(@RequestBody UserInfo userInfo) {
//調(diào)用api校驗
MyValidationUtils.validate(userInfo);
return new ResponseResult(ResultEnum.SUCCESS);
}
/**
* post方法傳入多個對象,當使用@Valid校驗對象集合時,要在控制層添加@Validated注解,否則不會對集合中的每個對象進行校驗
* @param userInfo
* @return ResponseResult
*/
@ApiOperation(value = "post方法傳入多個對象", notes = "多個對象")
@PostMapping("/testUserList")
public ResponseResult testUserList(@Valid @RequestBody List<UserInfo> userInfo) {
return new ResponseResult(ResultEnum.SUCCESS);
}
/**
* 測試對象中嵌套對象的情況,此時也要在對象屬性上添加@Valid注解
* @param user
* @return
*/
@ApiOperation(value = "測試對象中嵌套對象的情況")
@PostMapping("/checkUser")
public ResponseResult checkUser(@Valid @RequestBody User user) {
return new ResponseResult(ResultEnum.SUCCESS);
}
/**
* 將校驗結(jié)果放進BindingResult里面,用戶自行判斷并處理
* @param userInfo
* @param bindingResult
* @return
*/
@PostMapping("/testBindingResult")
public String testBindingResult(@RequestBody @Valid UserInfo userInfo, BindingResult bindingResult) {
// 參數(shù)校驗
if (bindingResult.hasErrors()) {
String messages = bindingResult.getAllErrors()
.stream()
.map(ObjectError::getDefaultMessage)
.reduce((m1, m2) -> m1 + ";" + m2)
.orElse("參數(shù)輸入有誤!");
//這里可以拋出自定義異常,或者進行其他操作
throw new IllegalArgumentException(messages);
}
return "操作成功!";
}
}7.進行測試
補充:使用自定義參數(shù)注解
1.我們這里創(chuàng)建一個身份證校驗注解
@Documented
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdentityCardNumberValidator.class)
public @interface IdentityCardNumber {
String message() default "身份證號碼不合法";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}這個注解是作用在Field字段上,運行時生效,觸發(fā)的是IdentityCardNumber這個驗證類。
- message 定制化的提示信息,主要是從ValidationMessages.properties里提取,也可以依據(jù)實際情況進行定制
- groups 這里主要進行將validator進行分類,不同的類group中會執(zhí)行不同的validator操作
- payload 主要是針對bean的,使用不多。
2.自定義Validator
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class IdentityCardNumberValidator implements ConstraintValidator<IdentityCardNumber, Object> {
@Override
public void initialize(IdentityCardNumber identityCardNumber) {
}
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
return IdCardValidatorUtils.isValidate18Idcard(o.toString());
}
}校驗工具類IdCardValidatorUtils.class
3. 使用自定義的注解
@NotBlank(message = "身份證號不能為空")
@IdentityCardNumber(message = "身份證信息有誤,請核對后提交")
private String clientCardNo;出處:csdn.net/chenyao1994/article/details/107858409
關(guān)注GitHub今日熱榜,專注挖掘好用的開發(fā)工具,致力于分享優(yōu)質(zhì)高效的工具、資源、插件等,助力開發(fā)者成長!
點個在看,你最好看
