一些好的異常處理實踐。
目錄
- 異常設計
- 自定義異常
- 為異常設計錯誤代碼(狀態碼)
- 設計粒度
- 全局異常處理
- 異常日志信息保留
- 異常處理時機
- 資源管理
- try-with-resources
- 異常中的事務
異常設計
自定義異常
自定義異常設計,如業務異常定義BusinessException,設置一個基礎異常類,如XXAppBaseException(或就叫BaseException),然后讓各類異常繼承,如下:
public class UserException extends XXAppBaseException { ... }public class MapException extends XXAppBaseException { ... }
這里異常的劃分可以按照模塊、業務來區分,也可以分離業務代碼異常與技術代碼異常。
為異常設計錯誤代碼(狀態碼)
常見的異常代碼設計有HTTP的異常狀態碼,如404、500、502這種。
這樣做主要是便于日志分析和客戶端處理,很明顯,使用錯誤代碼做篩選能提升檢索效率、方便收集、自動化處理,且使用異常狀態碼來傳輸異常信息提升了信息傳輸與存儲效率。
等等……
設計粒度
自定義異常和異常錯誤代碼都是比較常見的操作,但是設計時需要考慮粒度。
一般有層級關系的設計更便于理解、維護。
在自定義異常中就是多層繼承關系,在異常錯誤碼中就是分層錯誤碼設計,如全局錯誤碼 > 模塊錯誤碼 > 具體錯誤碼
5xx—>5xxx->5xxxx
全局異常處理
使用Spring的@ControllerAdvice
或類似機制統一處理異常。如:
/*** 全局異常處理器* * @author ruoyi*/
@RestControllerAdvice
public class GlobalExceptionHandler
{private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);/*** 權限校驗異常(ajax請求返回json,redirect請求跳轉頁面)*/@ExceptionHandler(AuthorizationException.class)public Object handleAuthorizationException(AuthorizationException e, HttpServletRequest request){String requestURI = request.getRequestURI();log.error("請求地址'{}',權限校驗失敗'{}'", requestURI, e.getMessage());if (ServletUtils.isAjaxRequest(request)){return AjaxResult.error(PermissionUtils.getMsg(e.getMessage()));}else{return new ModelAndView("error/unauth");}}/*** 請求方式不支持*/@ExceptionHandler(HttpRequestMethodNotSupportedException.class)public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,HttpServletRequest request){String requestURI = request.getRequestURI();log.error("請求地址'{}',不支持'{}'請求", requestURI, e.getMethod());return AjaxResult.error(e.getMessage());}/*** 攔截未知的運行時異常*/@ExceptionHandler(RuntimeException.class)public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request){String requestURI = request.getRequestURI();log.error("請求地址'{}',發生未知異常.", requestURI, e);return AjaxResult.error(e.getMessage());}/*** 系統異常*/@ExceptionHandler(Exception.class)public AjaxResult handleException(Exception e, HttpServletRequest request){String requestURI = request.getRequestURI();log.error("請求地址'{}',發生系統異常.", requestURI, e);return AjaxResult.error(e.getMessage());}/*** 業務異常*/@ExceptionHandler(ServiceException.class)public Object handleServiceException(ServiceException e, HttpServletRequest request){log.error(e.getMessage(), e);if (ServletUtils.isAjaxRequest(request)){return AjaxResult.error(e.getMessage());}else{return new ModelAndView("error/service", "errorMessage", e.getMessage());}}
}
異常日志信息保留
在拋出異常時,建議連帶當前業務標識信息一起拋出(每一層日志拋出記錄標識),這樣方便排查問題。
異常處理時機
偶爾能看到一類將所有代碼塊都包起來的try-catch異常處理,這種代碼被稱為“防御式編程”的過度使用,會導致多種問題。
- 代碼可讀性降低:大量的異常處理代碼掩蓋了業務邏輯
- 異常信息丟失:每層都捕獲并重新拋出,可能丟失原始堆棧信息
- 難以維護:當需要修改異常處理邏輯時,需要修改多處代碼
- 性能影響:過多的try-catch會對性能產生輕微影響
異常處理應該在能夠正確響應的層級進行,如:
- 邊界層處理:如API層、用戶界面層、外部系統集成點
- 業務決策點處理:在能夠做出恢復決策的地方處理異常
- 資源管理點處理:在使用需要清理的資源的地方處理
// 不好的做法
public void badExceptionHandling() {try {// 獲取用戶User user = null;try {user = userRepository.findById(userId);} catch (Exception e) {log.error("Failed to get user", e);}// 獲取訂單Order order = null;try {order = orderRepository.findById(orderId);} catch (Exception e) {log.error("Failed to get order", e);}// 處理業務邏輯try {processOrder(user, order);} catch (Exception e) {log.error("Failed to process order", e);}} catch (Exception e) {log.error("Unexpected error", e);}
}
// 好的做法
public void goodExceptionHandling() {try {User user = userRepository.findById(userId);Order order = orderRepository.findById(orderId);processOrder(user, order);} catch (UserNotFoundException e) {// 有針對性地處理用戶不存在的情況log.warn("Order processing failed: User not found", e);notifyAdministrator(e);} catch (OrderNotFoundException e) {// 有針對性地處理訂單不存在的情況log.warn("Order processing failed: Order not found", e);notifyCustomer(userId, e);} catch (BusinessException e) {// 處理所有業務異常log.warn("Business rule violation during order processing", e);// 可能的補救措施} catch (Exception e) {// 處理所有其他未預期的異常log.error("Unexpected error during order processing", e);// 緊急措施}
}
資源管理
try-with-resources
使用try-with-resources自動關閉資源,防止泄露。為自己的資源類實現AutoCloseable接口,如:
public class ResourceLock implements AutoCloseable {// 獲取資源public ResourceLock() { /* 獲取鎖或資源 */ }@Overridepublic void close() { /* 釋放鎖或資源 */ }
}
使用finally和這個一樣。常規操作。
異常中的事務
在Spring框架中,默認情況運行時異常與嚴重問題會導致事務回滾,檢查型異常不會。
- 運行時異常(unchecked):繼承自
RuntimeException
的異常,默認導致事務回滾 - 檢查型異常(checked):繼承自
Exception
但不是RuntimeException
的子類,默認不會導致事務回滾 - Error:嚴重問題,如
OutOfMemoryError
,默認導致事務回滾
因此,我們需要注意異常對事務的影響。