SpringBoot中處理校驗邏輯的兩種方式,真的很機智!

SpringBoot實戰電商項目mall(30k+star)地址:github.com/macrozheng/…java

摘要

平時在開發接口的時候,經常會須要對參數進行校驗,這裏提供兩種處理校驗邏輯的方式。一種是使用Hibernate Validator來處理,另外一種是使用全局異常來處理,下面咱們講下這兩種方式的用法。git

Hibernate Validator

Hibernate Validator是SpringBoot內置的校驗框架,只要集成了SpringBoot就自動集成了它,咱們能夠經過在對象上面使用它提供的註解來完成參數校驗。github

經常使用註解

咱們先來了解下經常使用的註解,對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. */
public class 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")
public class 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)
public class 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,不能是其餘數字,此時可使用自定義註解來實現該功能。app

  • 首先自定義一個校驗註解類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. */
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;
    }
}
複製代碼
  • 接下來咱們就能夠在傳參對象中使用該註解了;
/** * 品牌傳遞參數 * Created by macro on 2018/4/26. */
public class PmsBrandParam {

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

   //省略若干Getter和Setter方法...
}
複製代碼
  • 最後咱們測試下該註解,調用接口是傳入showStatus=3,會返回顯示狀態不正確的錯誤信息。

優缺點

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

全局異常處理

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

使用到的註解

@ControllerAdvice:相似於@Component註解,能夠指定一個組件,這個組件主要用於加強@Controller註解修飾的類的功能,好比說進行全局異常處理。工具

@ExceptionHandler:用來修飾全局異常處理的方法,能夠指定異常的類型。學習

使用方式

  • 首先咱們須要自定義一個異常類ApiException,當咱們校驗失敗時拋出該異常:
/** * 自定義API異常 * Created by macro on 2020/2/27. */
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異常 * Created by macro on 2020/2/27. */
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對象;
/** * 全局異常處理 * Created by macro on 2020/2/27. */
@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 * Created by macro on 2018/8/29. */
@Controller
@Api(tags = "UmsMemberCouponController", description = "用戶優惠券管理")
@RequestMapping("/member/coupon")
public class 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. */
public interface UmsMemberCouponService {
    /** * 會員添加優惠券(改進前) */
    @Transactional
    CommonResult add(Long couponId);

    /** * 會員添加優惠券(改進後) */
    @Transactional
    void add(Long couponId);
}
複製代碼
  • 再看下Service實現類中的代碼,能夠看到原先校驗邏輯中返回CommonResult的邏輯都改爲了調用Asserts的fail方法來實現;
/** * 會員優惠券管理Service實現類 * Created by macro on 2018/8/29. */
@Service
public class 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來實現,而一些涉及到數據庫操做的複雜校驗使用全局異常處理的方式來實現。

項目源碼地址

github.com/macrozheng/…

公衆號

mall項目全套學習教程連載中,關注公衆號第一時間獲取。

公衆號圖片
相關文章
相關標籤/搜索