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

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

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

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