深入聊一聊springboot項目全局異常處理那些事兒
前言
之前我們業(yè)務團隊在處理全局異常時,在每個業(yè)務微服務中都加入了@RestControllerAdvice+@ExceptionHandler來進行全局異常捕獲。某次領導在走查代碼的時候,就提出了一個問題,為什么要每個微服務項目都要自己在寫一套全局異常代碼,為什么不把全局異常塊抽成一個公共的jar,然后每個微服務以jar的形式引入。后面業(yè)務團隊就根據(jù)領導的要求,把全局異常塊單獨抽離出來封裝成jar。今天聊的話題就是關于把全局異常抽離出來,發(fā)生的一些問題
問題一:全局異常抽離出來后,業(yè)務錯誤碼如何定義?
之前團隊的業(yè)務錯誤碼定義是:業(yè)務服務前綴 + 業(yè)務模塊 + 錯誤碼,如果是識別不了的異常,則使用業(yè)務前綴 + 固定模塊碼 + 固定錯誤碼。
之前的全局異常偽代碼如下
@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 = "服務端異常";
}
log.error(msg, e);
return AjaxResult.error(msg, errorCode);
}
}現(xiàn)在全局異常抽離出來后,那個業(yè)務服務前綴如何識別?之前未抽離時,業(yè)務服務前綴各個業(yè)務服務直接寫死在代碼里。
當時我們臨時的解決方案是通過spring.application.name來解決。因為全局異常代碼塊抽離出來后,最終還是要被服務引入的。因此獲取業(yè)務服務前綴的偽代碼可以通過如下獲取
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();
}但這種方案其實是存在弊端
弊端一: 通過枚舉硬編碼,預設了目前了微服務名稱,一旦項目改變了微服務名,就找不到服務前綴了。
弊端二: 如果新上線了業(yè)務服務模塊,這個枚舉類還得改動
后面我們在全局異常jar中增加了自定義業(yè)務碼的配置,業(yè)務人員僅需在springboot配置文件配置,形如下
lybgeek:
bizcode:
prefix: U此時全局異常改造示例形如下
@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 = "服務端異常";
}
log.error(msg, e);
return AjaxResult.error(msg, errorCode);
}
}問題二:全局異常因引入了和業(yè)務相同的依賴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 = "服務端異常";
}
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è)務異常和Execption這兩種,這樣劃分的弊端在于沒辦法細分異常,而且也使項目組定義的模塊碼和業(yè)務碼沒法細分。因此我們也列出常用可以預知的系統(tǒng)異常,示例如下
/**
*參數(shù)驗證失敗
* @param e
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public AjaxResult handleException(ConstraintViolationException e)
{
log.error("參數(shù)驗證失敗", e);
return AjaxResult.error("參數(shù)驗證失敗", HttpStatus.BAD_REQUEST.value());
}
/**
* 數(shù)據(jù)庫異常
* @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ù)庫中已存在該記錄
* @param e
* @return
*/
@ExceptionHandler(DuplicateKeyException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public AjaxResult handleException(DuplicateKeyException e)
{
log.error("數(shù)據(jù)庫中已存在該記錄", e);
return AjaxResult.error("數(shù)據(jù)庫中已存在該記錄", HttpStatus.CONFLICT.value());
}不過這樣導致了一個問題,就是全局異常和業(yè)務方使用相同的依賴jar,但存在版本差異時,可能就會存在依賴沖突,導致業(yè)務項目啟動報錯。因此解決方案就是在pom文件加入optional標簽。示例如下
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<optional>true</optional>
</dependency>這標簽的意思這jar坐標是可選的,因此如果項目中已經(jīng)有引入該jar的坐標,就直接用該jar的坐標
問題三:引入maven optional標簽后,因業(yè)務沒引入全局異常需要的jar,導致項目啟動報錯
這個問題的產(chǎn)生:舉個示例,我們的業(yè)務微服務項目有聚合層,某些聚合層是不需要依賴存儲介質(zhì),比如mysql。因此這些聚合層項目pom就不會引入類似mybatis相關的依賴。但我們的全局異常又需要類似mybatis相關的依賴,這樣導致如果要引用全局異常模塊,有得額外加入業(yè)務方不需要的jar。
因此springboot的條件注解就派上用場了,利用@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ù)庫異常
* @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ù)庫中已存在該記錄
* @param e
* @return
*/
@ExceptionHandler(DuplicateKeyException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public AjaxResult handleException(DuplicateKeyException e)
{
log.error("數(shù)據(jù)庫中已存在該記錄", e);
return AjaxResult.error("數(shù)據(jù)庫中已存在該記錄", HttpStatus.CONFLICT.value());
}
}@ConditionalOnClass這個注解的作用就是如果classpath存在指定的類,則該注解上的類會生效。
同時這邊有個細節(jié)點,就是全局異??赡芫偷眉毞?,即把原來的大一統(tǒng)的全局異常,按業(yè)務場景分開,比如存儲介質(zhì)相關的存儲異常,web相關異常
總結(jié)
本文主要講當將全局異常抽離成jar,可能會發(fā)生的問題。這邊有涉及到一些細節(jié)點沒講,比如為啥要定義服務前綴+業(yè)務模塊碼+錯誤碼,其實主要還是為了好排查問題。
也許有朋友會問,你們都搞了微服務,難道不上分布式鏈路追蹤?根據(jù)分布式鏈路追蹤可以很方便定位到整個鏈路了。但真的開發(fā)微服務的時候,如果公司原來就就沒運維平臺,有時候為了成本考量,測試、開發(fā)環(huán)境都不會上的分布式鏈路追蹤的,甚至線上項目初期也不會上分布式鏈路追蹤。因此定義好相關的業(yè)務碼就變得格外重要
到此這篇關于springboot項目全局異常處理那些事兒的文章就介紹到這了,更多相關springboot項目全局異常處理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
demo鏈接
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-exception
相關文章
Java中的System.getenv()和System.getProperty()使用詳解
文章介紹了Java中用于讀取環(huán)境配置信息的兩種方法:System.getenv()和System.getProperty(),前者讀取系統(tǒng)環(huán)境變量,返回一個不可修改的Map;后者獲取JVM環(huán)境變量值,可以通過-D參數(shù)設置,文章還提到,通過這兩種方法可以簡化配置,不需要修改代碼2024-11-11
quartz定時執(zhí)行任務,并配置web.xml的操作方法
下面小編就為大家?guī)硪黄猶uartz定時執(zhí)行任務,并配置web.xml的操作方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-07-07
Java數(shù)據(jù)導出功能之導出Excel文件實例
這篇文章主要介紹了Java數(shù)據(jù)導出功能之導出Excel文件實例,本文給出了jar包的下載地址,并給出了導出Excel文件代碼實例,需要的朋友可以參考下2015-06-06
java實現(xiàn)酷狗音樂臨時緩存文件轉(zhuǎn)換為MP3文件的方法
這篇文章主要介紹了java實現(xiàn)酷狗音樂臨時緩存文件轉(zhuǎn)換為MP3文件的方法,涉及java針對文件操作的相關技巧,需要的朋友可以參考下2016-08-08

