SpringBoot中處理校驗邏輯的兩種方式

轉自:SpringBoot中處理校驗邏輯的兩種方式,真的很機智! 侵刪

平時在開發接口的時候,常常會需要對參數進行校驗,這裏提供兩種處理校驗邏輯的方式。一種是使用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應用中使用AOP記錄接口訪問日誌》。

  • 首先我們需要在添加品牌接口的參數PmsBrandParam中添加校驗註解,用於確定屬性的校驗規則及校驗失敗後需要返回的信息;
/**
 * 品牌傳遞參數
 * Created by macro on 2018/4/26.
 */
publicclass PmsBrandParam {
    @ApiModelProperty(value = "品牌名稱",required = true)
    @NotEmpty(message = "名稱不能爲空")
    private String name;
    @ApiModelProperty(value = "品牌首字母")
    private String firstLetter;
    @ApiModelProperty(value = "排序字段")
    @Min(value = 0, message = "排序最小爲0")
    private Integer sort;
    @ApiModelProperty(value = "是否爲廠家製造商")
    @FlagValidator(value = {"0","1"}, message = "廠家狀態不正確")
    private Integer factoryStatus;
    @ApiModelProperty(value = "是否進行顯示")
    @FlagValidator(value = {"0","1"}, message = "顯示狀態不正確")
    private Integer showStatus;
    @ApiModelProperty(value = "品牌logo",required = true)
    @NotEmpty(message = "品牌logo不能爲空")
    private String logo;
    @ApiModelProperty(value = "品牌大圖")
    private String bigPic;
    @ApiModelProperty(value = "品牌故事")
    private String brandStory;

   //省略若干Getter和Setter方法...
}
  • 然後在添加品牌的接口中添加@Validated註解,並注入一個BindingResult參數;
/**
 * 品牌功能Controller
 * Created by macro on 2018/4/26.
 */
@Controller
@Api(tags = "PmsBrandController", description = "商品品牌管理")
@RequestMapping("/brand")
publicclass PmsBrandController {
    @Autowired
    private PmsBrandService brandService;

    @ApiOperation(value = "添加品牌")
    @RequestMapping(value = "/create", method = RequestMethod.POST)
    @ResponseBody
    public CommonResult create(@Validated @RequestBody PmsBrandParam pmsBrand, BindingResult result) {
        CommonResult commonResult;
        int count = brandService.createBrand(pmsBrand);
        if (count == 1) {
            commonResult = CommonResult.success(count);
        } else {
            commonResult = CommonResult.failed();
        }
        return commonResult;
    }
}
  • 然後在整個Controller層創建一個切面,在其環繞通知中獲取到注入的BindingResult對象,通過hasErrors方法判斷校驗是否通過,如果有錯誤信息直接返回錯誤信息,驗證通過則放行;
/**
 * HibernateValidator錯誤結果處理切面
 * Created by macro on 2018/4/26.
 */
@Aspect
@Component
@Order(2)
publicclass BindingResultAspect {
    @Pointcut("execution(public * com.macro.mall.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();
    }
}
  • 此時我們訪問添加品牌的接口,不傳入name字段,就會返回名稱不能爲空的錯誤信息;
    在這裏插入圖片描述

自定義註解

有時候框架提供的校驗註解並不能滿足我們的需要,此時我們就需要自定義校驗註解。比如還是上面的添加品牌,此時有個參數showStatus,我們希望它只能是0或者1,不能是其他數字,此時可以使用自定義註解來實現該功能。

  • 首先自定義一個校驗註解類FlagValidator,然後添加@Constraint註解,使用它的validatedBy屬性指定校驗邏輯的具體實現類;
/**
 * 用戶驗證狀態是否在指定範圍內的註解
 * Created by macro on 2018/4/26.
 */
@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方法中就是具體的校驗邏輯。
/**
 * 狀態標記校驗器
 * Created by macro on 2018/4/26.
 */
publicclass 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){
            //當狀態爲空時使用默認值
            returntrue;
        }
        for(int i=0;i<values.length;i++){
            if(values[i].equals(String.valueOf(value))){
                isValid = true;
                break;
            }
        }
        return isValid;
    }
}
  • 接下來我們就可以在傳參對象中使用該註解了;
/**
 * 品牌傳遞參數
 * Created by macro on 2018/4/26.
 */
publicclass PmsBrandParam {

    @ApiModelProperty(value = "是否進行顯示")
    @FlagValidator(value = {"0","1"}, message = "顯示狀態不正確")
    private Integer showStatus;

   //省略若干Getter和Setter方法...
}
  • 最後我們測試下該註解,調用接口是傳入showStatus=3,會返回顯示狀態不正確的錯誤信息。
    在這裏插入圖片描述

優缺點

這種方式的優點是可以使用註解來實現參數校驗,不需要一些重複的校驗邏輯,但是也有一些缺點,比如需要在Controller的方法中額外注入一個BindingResult對象,只支持一些簡單的校驗,涉及到要查詢數據庫的校驗就無法滿足了。

二、全局異常處理

使用全局異常處理來處理校驗邏輯的思路很簡單,首先我們需要通過@ControllerAdvice註解定義一個全局異常的處理類,然後自定義一個校驗異常,當我們在Controller中校驗失敗時,直接拋出該異常,這樣就可以達到校驗失敗返回錯誤信息的目的了。

使用到的註解

  • @ControllerAdvice:類似於@Component註解,可以指定一個組件,這個組件主要用於增強
  • @Controller註解修飾的類的功能,比如說進行全局異常處理。
  • @ExceptionHandler:用來修飾全局異常處理的方法,可以指定異常的類型。

使用方式

  • 首先我們需要自定義一個異常類ApiException,當我們校驗失敗時拋出該異常:
/**
 * 自定義API異常
 * Created by macro on 2020/2/27.
 */
publicclass 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異常
 * Created by macro on 2020/2/27.
 */
publicclass Asserts {
    public static void fail(String message) {
        thrownew ApiException(message);
    }

    public static void fail(IErrorCode errorCode) {
        thrownew ApiException(errorCode);
    }
}
  • 然後再創建我們的全局異常處理類GlobalExceptionHandler,用於處理全局異常,並返回封裝好的CommonResult對象;
/**
 * 全局異常處理
 * Created by macro on 2020/2/27.
 */
@ControllerAdvice
publicclass 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
 * Created by macro on 2018/8/29.
 */
@Controller
@Api(tags = "UmsMemberCouponController", description = "用戶優惠券管理")
@RequestMapping("/member/coupon")
publicclass UmsMemberCouponController {
    @Autowired
    private UmsMemberCouponService memberCouponService;
    
    //改進前
    @ApiOperation("領取指定優惠券")
    @RequestMapping(value = "/add/{couponId}", method = RequestMethod.POST)
    @ResponseBody
    public CommonResult add(@PathVariable Long couponId) {
        return memberCouponService.add(couponId);
    }
    
    //改進後
    @ApiOperation("領取指定優惠券")
    @RequestMapping(value = "/add/{couponId}", method = RequestMethod.POST)
    @ResponseBody
    public CommonResult add(@PathVariable Long couponId) {
        memberCouponService.add(couponId);
        return CommonResult.success(null,"領取成功");
    }
}
  • 再看下Service接口中的代碼,區別在於返回結果,改進後返回的是void。其實CommonResult的作用本來就是爲了把Service中獲取到的數據封裝成統一返回結果,改進前的做法違背了這個原則,改進後的做法解決了這個問題;
/**
 * 用戶優惠券管理Service
 * Created by macro on 2018/8/29.
 */
publicinterface UmsMemberCouponService {
    /**
     * 會員添加優惠券(改進前)
     */
    @Transactional
    CommonResult add(Long couponId);

    /**
     * 會員添加優惠券(改進後)
     */
    @Transactional
    void add(Long couponId);
}
  • 再看下Service實現類中的代碼,可以看到原先校驗邏輯中返回CommonResult的邏輯都改成了調用Asserts的fail方法來實現;
/**
 * 會員優惠券管理Service實現類
 * Created by macro on 2018/8/29.
 */
@Service
publicclass UmsMemberCouponServiceImpl implements UmsMemberCouponService {
    @Autowired
    private UmsMemberService memberService;
    @Autowired
    private SmsCouponMapper couponMapper;
    @Autowired
    private SmsCouponHistoryMapper couponHistoryMapper;
    @Autowired
    private SmsCouponHistoryDao couponHistoryDao;
    
    //改進前
    @Override
    public CommonResult add(Long couponId) {
        UmsMember currentMember = memberService.getCurrentMember();
        //獲取優惠券信息,判斷數量
        SmsCoupon coupon = couponMapper.selectByPrimaryKey(couponId);
        if(coupon==null){
            return CommonResult.failed("優惠券不存在");
        }
        if(coupon.getCount()<=0){
            return CommonResult.failed("優惠券已經領完了");
        }
        Date now = new Date();
        if(now.before(coupon.getEnableTime())){
            return CommonResult.failed("優惠券還沒到領取時間");
        }
        //判斷用戶領取的優惠券數量是否超過限制
        SmsCouponHistoryExample couponHistoryExample = new SmsCouponHistoryExample();
        couponHistoryExample.createCriteria().andCouponIdEqualTo(couponId).andMemberIdEqualTo(currentMember.getId());
        long count = couponHistoryMapper.countByExample(couponHistoryExample);
        if(count>=coupon.getPerLimit()){
            return CommonResult.failed("您已經領取過該優惠券");
        }
        //省略領取優惠券邏輯...
        return CommonResult.success(null,"領取成功");
    }
    
    //改進後
     @Override
     public void add(Long couponId) {
         UmsMember currentMember = memberService.getCurrentMember();
         //獲取優惠券信息,判斷數量
         SmsCoupon coupon = couponMapper.selectByPrimaryKey(couponId);
         if(coupon==null){
             Asserts.fail("優惠券不存在");
         }
         if(coupon.getCount()<=0){
             Asserts.fail("優惠券已經領完了");
         }
         Date now = new Date();
         if(now.before(coupon.getEnableTime())){
             Asserts.fail("優惠券還沒到領取時間");
         }
         //判斷用戶領取的優惠券數量是否超過限制
         SmsCouponHistoryExample couponHistoryExample = new SmsCouponHistoryExample();
         couponHistoryExample.createCriteria().andCouponIdEqualTo(couponId).andMemberIdEqualTo(currentMember.getId());
         long count = couponHistoryMapper.countByExample(couponHistoryExample);
         if(count>=coupon.getPerLimit()){
             Asserts.fail("您已經領取過該優惠券");
         }
         //省略領取優惠券邏輯...
     }
}
  • 這裏我們輸入一個沒有的優惠券ID來測試下該功能,會返回優惠券不存在的錯誤信息。
    在這裏插入圖片描述

優缺點

使用全局異常來處理校驗邏輯的優點是比較靈活,可以處理複雜的校驗邏輯。缺點是我們需要重複編寫校驗代碼,不像使用Hibernate Validator那樣只要使用註解就可以了。不過我們可以在上面的Asserts類中添加一些工具方法來增強它的功能,比如判斷是否爲空和判斷長度等都可以自己實現。

三、總結

我們可以兩種方法一起結合使用,比如簡單的參數校驗使用Hibernate Validator來實現,而一些涉及到數據庫操作的複雜校驗使用全局異常處理的方式來實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章