深入聊一聊springboot項(xiàng)目全局異常處理那些事兒
前言
之前我們業(yè)務(wù)團(tuán)隊(duì)在處理全局異常時(shí),在每個(gè)業(yè)務(wù)微服務(wù)中都加入了@RestControllerAdvice+@ExceptionHandler來進(jìn)行全局異常捕獲。某次領(lǐng)導(dǎo)在走查代碼的時(shí)候,就提出了一個(gè)問題,為什么要每個(gè)微服務(wù)項(xiàng)目都要自己在寫一套全局異常代碼,為什么不把全局異常塊抽成一個(gè)公共的jar,然后每個(gè)微服務(wù)以jar的形式引入。后面業(yè)務(wù)團(tuán)隊(duì)就根據(jù)領(lǐng)導(dǎo)的要求,把全局異常塊單獨(dú)抽離出來封裝成jar。今天聊的話題就是關(guān)于把全局異常抽離出來,發(fā)生的一些問題
問題一:全局異常抽離出來后,業(yè)務(wù)錯(cuò)誤碼如何定義?
之前團(tuán)隊(duì)的業(yè)務(wù)錯(cuò)誤碼定義是:業(yè)務(wù)服務(wù)前綴 + 業(yè)務(wù)模塊 + 錯(cuò)誤碼,如果是識(shí)別不了的異常,則使用業(yè)務(wù)前綴 + 固定模塊碼 + 固定錯(cuò)誤碼。
之前的全局異常偽代碼如下
@RestControllerAdvice @Slf4j public class GlobalExceptionBaseHandler { @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public AjaxResult handleException(Exception e) { String servicePrifix = "U"; String moudleCode = "001"; String code = "0001"; String errorCode = servicePrifix + moudleCode + code; String msg = e.getMessage(); if(StringUtils.isEmpty(msg)){ msg = "服務(wù)端異常"; } log.error(msg, e); return AjaxResult.error(msg, errorCode); } }
現(xiàn)在全局異常抽離出來后,那個(gè)業(yè)務(wù)服務(wù)前綴如何識(shí)別?之前未抽離時(shí),業(yè)務(wù)服務(wù)前綴各個(gè)業(yè)務(wù)服務(wù)直接寫死在代碼里。
當(dāng)時(shí)我們臨時(shí)的解決方案是通過spring.application.name來解決。因?yàn)槿之惓4a塊抽離出來后,最終還是要被服務(wù)引入的。因此獲取業(yè)務(wù)服務(wù)前綴的偽代碼可以通過如下獲取
public enum ServicePrefixEnum { USER_SERVICE("U","用戶中心"); private final String servicePrefix; private final String serviceDesc; ServicePrefixEnum(String servicePrefix,String serviceDesc) { this.servicePrefix = servicePrefix; this.serviceDesc = serviceDesc; } public String getServicePrefix() { return servicePrefix; } public String getServiceDesc() { return serviceDesc; } }
public String getServicePrefix(@Value("${spring.application.name}") String serviceName){ return ServicePrefixEnum.valueOf(serviceName).getServicePrefix(); }
但這種方案其實(shí)是存在弊端
弊端一: 通過枚舉硬編碼,預(yù)設(shè)了目前了微服務(wù)名稱,一旦項(xiàng)目改變了微服務(wù)名,就找不到服務(wù)前綴了。
弊端二: 如果新上線了業(yè)務(wù)服務(wù)模塊,這個(gè)枚舉類還得改動(dòng)
后面我們?cè)谌之惓ar中增加了自定義業(yè)務(wù)碼的配置,業(yè)務(wù)人員僅需在springboot配置文件配置,形如下
lybgeek: bizcode: prefix: U
此時(shí)全局異常改造示例形如下
@RestControllerAdvice @Slf4j public class GlobalExceptionBaseHandler { @Autowired private ServiceCodeProperties serviceCodeProperties; @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public AjaxResult handleException(Exception e) { String servicePrifix = serviceCodeProperties.getPrifix(); String moudleCode = "001"; String code = "0001"; String errorCode = servicePrifix + moudleCode + code; String msg = e.getMessage(); if(StringUtils.isEmpty(msg)){ msg = "服務(wù)端異常"; } log.error(msg, e); return AjaxResult.error(msg, errorCode); } }
問題二:全局異常因引入了和業(yè)務(wù)相同的依賴jar,但jar存在版本差異
如果全局異常直接如下寫,是不存在問題。示例如下
@RestControllerAdvice @Slf4j public class GlobalExceptionBaseHandler { @Autowired private ServiceCodeProperties serviceCodeProperties; @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public AjaxResult handleException(Exception e) { String servicePrifix = serviceCodeProperties.getPrifix(); String moudleCode = "001"; String code = "0001"; String errorCode = servicePrifix + moudleCode + code; String msg = e.getMessage(); if(StringUtils.isEmpty(msg)){ msg = "服務(wù)端異常"; } log.error(msg, e); return AjaxResult.error(msg, HttpStatus.INTERNAL_SERVER_ERROR.value()); } @ExceptionHandler(BizException.class) public AjaxResult handleException(BizException e) { return AjaxResult.error(e.getMessage(), e.getErrorCode()); } }
即全局異常直接分為業(yè)務(wù)異常和Execption這兩種,這樣劃分的弊端在于沒辦法細(xì)分異常,而且也使項(xiàng)目組定義的模塊碼和業(yè)務(wù)碼沒法細(xì)分。因此我們也列出常用可以預(yù)知的系統(tǒng)異常,示例如下
/** *參數(shù)驗(yàn)證失敗 * @param e * @return */ @ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public AjaxResult handleException(ConstraintViolationException e) { log.error("參數(shù)驗(yàn)證失敗", e); return AjaxResult.error("參數(shù)驗(yàn)證失敗", HttpStatus.BAD_REQUEST.value()); } /** * 數(shù)據(jù)庫(kù)異常 * @param e * @return */ @ExceptionHandler({SQLException.class, MybatisPlusException.class, MyBatisSystemException.class, org.apache.ibatis.exceptions.PersistenceException.class, BadSqlGrammarException.class }) @ResponseStatus(HttpStatus.BAD_REQUEST) public AjaxResult dbException(Exception e) { String msg = ExceptionUtil.getExceptionMessage(e); log.error(msg, e); return AjaxResult.error(msg,HttpStatus.BAD_REQUEST.value()); } /** * 數(shù)據(jù)庫(kù)中已存在該記錄 * @param e * @return */ @ExceptionHandler(DuplicateKeyException.class) @ResponseStatus(HttpStatus.CONFLICT) public AjaxResult handleException(DuplicateKeyException e) { log.error("數(shù)據(jù)庫(kù)中已存在該記錄", e); return AjaxResult.error("數(shù)據(jù)庫(kù)中已存在該記錄", HttpStatus.CONFLICT.value()); }
不過這樣導(dǎo)致了一個(gè)問題,就是全局異常和業(yè)務(wù)方使用相同的依賴jar,但存在版本差異時(shí),可能就會(huì)存在依賴沖突,導(dǎo)致業(yè)務(wù)項(xiàng)目啟動(dòng)報(bào)錯(cuò)。因此解決方案就是在pom文件加入optional標(biāo)簽。示例如下
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <optional>true</optional> </dependency>
這標(biāo)簽的意思這jar坐標(biāo)是可選的,因此如果項(xiàng)目中已經(jīng)有引入該jar的坐標(biāo),就直接用該jar的坐標(biāo)
問題三:引入maven optional標(biāo)簽后,因業(yè)務(wù)沒引入全局異常需要的jar,導(dǎo)致項(xiàng)目啟動(dòng)報(bào)錯(cuò)
這個(gè)問題的產(chǎn)生:舉個(gè)示例,我們的業(yè)務(wù)微服務(wù)項(xiàng)目有聚合層,某些聚合層是不需要依賴存儲(chǔ)介質(zhì),比如mysql。因此這些聚合層項(xiàng)目pom就不會(huì)引入類似mybatis相關(guān)的依賴。但我們的全局異常又需要類似mybatis相關(guān)的依賴,這樣導(dǎo)致如果要引用全局異常模塊,有得額外加入業(yè)務(wù)方不需要的jar。
因此springboot的條件注解就派上用場(chǎng)了,利用@ConditionalOnClass注解。示例如下
@RestControllerAdvice @Slf4j @ConditionalOnClass({SQLException.class, MybatisPlusException.class, MyBatisSystemException.class, org.apache.ibatis.exceptions.PersistenceException.class, BadSqlGrammarException.class, DuplicateKeyException.class}) public class GlobalExceptionDbHandler { /** * 數(shù)據(jù)庫(kù)異常 * @param e * @return */ @ExceptionHandler({SQLException.class, MybatisPlusException.class, MyBatisSystemException.class, org.apache.ibatis.exceptions.PersistenceException.class, BadSqlGrammarException.class }) @ResponseStatus(HttpStatus.BAD_REQUEST) public AjaxResult dbException(Exception e) { String msg = ExceptionUtil.getExceptionMessage(e); log.error(msg, e); return AjaxResult.error(msg,HttpStatus.BAD_REQUEST.value()); } /** * 數(shù)據(jù)庫(kù)中已存在該記錄 * @param e * @return */ @ExceptionHandler(DuplicateKeyException.class) @ResponseStatus(HttpStatus.CONFLICT) public AjaxResult handleException(DuplicateKeyException e) { log.error("數(shù)據(jù)庫(kù)中已存在該記錄", e); return AjaxResult.error("數(shù)據(jù)庫(kù)中已存在該記錄", HttpStatus.CONFLICT.value()); } }
@ConditionalOnClass這個(gè)注解的作用就是如果classpath存在指定的類,則該注解上的類會(huì)生效。
同時(shí)這邊有個(gè)細(xì)節(jié)點(diǎn),就是全局異??赡芫偷眉?xì)分,即把原來的大一統(tǒng)的全局異常,按業(yè)務(wù)場(chǎng)景分開,比如存儲(chǔ)介質(zhì)相關(guān)的存儲(chǔ)異常,web相關(guān)異常
總結(jié)
本文主要講當(dāng)將全局異常抽離成jar,可能會(huì)發(fā)生的問題。這邊有涉及到一些細(xì)節(jié)點(diǎn)沒講,比如為啥要定義服務(wù)前綴+業(yè)務(wù)模塊碼+錯(cuò)誤碼,其實(shí)主要還是為了好排查問題。
也許有朋友會(huì)問,你們都搞了微服務(wù),難道不上分布式鏈路追蹤?根據(jù)分布式鏈路追蹤可以很方便定位到整個(gè)鏈路了。但真的開發(fā)微服務(wù)的時(shí)候,如果公司原來就就沒運(yùn)維平臺(tái),有時(shí)候?yàn)榱顺杀究剂?,測(cè)試、開發(fā)環(huán)境都不會(huì)上的分布式鏈路追蹤的,甚至線上項(xiàng)目初期也不會(huì)上分布式鏈路追蹤。因此定義好相關(guān)的業(yè)務(wù)碼就變得格外重要
到此這篇關(guān)于springboot項(xiàng)目全局異常處理那些事兒的文章就介紹到這了,更多相關(guān)springboot項(xiàng)目全局異常處理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
demo鏈接
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-exception
相關(guān)文章
SpringBoot @SpringBootTest加速單元測(cè)試的小訣竅
這篇文章主要介紹了SpringBoot @SpringBootTest加速單元測(cè)試的小訣竅,具有很好的參考價(jià)值,對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11java中帶參數(shù)的try(){}語(yǔ)法含義詳解
這篇文章主要介紹了java中帶參數(shù)的try(){}語(yǔ)法含義詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02springboot支持https請(qǐng)求的實(shí)現(xiàn)
本文主要介紹了springboot支持https請(qǐng)求的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01Springmvc nginx實(shí)現(xiàn)動(dòng)靜分離過程詳解
這篇文章主要介紹了Springmvc nginx實(shí)現(xiàn)動(dòng)靜分離過程詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09SpringBoot從配置文件中獲取屬性的四種方法總結(jié)
這篇文章主要介紹了SpringBoot從配置文件中獲取屬性的四種方法總結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02spring boot啟動(dòng)時(shí)加載外部配置文件的方法
這篇文章主要給大家介紹了關(guān)于spring boot啟動(dòng)時(shí)加載外部配置文件的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-02-02spring注解之@Valid和@Validated的區(qū)分總結(jié)
@Validated和@Valid在基本驗(yàn)證功能上沒有太多區(qū)別,但在分組、注解地方、嵌套驗(yàn)證等功能上有所不同,下面這篇文章主要給大家介紹了關(guān)于spring注解之@Valid和@Validated區(qū)分的相關(guān)資料,需要的朋友可以參考下2022-03-03Java基于注解實(shí)現(xiàn)的鎖實(shí)例解析
這篇文章主要介紹了Java基于注解實(shí)現(xiàn)的鎖實(shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02深入解析StringBuffer和StringBuilder的區(qū)別
以下是對(duì)java中StringBuffer與StringBuilder的區(qū)別進(jìn)行了詳細(xì)的分析介紹,需要的朋友可以參考下2013-07-07