在筆者從業的多年時間內,參與設計了不少系統。從知足業務須要的角度出發,能快速支撐業務發展都能稱之爲「好」的系統。畢竟,創造價值的是業務,若是沒有業務驅動,工程師掌握屠龍之技沒有龍也是至關苦悶之事。作爲一名卓越的程序員,相信你們都但願本身開發的系統易維護,更健壯。 固然這只是一種理想主義。且不說互聯網行業瞬息萬變,單是工期緊,面向deadline
編程就須要使不少工程師放棄對代碼維護性的執念。就筆者我的經歷而言,早上拿到需求文檔晚上上線也是常有的事。儘管如此,我仍是但願從我的角度來談談,一些簡單易行、順手培養的習慣到底能給程序維護性帶來怎樣的便利。 本文是從異常的使用着手,聊一聊使用不當帶來的壞味道。git
在Java
中存在RuntimeException
和Exception
。典型的如NullPointerException
就屬於RuntimeException
,這些異常不須要開發者捕獲在運行時一旦觸發自動拋出。同時,RuntimeException
也被稱爲非受檢異常,見名知意,在編譯期不受檢查。除了RuntimeException
之外都稱爲受檢異常,在編譯期須要強制處理,拋出或捕獲,常見的如ClassNotFoundException
,InterruptedException
。程序員
筆者見過不少這樣的代碼:github
PayOrder selectByOrderId(Long orderId) throw Exception;
複製代碼
出於對網絡鏈接/數據庫的懷疑,總感受本身寫的SQL
會拋出異常,而且想讓上游去處理這個異常。美其名曰,面向防護編程,但其實給程序維護增長了不少的煩惱。 首先,由於這種異常的拋出很泛泛,調用方並不知道拋出這個異常的人當時的想法,只能在外層強制捕獲這個異常。若是上層調用者也不想處理這個異常,他又會繼續往上層拋,這樣處理幾回後,最外層的程序塊則徹底不知道這個異常是什麼狀況下拋出的了。 可能不少小夥伴會說,個人編程風格就是別人有異常本身能處理就處理,不能處理才往上層拋。那我想說,你真棒。可是,一個項目中並非只有一個開發者,每一個人都有本身的編程習慣。有的小夥伴就是喜歡把異常往外層拋,繼續爲難上層調用者。 固然,這裏並非說拋異常很差。合理的使用異常能使程序的結構更清晰,語義更明確。也方便高層調用者針對不一樣的異常進行處理,而不是隻能無奈的捕獲Exception
。web
接着上文,若是有人從很最底層一路拋出了一個受檢異常。在這樣的系統中,想一想你們會怎麼作呢?很容易,捕獲就行。可是,若是下層拋出的是一個非受檢異常
也就是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
便可。成功的消除了噁心的代碼片斷。
本文都是本人經驗的一些總結,不免疏漏甚至是錯誤,若是有不合理、不足,還望指正。另外,但願你們聊聊本身在項目中是如何使用異常的,互相學習。