務(wù)異常體系設(shè)計(jì))
Java 業(yè)務(wù)異常體系設(shè)計(jì)一、核心概念在 Spring Boot 項(xiàng)目中異常分為兩大類(lèi)類(lèi)型含義誰(shuí)關(guān)心業(yè)務(wù)校驗(yàn)異常用戶(hù)輸入不合法、業(yè)務(wù)規(guī)則不滿(mǎn)足前端/用戶(hù)系統(tǒng)服務(wù)異常代碼邏輯錯(cuò)誤、外部依賴(lài)故障開(kāi)發(fā)/運(yùn)維兩者的本質(zhì)區(qū)別在于業(yè)務(wù)異常是預(yù)期內(nèi)的失敗系統(tǒng)異常是預(yù)期外的故障。注博客https://blog.csdn.net/badao_liumang_qizhi二、為什么要區(qū)分兩種異常如果不區(qū)分會(huì)出現(xiàn)這些問(wèn)題// 反面示例全部用 RuntimeExceptionthrownewRuntimeException(手機(jī)號(hào)格式不正確);// 業(yè)務(wù)校驗(yàn)thrownewRuntimeException(Redis連接超時(shí));// 系統(tǒng)故障全局異常處理器無(wú)法區(qū)分該返回 400 還是 500日志級(jí)別不好定——業(yè)務(wù)校驗(yàn) warn 就夠系統(tǒng)異常要 error前端不知道該展示錯(cuò)誤提示還是系統(tǒng)繁忙請(qǐng)重試監(jiān)控報(bào)警會(huì)被大量業(yè)務(wù)校驗(yàn)失敗淹沒(méi)三、異常類(lèi)設(shè)計(jì)3.1 基類(lèi)/** * 業(yè)務(wù)異常基類(lèi). */publicabstractclassBaseBusinessExceptionextendsRuntimeException{/** 錯(cuò)誤碼 */privateStringerrorCode;/** 給前端展示的消息 */privateStringdisplayMessage;publicBaseBusinessException(StringerrorCode,StringdisplayMessage){super(displayMessage);this.errorCodeerrorCode;this.displayMessagedisplayMessage;}publicStringgetErrorCode(){returnerrorCode;}publicStringgetDisplayMessage(){returndisplayMessage;}}3.2 業(yè)務(wù)校驗(yàn)異常CheckException用戶(hù)操作不符合業(yè)務(wù)規(guī)則時(shí)拋出。消息是給用戶(hù)看的。/** * 業(yè)務(wù)校驗(yàn)異常 — 用戶(hù)可感知、可處理的錯(cuò)誤. * * 場(chǎng)景參數(shù)校驗(yàn)失敗、業(yè)務(wù)規(guī)則不滿(mǎn)足、前置條件不具備 * HTTP 狀態(tài)碼200業(yè)務(wù)層面的失敗不是HTTP層面的錯(cuò)誤 * 日志級(jí)別WARN */publicclassCheckExceptionextendsBaseBusinessException{publicCheckException(StringerrorCode){super(errorCode,null);}publicCheckException(StringerrorCode,StringdisplayMessage){super(errorCode,displayMessage);}}3.3 系統(tǒng)服務(wù)異常ServerException系統(tǒng)內(nèi)部出錯(cuò)或外部依賴(lài)不可用時(shí)拋出。消息是給開(kāi)發(fā)排查用的。/** * 系統(tǒng)服務(wù)異常 — 非預(yù)期的系統(tǒng)錯(cuò)誤. * * 場(chǎng)景外部接口調(diào)用失敗、數(shù)據(jù)不一致、空指針前的主動(dòng)拋出 * HTTP 狀態(tài)碼200統(tǒng)一返回結(jié)構(gòu)通過(guò) successfalse 標(biāo)記 * 日志級(jí)別ERROR */publicclassServerExceptionextendsBaseBusinessException{publicServerException(Stringmessage){super(SYSTEM_ERROR,message);}publicServerException(Stringmessage,Throwablecause){super(SYSTEM_ERROR,message);initCause(cause);}}四、全局異常處理器通過(guò)RestControllerAdvice統(tǒng)一攔截異常返回標(biāo)準(zhǔn)化響應(yīng)RestControllerAdvicepublicclassGlobalExceptionHandler{privatestaticfinalLoggerlogLoggerFactory.getLogger(GlobalExceptionHandler.class);/** * 業(yè)務(wù)校驗(yàn)異常 — 返回錯(cuò)誤提示給前端. */ExceptionHandler(CheckException.class)publicRestControllerResult?handleCheckException(CheckExceptione){log.warn(業(yè)務(wù)校驗(yàn)失敗: errorCode{}, message{},e.getErrorCode(),e.getMessage());RestControllerResult?resultnewRestControllerResult();result.setSuccess(false);result.setErrorMsg(resolveMessage(e));result.setErrCode(e.getErrorCode());returnresult;}/** * 系統(tǒng)異常 — 返回通用提示詳細(xì)信息記入日志. */ExceptionHandler(ServerException.class)publicRestControllerResult?handleServerException(ServerExceptione){log.error(系統(tǒng)異常: {},e.getMessage(),e);RestControllerResult?resultnewRestControllerResult();result.setSuccess(false);result.setErrorMsg(系統(tǒng)繁忙請(qǐng)稍后重試);result.setErrCode(SYSTEM_ERROR);returnresult;}/** * 兜底 — 未預(yù)期的異常. */ExceptionHandler(Exception.class)publicRestControllerResult?handleException(Exceptione){log.error(未知異常,e);RestControllerResult?resultnewRestControllerResult();result.setSuccess(false);result.setErrorMsg(系統(tǒng)繁忙請(qǐng)稍后重試);returnresult;}/** * 解析錯(cuò)誤消息支持 i18n 資源 key 或直接文本. */privateStringresolveMessage(CheckExceptione){if(e.getDisplayMessage()!null){returne.getDisplayMessage();}// 嘗試從 i18n 資源文件解析 errorCode 對(duì)應(yīng)的文本// 如 xxx.delivery.confirm.install-time-empty → 請(qǐng)選擇安裝時(shí)間returnMessageSourceUtil.getMessage(e.getErrorCode());}}五、i18n 國(guó)際化消息CheckException 的 errorCode 模式當(dāng)CheckException只傳 errorCode 時(shí)通過(guò)資源文件解析對(duì)應(yīng)文案# messages.properties xxx.delivery.confirm.install-time-empty請(qǐng)選擇安裝時(shí)間 xxx.delivery.confirm.install-time-too-early安裝時(shí)間不能早于當(dāng)前時(shí)間1小時(shí) xxx.check.warehouse.delivery.range.error該倉(cāng)庫(kù)不在配送范圍內(nèi) // 使用方式 — 只傳 key throw new CheckException(xxx.delivery.confirm.install-time-empty); // 前端收到: {success:false, errorMsg:請(qǐng)選擇安裝時(shí)間}好處錯(cuò)誤文案統(tǒng)一管理修改不用改代碼支持多語(yǔ)言errorCode 可用于前端精確匹配特定錯(cuò)誤做差異化處理六、兩種異常的使用場(chǎng)景對(duì)比6.1 CheckException 適用場(chǎng)景// 1. 參數(shù)校驗(yàn)if(StringUtils.isEmpty(orderCode)){thrownewCheckException(ORDER_CODE_EMPTY,訂單號(hào)不能為空);}// 2. 業(yè)務(wù)規(guī)則校驗(yàn)if(stockdeliveryQty){thrownewCheckException(STOCK_NOT_ENOUGH,庫(kù)存不足當(dāng)前庫(kù)存stock);}// 3. 狀態(tài)校驗(yàn)if(!Objects.equals(order.getStatus(),WAIT_DELIVERY)){thrownewCheckException(ORDER_STATUS_ERROR,當(dāng)前訂單狀態(tài)不允許發(fā)貨);}// 4. 用 i18n key 的方式if(installTime.before(DateUtils.addHour(newDate(),1))){thrownewCheckException(stock.delivery.confirm.install-time-too-early);}6.2 ServerException 適用場(chǎng)景// 1. 外部服務(wù)調(diào)用失敗RestControllerResult?resultorderFeign.getOrderInfo(orderId);if(!Boolean.TRUE.equals(result.getSuccess())){thrownewServerException(查詢(xún)訂單失敗orderIdorderId, msgresult.getErrorMsg());}// 2. 數(shù)據(jù)一致性異常不應(yīng)該出現(xiàn)的情況WaitDeliveryMastermasterrepository.findById(id);if(masternull){thrownewServerException(xxx主表數(shù)據(jù)不存在idid);}// 3. 直接拼接錯(cuò)誤信息本次需求的用法thrownewServerException(goodsNames缺少安裝時(shí)間);七、通用示例一個(gè)完整的 Service 方法ServicepublicclassOrderServiceImplimplementsOrderService{OverridepublicvoidsubmitOrder(SubmitOrderParamparam){// 1. 參數(shù)校驗(yàn) → CheckExceptionif(param.getItems()null||param.getItems().isEmpty()){thrownewCheckException(ORDER_ITEMS_EMPTY,請(qǐng)至少選擇一件商品);}// 2. 業(yè)務(wù)規(guī)則校驗(yàn) → CheckException (i18n key)if(param.getTotalAmount().compareTo(BigDecimal.ZERO)0){thrownewCheckException(order.submit.amount-invalid);}// 3. 調(diào)用外部服務(wù) → ServerExceptionRestControllerResultStockInfostockResultstockFeign.checkStock(param.getItems());if(!Boolean.TRUE.equals(stockResult.getSuccess())){thrownewServerException(xx服務(wù)調(diào)用失敗: stockResult.getErrorMsg());}// 4. 動(dòng)態(tài)拼接的業(yè)務(wù)提示 → ServerExceptionListStringnoStockItemsfindNoStockItems(stockResult.getData(),param.getItems());if(!noStockItems.isEmpty()){thrownewServerException(String.join(,,noStockItems) 庫(kù)存不足);}// 5. 正常業(yè)務(wù)邏輯orderRepository.save(buildOrder(param));}}八、總結(jié)維度CheckExceptionServerException語(yǔ)義業(yè)務(wù)規(guī)則不滿(mǎn)足系統(tǒng)出了問(wèn)題消息對(duì)象用戶(hù)開(kāi)發(fā)者消息內(nèi)容i18n key 或用戶(hù)友好文案帶上下文的技術(shù)描述日志級(jí)別WARNERROR是否觸發(fā)告警一般不是HTTP 狀態(tài)碼200 successfalse200 successfalse前端處理展示 errorMsg 給用戶(hù)展示系統(tǒng)繁忙