支付系統 - 全局異常以及對外返回的統一處理

前言

在筆者從業的多年時間內,參與設計了不少系統。從知足業務須要的角度出發,能快速支撐業務發展都能稱之爲「好」的系統。畢竟,創造價值的是業務,若是沒有業務驅動,工程師掌握屠龍之技沒有龍也是至關苦悶之事。作爲一名卓越的程序員,相信你們都但願本身開發的系統易維護,更健壯。 固然這只是一種理想主義。且不說互聯網行業瞬息萬變,單是工期緊,面向deadline編程就須要使不少工程師放棄對代碼維護性的執念。就筆者我的經歷而言,早上拿到需求文檔晚上上線也是常有的事。儘管如此,我仍是但願從我的角度來談談,一些簡單易行、順手培養的習慣到底能給程序維護性帶來怎樣的便利。 本文是從異常的使用着手,聊一聊使用不當帶來的壞味道。git

受檢/非受檢異常

Java中存在RuntimeExceptionException。典型的如NullPointerException就屬於RuntimeException,這些異常不須要開發者捕獲在運行時一旦觸發自動拋出。同時,RuntimeException也被稱爲非受檢異常,見名知意,在編譯期不受檢查。除了RuntimeException之外都稱爲受檢異常,在編譯期須要強制處理,拋出或捕獲,常見的如ClassNotFoundExceptionInterruptedException程序員

感受這裏有異常就聲明

筆者見過不少這樣的代碼:github

PayOrder selectByOrderId(Long orderId) throw Exception;
複製代碼

出於對網絡鏈接/數據庫的懷疑,總感受本身寫的SQL會拋出異常,而且想讓上游去處理這個異常。美其名曰,面向防護編程,但其實給程序維護增長了不少的煩惱。 首先,由於這種異常的拋出很泛泛,調用方並不知道拋出這個異常的人當時的想法,只能在外層強制捕獲這個異常。若是上層調用者也不想處理這個異常,他又會繼續往上層拋,這樣處理幾回後,最外層的程序塊則徹底不知道這個異常是什麼狀況下拋出的了。 可能不少小夥伴會說,個人編程風格就是別人有異常本身能處理就處理,不能處理才往上層拋。那我想說,你真棒。可是,一個項目中並非只有一個開發者,每一個人都有本身的編程習慣。有的小夥伴就是喜歡把異常往外層拋,繼續爲難上層調用者。 固然,這裏並非說拋異常很差。合理的使用異常能使程序的結構更清晰,語義更明確。也方便高層調用者針對不一樣的異常進行處理,而不是隻能無奈的捕獲Exceptionweb

無論有沒有就捕獲

接着上文,若是有人從很最底層一路拋出了一個受檢異常。在這樣的系統中,想一想你們會怎麼作呢?很容易,捕獲就行。可是,若是下層拋出的是一個非受檢異常也就是RuntimeException呢?很差意思,一旦團隊中有人這麼作,在沒有全局異常處理的狀況下,有經驗的開發者會選擇在外層捕獲,而缺少一些經驗或者不熟悉系統的開發者天然不會捕獲這個異常。那這種錯誤就會拋到容器中,這又是什麼意思呢?假設你返回的是一個 JSON 格式,若是拋到容器中返回的內容就是程序的錯誤信息。其餘調用方就沒法解析這個返回值,這種狀況確定是不能出現的。數據庫

今後之後,你可能會看見全部的外層程序都有着醜陋的try catch塊,不管調用的程序是否會拋出異常。編程

一些實踐

異常與枚舉

Spring容器中,你們通常使用聲明式事務來管理數據庫事務。在@Transaction的使用過程當中,必須指定對應的異常類型。筆者遇到不少項目中,回滾的異常是RuntimeException。問及緣由回答是Exception不會回滾,這實際上是配置不正確致使的。上面已經分析過了使用RuntimeException的壞處,那咱們如今來講說Exception。這種強制須要捕獲的異常配合自定義異常有很強的語義,便於高層靈活選擇處理,通常在工程中應用比較普遍。可是若是每種異常都定義一個新的類,這樣又顯得很囉嗦。一種常見的實踐是,經過枚舉值配合異常來作業務的判斷。以下:網絡

public enum ResultCodeEnum {
 /**  * 成功  */  SUCCESS("SUCCESS", "ok"),  /**  * 操做失敗  */  FAIL("FAIL", "操做失敗"),  /**  * 系統錯誤  */  ERROR("ERROR", "系統繁忙,請稍後再試。"),  /**  * 驗籤失敗  */  VERIFY_FAILED("VERIFY_FAILED", "驗籤失敗"),  /**  * 缺乏參數  */  LACK_PARAM("LACK_PARAM", "缺乏參數"),   ;    @Getter  private String code;  @Getter  private String msg;  private ResultCodeEnum(String code, String msg) {  this.code = code;  this.msg = msg;  } 複製代碼

異常中配合枚舉參數:編輯器

public class BusinessException extends Exception {
  private static final long serialVersionUID = -121219158129626814L;  @Getter  private ResultCodeEnum resultCode;  @Getter  private String msg;  public BusinessException() {  }  public BusinessException(ResultCodeEnum rsCode) {  super(rsCode.getCode() + ":" + rsCode.getMsg());  this.resultCode = rsCode;  this.msg = rsCode.getMsg();  }  public BusinessException(ResultCodeEnum rsCode, String message) {  super(rsCode.getCode() + ":" + message);  this.resultCode = rsCode;  this.msg = message;  }  public BusinessException(ResultCodeEnum rsCode, Throwable cause) {  super(rsCode.getCode() + ":" + rsCode.getMsg(), cause);  this.resultCode = rsCode;  this.msg = rsCode.getMsg();  }  public BusinessException(ResultCodeEnum rsCode, String message, Throwable cause) {  super(rsCode.getCode() + ":" + message, cause);  this.resultCode = rsCode;  this.msg = message;  } } 複製代碼

如此,在須要拋出異常的地方使用便可。學習

PayTypeEnum payTypeEnum = PayTypeEnum.toEumByName(payRequestDTO.getPayType());
if (payTypeEnum == null) {  throw new BusinessException(ResultCodeEnum.INVALID_PAY_TYPE); } 複製代碼

這樣外層必須捕獲這個異常,能夠根據 ResultCodeEnum 的值來區分業務進行相應的處理。this

統一返回值

通常而言,一個服務提供給外界的出參建議是統一的。可使用payload的模式將返回的結果包裝起來,你可能沒明白,看下下面這個類:

public class ResultMessageVO<T> {
  public static final String SUCCESS = "success";   public static final String ERROR = "error";  private String status; //狀態   private String message; //消息   private T data; //返回的數據   ... } 複製代碼

這樣,若是若是使用了切面或者其它全局異常的處理機制,就很容易規範返回。 以一個驗證參數的切面舉例:

@Aspect
@Component @Slf4j public class ValidationAspect {  @Around("execution(* io.github.pleuvoir.gateway..*.*(..))")  public Object around(ProceedingJoinPoint point) throws Throwable {  MethodSignature methodSignature = (MethodSignature) point.getSignature();  Method method = methodSignature.getMethod();  Object[] args = point.getArgs();  Parameter[] parameters = method.getParameters();  for (int i = 0; i < parameters.length; i++) {  if (parameters[i].isAnnotationPresent(Valid.class)) {  Object val = args[i];  ValidationResult validationResult = HibernateValidatorUtils.validateEntity(val);  if (validationResult.isHasErrors()) {  return ResultMessageVO.fail(ResultCodeEnum.PARAM_ERROR, validationResult.getErrorMessageOneway());  }  }  }  return point.proceed();  } } 複製代碼

由於咱們對外返回的都是 ResultMessageVO 因此能夠在切面中作統一處理,不然每一個方法都須要單獨作參數校驗,這就是統一返回值的好處。

固然了,若是有統一兜底異常的地方,也由於這個統一返回值的存在,更好處理:

@Slf4j
@ControllerAdvice public class GlobalExceptionHandler {  @ExceptionHandler(Exception.class)  @ResponseBody  public ResultMessageVO<?> exception(HttpServletRequest request, Exception e){  if (e instanceof NoHandlerFoundException) {  log.error("頁面不存在:{}", e.getMessage());  return new ResultMessageVO(ResultCodeEnum.ERROR, "頁面不存在");  } else if (e instanceof BindException) {  log.error("參數格式錯誤:{} url:{}", e.getMessage(), request.getRequestURI());  return new ResultMessageVO(ResultCodeEnum.INVALID_ARGUMENTS, "參數格式錯誤");  } else if (e instanceof HttpRequestMethodNotSupportedException){  log.error("不支持的請求方式:{} url:{}", e.getMessage(), request.getRequestURI());  return new ResultMessageVO(ResultCodeEnum.ERROR, "不支持的請求方式");  } else if (e instanceof BusinessException) {  BusinessException exception = (BusinessException) e;  log.warn("業務異常:{} url: {}", exception.getMsg(), request.getRequestURI());  return new ResultMessageVO<>(exception.getResultCodeEnum(), exception.getMsg());  } else {  log.error("系統異常:{} \t\r\n url: {} \t\r\n header: {} \t\r\n params: {} \t\r\n body: {}",  e.getMessage(), request.getRequestURI(), RequestUtil.getHeaders(request), RequestUtil.getParameterMap(request), getBody(request), e);  return new ResultMessageVO<>(ResultCodeEnum.ERROR);  }  }  private String getBody(HttpServletRequest request) {  String body = StringUtils.EMPTY;  try {  body = RequestUtil.getBody(request);  } catch (IOException e) {  log.error("打印系統異常日誌時,讀取請求body失敗,url:{}", request.getRequestURI(), e);  }  return body;  } } 複製代碼

經過以上兩步的組合拳,咱們成功使用受檢異常配合枚舉完成了異常的合理使用,再配合全局異常處理,完成了最後的兜底。這樣,程序中只須要捕獲 BusinessException 便可。成功的消除了噁心的代碼片斷。

後語

本文都是本人經驗的一些總結,不免疏漏甚至是錯誤,若是有不合理、不足,還望指正。另外,但願你們聊聊本身在項目中是如何使用異常的,互相學習。

相關文章
相關標籤/搜索