Spring Boot引數校驗以及分組校驗的使用
簡介:做web開發基本上每個介面都要對引數進行校驗,如果引數比較少,還比較容易處理,一但引數比較多了的話程式碼中就會出現大量的if-else語句。雖然這種方式簡單直接,但會大大降低開發效率和程式碼可讀性。所以我們可以使用validator元件來代替我們進行不必要的coding操作。本文將基於validator的介紹資料,同時結合作者自己在專案中的實際使用經驗進行了總結。
作者 | 江巖
來源 | 阿里技術公眾號
一 前言
做web開發有一點很煩人就是要對前端輸入引數進行校驗,基本上每個介面都要對引數進行校驗,比如一些非空校驗、格式校驗等。如果引數比較少的話還是容易處理的一但引數比較多了的話程式碼中就會出現大量的if-else語句。
使用這種方式雖然簡單直接,但是也有不好的地方,一是降低了開發效率,因為我們需要校驗的引數會存在很多地方,並且不同地方會有重複校驗,其次降低了程式碼可讀性,因為在業務程式碼中摻雜了太多額外工作的程式碼。
所以我們可以使用validator元件來代替我們進行不必要的coding操作。本文基於validator的介紹資料,也結合自己在專案中的實際使用經驗進行了總結,希望能幫到大家。
1 什麼是validator
Bean Validation是Java定義的一套基於註解的資料校驗規範,目前已經從JSR 303的1.0版本升級到JSR 349的1.1版本,再到JSR 380的2.0版本(2.0完成於2017.08),已經經歷了三個版本 。需要注意的是,JSR只是一項標準,它規定了一些校驗註解的規範,但沒有實現,比如@Null、@NotNull、@Pattern等,它們位於 javax.validation.constraints這個包下。而hibernate validator是對這個規範的實現,並增加了一些其他校驗註解,如 @NotBlank、@NotEmpty、@Length等,它們位於org.hibernate.validator.constraints這個包下。
如果我們的專案使用了Spring Boot,hibernate validator框架已經整合在 spring-boot-starter-web中,所以無需再新增其他依賴。如果不是Spring Boot專案,需要新增如下依賴。
二 註解介紹
1 validator內建註解
hibernate validator中擴充套件定義瞭如下註解:
三 使用
使用起來比較簡單,都是使用註解方式使用。具體來說分為單引數校驗、物件引數校驗,單引數校驗就是controller介面按照單引數接收前端傳值,沒有封裝物件進行接收,如果有封裝物件那就是物件引數校驗。
1 單引數校驗
單引數校驗只需要在引數前添加註解即可,如下所示:
public Result deleteUser(@NotNull(message = "id不能為空") Long id) {
// do something
}
但有一點需要注意,如果使用單引數校驗,controller類上必須新增@Validated註解,如下所示:
@RestController
@RequestMapping("/user")
@Validated // 單引數校驗需要加的註解
public class UserController {
// do something
}
2 物件引數校驗
物件引數校驗使用時,需要先在物件的校驗屬性上添加註解,然後在Controller方法的物件引數前新增@Validated 註解,如下所示:
public Result addUser(@Validated UserAO userAo) {
// do something
}
public class UserAO {
@NotBlank
private String name;
@NotNull
private Integer age;
……
}
註解分組
在物件引數校驗場景下,有一種特殊場景,同一個引數物件在不同的場景下有不同的校驗規則。比如,在建立物件時不需要傳入id欄位(id欄位是主鍵,由系統生成,不由使用者指定),但是在修改物件時就必須要傳入id欄位。在這樣的場景下就需要對註解進行分組。
1)元件有個預設分組Default.class, 所以我們可以再建立一個分組UpdateAction.class,如下所示:
public interface UpdateAction {
}
2)在引數類中需要校驗的屬性上,在註解中新增groups屬性:
public class UserAO {
@NotNull(groups = UpdateAction.class, message = "id不能為空")
private Long id;
@NotBlank
private String name;
@NotNull
private Integer age;
……
}
如上所示,就表示只在UpdateAction分組下校驗id欄位,在預設情況下就會校驗name欄位和age欄位。
然後在controller的方法中,在@Validated註解裡指定哪種場景即可,沒有指定就代表採用Default.class,採用其他分組就需要顯示指定。如下程式碼便表示在addUser()介面中按照預設情況進行引數校驗,在updateUser()介面中按照預設情況和UpdateAction分組對引數進行共同校驗。
public Result addUser(@Validated UserAO userAo) {
// do something
}
public Result updateUser(@Validated({Default.class, UpdateAction.class}) UserAO userAo) {
// do something
}
物件巢狀
如果需要校驗的引數物件中還巢狀有一個物件屬性,而該巢狀的物件屬性也需要校驗,那麼就需要在該物件屬性上增加@Valid註解。
public class UserAO {
@NotNull(groups = UpdateAction.class, message = "id不能為空")
private Long id;
@NotBlank
private String name;
@NotNull
private Integer age;
@Valid
private Phone phone;
……
}
public class Phone {
@NotBlank
private String operatorType;
@NotBlank
private String phoneNum;
}
3 錯誤訊息的捕獲
引數校驗失敗後會丟擲異常,我們只需要在全域性異常處理類中捕獲引數校驗的失敗異常,然後將錯誤訊息新增到返回值中即可。捕獲異常的方法如下所示,返回值Result是我們系統自定義的返回值類。
@RestControllerAdvice(basePackages= {"com.alibaba.dc.controller","com.alibaba.dc.service"})
public class GlobalExceptionHandler {
@ExceptionHandler(value = {Throwable.class})
Result handleException(Throwable e, HttpServletRequest request){
// 異常處理
}
}
需要注意的是,如果缺少引數丟擲的異常是MissingServletRequestParameterException,單引數校驗失敗後丟擲的異常是ConstraintViolationException,get請求的物件引數校驗失敗後丟擲的異常是BindException,post請求的物件引數校驗失敗後丟擲的異常是MethodArgumentNotValidException,不同異常物件的結構不同,對異常訊息的提取方式也就不同。如下圖所示:
1)MissingServletRequestParameterException
if(e instanceof MissingServletRequestParameterException){
Result result = Result.buildErrorResult(ErrorCodeEnum.PARAM_ILLEGAL);
String msg = MessageFormat.format("缺少引數{0}", ((MissingServletRequestParameterException) e).getParameterName());
result.setMessage(msg);
return result;
}
2)ConstraintViolationException異常
if(e instanceof ConstraintViolationException){
// 單個引數校驗異常
Result result = Result.buildErrorResult(ErrorCodeEnum.PARAM_ILLEGAL);
Set<ConstraintViolation<?>> sets = ((ConstraintViolationException) e).getConstraintViolations();
if(CollectionUtils.isNotEmpty(sets)){
StringBuilder sb = new StringBuilder();
sets.forEach(error -> {
if (error instanceof FieldError) {
sb.append(((FieldError)error).getField()).append(":");
}
sb.append(error.getMessage()).append(";");
});
String msg = sb.toString();
msg = StringUtils.substring(msg, 0, msg.length() -1);
result.setMessage(msg);
}
return result;
}
3)BindException異常
if (e instanceof BindException){
// get請求的物件引數校驗異常
Result result = Result.buildErrorResult(ErrorCodeEnum.PARAM_ILLEGAL);
List<ObjectError> errors = ((BindException) e).getBindingResult().getAllErrors();
String msg = getValidExceptionMsg(errors);
if (StringUtils.isNotBlank(msg)){
result.setMessage(msg);
}
return result;
}
private String getValidExceptionMsg(List<ObjectError> errors) {
if(CollectionUtils.isNotEmpty(errors)){
StringBuilder sb = new StringBuilder();
errors.forEach(error -> {
if (error instanceof FieldError) {
sb.append(((FieldError)error).getField()).append(":");
}
sb.append(error.getDefaultMessage()).append(";");
});
String msg = sb.toString();
msg = StringUtils.substring(msg, 0, msg.length() -1);
return msg;
}
return null;
}
4)MethodArgumentNotValidException異常
if (e instanceof MethodArgumentNotValidException){
// post請求的物件引數校驗異常
Result result = Result.buildErrorResult(ErrorCodeEnum.PARAM_ILLEGAL);
List<ObjectError> errors = ((MethodArgumentNotValidException) e).getBindingResult().getAllErrors();
String msg = getValidExceptionMsg(errors);
if (StringUtils.isNotBlank(msg)){
result.setMessage(msg);
}
return result;
}
原文連結
本文為阿里雲原創內容,未經允許不得轉載。