SpringBoot中處理校驗邏輯的兩種方式,真的很機智!
大家好,我是二哥呀。最近正在開發(fā)一個知識庫學(xué)習(xí)網(wǎng)站編程貓,需要對請求參數(shù)進行校驗,比如說非空啊、長度限制啊等等,可選的解決方案有兩種:
一種是用 Hibernate Validator 來處理 一種是用全局異常來處理
兩種方式,我們一一來實踐體驗一下。
一、Hibernate Validator
Spring Boot 已經(jīng)內(nèi)置了 Hibernate Validator 校驗框架,這個可以通過 Spring Boot 官網(wǎng)查看和確認。
第一步,進入 Spring Boot 官網(wǎng),點擊 learn 這個面板,點擊參考文檔。

第二步,在參考文檔頁點擊「依賴的版本」。

第三步,在依賴版本頁就可以查看到所有的依賴了,包括版本號。

PS:如果發(fā)現(xiàn)沒有起效,可能是依賴版本沖突了,手動把 Hibernate Validator 依賴添加到 pom.xml 文件就可以了。
????org.hibernate.validator
????hibernate-validator
????6.0.17.Final
????javax.validation
????validation-api
????2.0.1.Final
通過 Hibernate Validator 校驗框架,我們可以直接在請求參數(shù)的字段上加入注解來完成校驗。
具體該怎么做呢?
第一步,在需要驗證的字段上加上 Hibernate Validator 提供的校驗注解。
比如說我現(xiàn)在有一個用戶名和密碼登錄的請求參數(shù) UsersLoginParam 類:
@Data
@ApiModel(value="用戶登錄",?description="用戶表")
public?class?UsersLoginParam?implements?Serializable?{
????private?static?final?long?serialVersionUID?=?1L;
????@ApiModelProperty(value?=?"登錄名")
????@NotBlank(message="登錄名不能為空")
????private?String?userLogin;
????@ApiModelProperty(value?=?"密碼")
????@NotBlank(message="密碼不能為空")
????private?String?userPass;
}
就可以通過 @NotBlank 注解來對用戶名和密碼進行判空校驗。除了 @NotBlank 注解,Hibernate Validator 還提供了以下常用注解:
@NotNull:被注解的字段不能為 null;@NotEmpty:被注解的字段不能為空;@Min:被注解的字段必須大于等于其value值;@Max:被注解的字段必須小于等于其value值;@Size:被注解的字段必須在其min和max值之間;@Pattern:被注解的字段必須符合所定義的正則表達式;@Email:被注解的字段必須符合郵箱格式。
第二步,在對應(yīng)的請求接口(UsersController.login())中添加 @Validated 注解,并注入一個 BindingResult 參數(shù)。
@Controller
@Api(tags="用戶")
@RequestMapping("/users")
public?class?UsersController?{
????@Autowired
????private?IUsersService?usersService;
????@ApiOperation(value?=?"登錄以后返回token")
????@RequestMapping(value?=?"/login",?method?=?RequestMethod.POST)
????@ResponseBody
????public?ResultObject?login(@Validated?UsersLoginParam?users,?BindingResult?result)?{
????????String?token?=?usersService.login(users.getUserLogin(),?users.getUserPass());
????????if?(token?==?null)?{
????????????return?ResultObject.validateFailed("用戶名或密碼錯誤");
????????}
????????Map?tokenMap?=?new?HashMap<>();
????????tokenMap.put("token",?token);
????????tokenMap.put("tokenHead",?tokenHead);
????????return?ResultObject.success(tokenMap);
????}
}
第三步,為控制層(UsersController)創(chuàng)建一個切面,將通知注入到 BindingResult 對象中,然后再判斷是否有校驗錯誤,有錯誤的話返回校驗提示信息,否則放行。
@Aspect
@Component
@Order(2)
public?class?BindingResultAspect?{
????@Pointcut("execution(public?*?com.codingmore.controller.*.*(..))")
????public?void?BindingResult()?{
????}
????@Around("BindingResult()")
????public?Object?doAround(ProceedingJoinPoint?joinPoint)?throws?Throwable?{
????????Object[]?args?=?joinPoint.getArgs();
????????for?(Object?arg?:?args)?{
????????????if?(arg?instanceof?BindingResult)?{
????????????????BindingResult?result?=?(BindingResult)?arg;
????????????????if?(result.hasErrors())?{
????????????????????FieldError?fieldError?=?result.getFieldError();
????????????????????if(fieldError!=null){
????????????????????????return?ResultObject.validateFailed(fieldError.getDefaultMessage());
????????????????????}else{
????????????????????????return?ResultObject.validateFailed();
????????????????????}
????????????????}
????????????}
????????}
????????return?joinPoint.proceed();
????}
}
這里涉及到了 SpringBoot AOP 的知識,我在前面的文章里講解過了,戳這個鏈接可以直達:SpringBoot AOP 掃盲
第四步,訪問登錄接口,用戶名和密碼都不傳入的情況下,就會返回“用戶名不能為空”的提示信息。

通過 debug 的形式,體驗一下整個工作流程。

可以看得出,Hibernate Validator 帶來的優(yōu)勢有這些:
驗證邏輯與業(yè)務(wù)邏輯進行了分離,降低了程序耦合度; 統(tǒng)一且規(guī)范的驗證方式,無需再次編寫重復(fù)的驗證代碼。
不過,也帶來一些弊端,比如說:
需要在請求接口的方法中注入 BindingResult 對象 只能校驗一些非常簡單的邏輯,涉及到數(shù)據(jù)查詢就無能為力了。
二、全局異常處理
使用全局異常處理的優(yōu)點就是比較靈活,可以處理比較復(fù)雜的邏輯校驗,在校驗失敗的時候直接拋出異常,然后進行捕獲處理就可以了。
第一步,新建一個自定義異常類 ApiException。
public?class?ApiException?extends?RuntimeException?{
????private?IErrorCode?errorCode;
????public?ApiException(IErrorCode?errorCode)?{
????????super(errorCode.getMessage());
????????this.errorCode?=?errorCode;
????}
????public?ApiException(String?message)?{
????????super(message);
????}
????public?ApiException(Throwable?cause)?{
????????super(cause);
????}
????public?ApiException(String?message,?Throwable?cause)?{
????????super(message,?cause);
????}
????public?IErrorCode?getErrorCode()?{
????????return?errorCode;
????}
}
第二步,新建一個斷言處理類 Asserts,簡化拋出 ApiException 的步驟。
public?class?Asserts?{
????public?static?void?fail(String?message)?{
????????throw?new?ApiException(message);
????}
????public?static?void?fail(IErrorCode?errorCode)?{
????????throw?new?ApiException(errorCode);
????}
}
第三步,新建一全局異常處理類 GlobalExceptionHandler,對異常信息進行解析,并封裝到統(tǒng)一的返回對象 ResultObject 中。
@ControllerAdvice
public?class?GlobalExceptionHandler?{
????@ResponseBody
????@ExceptionHandler(value?=?ApiException.class)
????public?ResultObject?handle(ApiException?e)?{
????????if?(e.getErrorCode()?!=?null)?{
????????????return?ResultObject.failed(e.getErrorCode());
????????}
????????return?ResultObject.failed(e.getMessage());
????}
}
全局異常處理類用到了兩個注解,@ControllerAdvice 和 @ExceptionHandler。
@ControllerAdvice 是一個特殊的 @Component(可以通過源碼看得到),用于標(biāo)識一個類,這個類中被以下三種注解標(biāo)識的方法:@ExceptionHandler,@InitBinder,@ModelAttribute,將作用于所有@Controller 類的接口上。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public?@interface?ControllerAdvice?{
}
@ExceptionHandler 注解的作用就是標(biāo)識統(tǒng)一異常處理,它可以指定要統(tǒng)一處理的異常類型,比如說我們自定義的 ApiException。
第四步,在需要校驗的地方通過 Asserts 類拋出異常 ApiException。還拿用戶登錄這個接口來說明吧。
@Controller
@Api(tags="用戶")
@RequestMapping("/users")
public?class?UsersController?{
????@ApiOperation(value?=?"登錄以后返回token")
????@RequestMapping(value?=?"/login",?method?=?RequestMethod.POST)
????@ResponseBody
????public?ResultObject?login(@Validated?UsersLoginParam?users,?BindingResult?result)?{
????????String?token?=?usersService.login(users.getUserLogin(),?users.getUserPass());
?????
????????Map?tokenMap?=?new?HashMap<>();
????????tokenMap.put("token",?token);
????????tokenMap.put("tokenHead",?tokenHead);
????????return?ResultObject.success(tokenMap);
????}
}
該接口需要查詢數(shù)據(jù)庫驗證密碼是否正確,如果密碼不正確就拋出校驗信息“密碼不正確”。
@Service
public?class?UsersServiceImpl?extends?ServiceImpl<UsersMapper,?Users>?implements?IUsersService?{
????public?String?login(String?username,?String?password)?{
????????String?token?=?null;
????????//密碼需要客戶端加密后傳遞
????????UserDetails?userDetails?=?loadUserByUsername(username);
????????if?(!passwordEncoder.matches(password,?userDetails.getPassword()))?{
????????????Asserts.fail("密碼不正確");
?????????}
????????//?其他代碼省略
????????return?token;
????}
}
第五步,通過 ApiPost 來測試一下接口,故意把密碼輸錯。

也可以通過 debug 的形式,體驗一下整個工作流程。

三、總結(jié)
實際開發(fā)中把兩者結(jié)合在一起用,就可以彌補彼此的短板了,簡單校驗用 Hibernate Validator,復(fù)雜一點的邏輯校驗,比如說需要數(shù)據(jù)庫查詢用全局異常處理來實現(xiàn)。
源碼地址:https://github.com/itwanger/coding-more
參考鏈接:http://www.macrozheng.com

沒有什么使我停留——除了目的,縱然岸旁有玫瑰、有綠蔭、有寧靜的港灣,我是不系之舟。
推薦閱讀:
