平時在開發接口的時候,經常會需要對參數進行校驗,這里提供兩種處理校驗邏輯的方式。一種是使用Hibernate Validator來處理,另一種是使用全局異常來處理,下面我們講下這兩種方式的用法。
Hibernate Validator
Hibernate Validator是SpringBoot內置的校驗框架,只要集成了SpringBoot就自動集成了它,就可以在對象上面使用它提供的注解來完成參數校驗。
常用注解
我們先來了解下常用的注解,對Hibernate Validator所提供的校驗功能有個印象。
- @Null:被注釋的屬性必須為null;
- @NotNull:被注釋的屬性不能為null;
- @AssertTrue:被注釋的屬性必須為true;
- @AssertFalse:被注釋的屬性必須為false;
- @Min:被注釋的屬性必須大於等於其value值;
- @Max:被注釋的屬性必須小於等於其value值;
- @Size:被注釋的屬性必須在其min和max值之間;
- @Pattern:被注釋的屬性必須符合其regexp所定義的正則表達式;
- @NotBlank:被注釋的字符串不能為空字符串;
- @NotEmpty:被注釋的屬性不能為空;
- @Email:被注釋的屬性必須符合郵箱格式。
使用方式
接下來以添加用戶接口的參數校驗為例來講解下Hibernate Validator的使用方法,其中涉及到一些AOP的知識,沒有接觸過的同學可以去SpringBoot官網查看文檔
- 首先我們需要在添加用戶接口的參數UserParam中添加校驗注解,用於確定屬性的校驗規則及校驗失敗后需要返回的信息;
/**
* 用戶傳遞參數
*/
public class UserParam {
@ApiModelProperty(value = "用戶名稱",required = true)
@NotEmpty(message = "用戶名稱不能為空")
private String username;
@ApiModelProperty(value = "密碼", required = true)
@NotEmpty
private String password;
@ApiModelProperty(value = "年齡")
@Min(value = 0, message = "年齡最小為0")
private Integer age;
@ApiModelProperty(value = "用戶頭像",required = true)
@NotEmpty(message = "用戶頭像不能為空")
private String icon;
//省略Getter和Setter方法...
}
- 然后在添加用戶的接口中添加@Validated注解,並注入一個BindingResult參數;
/**
* 用戶功能Controller
*/
@Controller
@Api(tags = "UserController", description = "用戶管理")
@RequestMapping("/user")
public class UserController {
@Autowired
private UserServiceuserService;
@ApiOperation(value = "添加用戶")
@RequestMapping(value = "/create", method = RequestMethod.POST)
@ResponseBody
public CommonResult create(@Validated @RequestBody UserParam user, BindingResult result) {
CommonResult commonResult;
int count = userService.createUser(user);
if (count == 1) {
commonResult = CommonResult.success(count);
} else {
commonResult = CommonResult.failed();
}
return commonResult;
}
}
- 然后在整個Controller層創建一個切面,在其環繞通知中獲取到注入的BindingResult對象,通過hasErrors方法判斷校驗是否通過,如果有錯誤信息直接返回錯誤信息,驗證通過則放行;
/**
* HibernateValidator錯誤結果處理切面
*/
@Aspect
@Component
@Order(2)
public class BindingResultAspect {
@Pointcut("execution(public * com.demo.test.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 CommonResult.validateFailed(fieldError.getDefaultMessage());
}else{
return CommonResult.validateFailed();
}
}
}
}
return joinPoint.proceed();
}
}
- 使用切面的話,由於每個校驗方法中都需要注入
BindingResult
對象,這樣會導致很多重復工作,其實當校驗失敗時,SpringBoot默認會拋出MethodArgumentNotValidException
或BindException
異常,我們只要全局處理該異常依然可以得到校驗失敗信息。
/**
* 全局異常處理
*/
@ControllerAdvice
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
String message = null;
if (bindingResult.hasErrors()) {
FieldError fieldError = bindingResult.getFieldError();
if (fieldError != null) {
message = fieldError.getField()+fieldError.getDefaultMessage();
}
}
return CommonResult.failed(message);
}
@ResponseBody
@ExceptionHandler(value = BindException.class)
public R handleValidException(BindException e) {
BindingResult bindingResult = e.getBindingResult();
String message = null;
if (bindingResult.hasErrors()) {
FieldError fieldError = bindingResult.getFieldError();
if (fieldError != null) {
message = fieldError.getField()+fieldError.getDefaultMessage();
}
}
return Response.failed(message);
}
}
- 由於SpringBoot 2.3版本默認移除了校驗功能,如果想要開啟的話需要添加如下依賴。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
自定義注解
有時候框架提供的校驗注解並不能滿足我們的需要,此時我們就需要自定義校驗注解。比如還是上面的添加品牌,此時有個參數showStatus,我們希望它只能是0或者1,不能是其他數字,此時可以使用自定義注解來實現該功能。
- 首先自定義一個校驗注解類FlagValidator,然后添加@Constraint注解,使用它的validatedBy屬性指定校驗邏輯的具體實現類;
/**
* 用戶驗證狀態是否在指定范圍內的注解
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Constraint(validatedBy = FlagValidatorClass.class)
public @interface FlagValidator {
String[] value() default {};
String message() default "flag is not found";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- 然后創建FlagValidatorClass作為校驗邏輯的具體實現類,實現ConstraintValidator接口,這里需要指定兩個泛型參數,第一個需要指定為你自定義的校驗注解類,第二個指定為你要校驗屬性的類型,isValid方法中就是具體的校驗邏輯。
/**
* 狀態標記校驗器
*/
public class FlagValidatorClass implements ConstraintValidator<FlagValidator,Integer> {
private String[] values;
@Override
public void initialize(FlagValidator flagValidator) {
this.values = flagValidator.value();
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
boolean isValid = false;
if(value==null){
//當狀態為空時使用默認值
return true;
}
for(int i=0;i<values.length;i++){
if(values[i].equals(String.valueOf(value))){
isValid = true;
break;
}
}
return isValid;
}
}
- 接下來就能在傳參對象中使用該注解了;
/**
* 用戶傳遞參數
*/
public class UserParam {
@ApiModelProperty(value = "用戶賬戶狀態是否正常")
@FlagValidator(value = {"0","1"}, message = "賬戶狀態不正常,請聯系管理員")
private Integer Status;
//省略Getter和Setter方法...
}
優缺點
這種方式的優點是可以使用注解來實現參數校驗,不需要一些重復的校驗邏輯,但也有一些缺點,比如需要在Controller的方法中額外注入一個BindingResult對象,只支持一些簡單的校驗,涉及到要查詢數據庫的校驗就無法滿足。
全局異常處理
使用全局異常處理來處理校驗邏輯的思路很簡單,首先我們需要通過@ControllerAdvice注解定義一個全局異常的處理類,然后自定義一個校驗異常,當我們在Controller中校驗失敗時,直接拋出該異常,這樣就能達到校驗失敗返回錯誤信息的目的。
使用到的注解
-
@ControllerAdvice:類似於@Component注解,可以指定一個組件,這個組件主要用於增強@Controller注解修飾的類的功能,比如說進行全局異常處理。
-
@ExceptionHandler:用來修飾全局異常處理的方法,可以指定異常的類型。
使用方式
- 首先我們需要自定義一個異常類
ApiException
,當我們校驗失敗時拋出該異常:
/**
* 自定義API異常
*/
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;
/**
* 斷言處理類,用於拋出各種API異常
*/
public class Asserts {
public static void fail(String message) {
throw new ApiException(message);
}
public static void fail(IErrorCode errorCode) {
throw new ApiException(errorCode);
}
}
- 然后再創建我們的全局異常處理類GlobalExceptionHandler,用於處理全局異常,並返回封裝好的CommonResult對象;
/**
* 全局異常處理
*/
@ControllerAdvice
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(value = ApiException.class)
public CommonResult handle(ApiException e) {
if (e.getErrorCode() != null) {
return CommonResult.failed(e.getErrorCode());
}
return CommonResult.failed(e.getMessage());
}
}
- 這里以添加用戶為例,我們先對比下改進前后的代碼,首先看Controller層代碼。改進后只要Service中的方法執行成功就表示添加用戶成功,因為添加不成功的話會直接拋出ApiException從而返回錯誤信息;
/**
* 用戶管理Controller
*/
@Controller
@Api(tags = "UserController", description = "用戶管理")
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
//改進前
@ApiOperation("添加用戶")
@RequestMapping(value = "/add/{username}", method = RequestMethod.POST)
@ResponseBody
public CommonResult add(@PathVariable String username) {
return userService.add(username);
}
//改進后
@ApiOperation("添加用戶")
@RequestMapping(value = "/add/{username}", method = RequestMethod.POST)
@ResponseBody
public CommonResult add(@PathVariable String username) {
user.add(username);
return CommonResult.success(null,"添加成功");
}
}
- 再看下Service接口中的代碼,區別在於返回結果,改進后返回的是void。其實CommonResult的作用本來就是為了把Service中獲取到的數據封裝成統一返回結果,改進前的做法違背了這個原則,改進后的做法解決了這個問題;
/**
* 用戶管理Service
*/
public interface UserService {
/**
* 添加用戶(改進前)
*/
@Transactional
CommonResult add(String username);
/**
* 添加用戶(改進后)
*/
@Transactional
void add(String username);
}
- 再改變Service實現類中的代碼,原先校驗邏輯中返回CommonResult的邏輯都改成了調用Asserts的fail方法來實現;
優缺點
使用全局異常來處理校驗邏輯的優點是比較靈活,可以處理復雜的校驗邏輯。缺點是我們需要重復編寫校驗代碼,不像使用Hibernate Validator那樣只要使用注解就可以了。不過我們可以在上面的Asserts
類中添加一些工具方法來增強它的功能,比如判斷是否為空和判斷長度等都可以自己實現。
總結
我們可以兩種方法一起結合使用,比如簡單的參數校驗使用Hibernate Validator來實現,而一些涉及到數據庫操作的復雜校驗使用全局異常處理的方式來實現。