文章會(huì)從三個(gè)方面進(jìn)行分析:
- 提出統(tǒng)一異常處理機(jī)制的好處,以及該機(jī)制使用姿勢(shì)
- 提供案例:不使用該機(jī)制會(huì)產(chǎn)生什么樣的情況
- 機(jī)制背后對(duì)應(yīng)的原理分析(重點(diǎn))
機(jī)制好處及使用姿勢(shì)
Spring MVC為我們的WEB應(yīng)用提供了統(tǒng)一異常處理機(jī)制,其好處是:
- 業(yè)務(wù)邏輯和異常處理解耦(業(yè)務(wù)代碼不應(yīng)該過多地關(guān)注異常的處理[職責(zé)單一原則])
- 消除充斥各處的
try catch
塊代碼恤磷,使代碼更整潔 - 便于統(tǒng)一向前端、客戶端返回友好的錯(cuò)誤提示
使用姿勢(shì)如下
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // Http Status Code 500
public ResponseDTO handleException(Exception e) {
// 兜底邏輯河胎,通常用于處理未預(yù)期的異常仿粹,比如不知道哪兒冒出來的空指針異常
log.error("", e);
return ResponseDTO.failedResponse().withErrorMessage("服務(wù)器開小差了");
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST) // Http Status Code 400
public ResponseDTO handleBizException(BizException e) {
// 可預(yù)期的業(yè)務(wù)異常,根據(jù)實(shí)際情況晌区,決定是否要打印異常堆棧
log.warn("業(yè)務(wù)異常:{}", e);
return ResponseDTO.failedResponse().withErrorMessage(e.getMessage());
}
}
注:該demo隱含的前提條件如下
- 使用Lombok(當(dāng)然恼五,也可以手動(dòng)獲取Logger)
-
GlobalExceptionHandler
需要被@ControllerAdvice
(Spring 3.2+)或@RestControllerAdvice
(Spring 4.3+)注解灾馒,并且能夠被Spring掃描到
為配合解釋該解決方案,再提供一些基礎(chǔ)信息
- 業(yè)務(wù)異常類
- 響應(yīng)信息包裝類
// 1
public class BizException extends RuntimeException {
public BizException(String message) {
super(message);
}
}
// 2
@Data
public class ResponseDTO<T> implements Serializable {
private static final long serialVersionUID = -3436143993984825439L;
private boolean ok = false;
private T data;
private String errorMessage = "";
public static ResponseDTO successResponse() {
ResponseDTO message = new ResponseDTO();
message.setOk(true);
return message;
}
public static ResponseDTO failedResponse() {
ResponseDTO message = new ResponseDTO();
message.setOk(false);
return message;
}
public ResponseDTO withData(T data) {
this.data = data;
return this;
}
public ResponseDTO withErrorMessage(String errorMsg) {
this.errorMessage = errorMsg;
return this;
}
}
案例分析
案例分析一:
@GetMapping("/testBizException")
public ResponseDTO testBizException() {
if (checkFailed) {
throw new BizException("test BizException");
}
}
當(dāng)我們請(qǐng)求/testBizException
時(shí),該接口在校驗(yàn)失敗后拋出了一個(gè)BizException
,用以代表我們的業(yè)務(wù)異常算芯,比如參數(shù)校驗(yàn)失敗(解決方案還有JSR-303的Bean Validation也祠,在此不討論)堪旧,優(yōu)惠券已過期等等業(yè)務(wù)異常信息淳梦。如果沒有統(tǒng)一異常處理爆袍,我們可能會(huì)使用如下方式
try {
// check
} catch (BizException e) {
return ResponseDTO.failedResponse().withErrorMessage("test BizException");
}
這種方式弦疮,一是不優(yōu)雅,二是業(yè)務(wù)邏輯跟異常處理耦合在了一起啸罢。
使用統(tǒng)一異常處理之后允懂,直接拋出業(yè)務(wù)異常蕾总,并提供異常上下文(message + errorCode),代碼會(huì)流轉(zhuǎn)到GlobalExceptionHandler#handleBizException
置侍,統(tǒng)一打印業(yè)務(wù)日志以及返回錯(cuò)誤碼和業(yè)務(wù)異常信息,且Http Status Code 返回400秕衙。
案例分析二:
@GetMapping("/testUnExpectedException")
public ResponseDTO testUnExpectedException() {
int i = 1 / 0;
}
當(dāng)我們請(qǐng)求/testUnExpectedException
時(shí),該接口會(huì)拋出java.lang.ArithmeticException: / by zero
,用以代表未預(yù)期的異常汉规,比如該案例中的0除
異常,僅管此處能一眼辯識(shí)出來,但更多的時(shí)候射亏,0
由變量表示及舍,很容易被忽視锯玛,又或者是其它未預(yù)期的空指針異常等。當(dāng)不知道哪里有可能會(huì)出異常,又為了前端友好提示病曾,其中一個(gè)做法就是try catch
大包大攬,將整個(gè)方法都try catch
住逼蒙,于是代碼產(chǎn)生了腐朽的味道陕截。
try {
// do business
} catch (Exception e) {
log.error("xxx", e);
return ResponseDTO.failedResponse().withErrorMessage("服務(wù)器開小差");
}
使用統(tǒng)一異常處理之后艘策,業(yè)務(wù)代碼里不再充斥(濫用)try catch
塊却汉,只需要關(guān)心業(yè)務(wù)邏輯,當(dāng)出現(xiàn)不可預(yù)期的異常時(shí),代碼會(huì)流轉(zhuǎn)到GlobalExceptionHandler#handleException
谈息,統(tǒng)一打印異常堆棧,以及返回錯(cuò)誤碼和統(tǒng)一異常信息,且Http Status Code 返回500余素。
以上便是Spring MVC為我們提供的統(tǒng)一異常處理機(jī)制窑眯,我們可以好好加以利用炊林。實(shí)際上渣聚,該機(jī)制在很多公司都在使用,可以從一些開源代碼管中窺豹,其中著名的代表就有Apollo
,參考com.ctrip.framework.apollo.common.controller.GlobalDefaultExceptionHandler
原理分析
了解存在的問題,以及對(duì)應(yīng)的解決方案之后闰蛔,接下來分析統(tǒng)一異常處理的工作原理
前提假設(shè):
- 原理分析基于Spring Boot
1.5.19.RELEASE
,對(duì)應(yīng)的springframework版本為4.3.22.RELEASE
- 理解Spring Boot自動(dòng)裝配原理
先來分析Spring Boot是如何使GlobalExceptionHandler
生效的,步驟如下:
- 啟動(dòng)類
(XXXApplication)
被@SpringBootApplication
注解余佃,而@SpringBootApplication
又被@EnableAutoConfiguration
所注解爆土,@EnableAutoConfiguration
導(dǎo)入EnableAutoConfigurationImportSelector -
EnableAutoConfigurationImportSelector
實(shí)現(xiàn)了ImportSelector
接口背犯,其核心方法是selectImports
漠魏,在該方法中有一行代碼是List<String> configurations = getCandidateConfigurations(annotationMetadata,attributes);
其含義是通過Spring 的SPI機(jī)制,從classpath 所有jar包的META-INF/spring.factories
文件中瞧毙,找到EnableAutoConfiguration
對(duì)應(yīng)的"一堆"類[自動(dòng)裝配原理]鲜结。這些類作為selectImports
的返回值灵汪,后期會(huì)被Spring加載并實(shí)例化,并置入IOC容器中差牛,其中有一項(xiàng)為WebMvcAutoConfiguration -
WebMvcAutoConfiguration
類存在內(nèi)部類WebMvcAutoConfigurationAdapter
驶冒,內(nèi)部類將導(dǎo)入EnableWebMvcConfiguration(WebMvcConfigurationSupport
的子類) -
WebMvcConfigurationSupport
有個(gè)factory methodhandlerExceptionResolver()
骗污,該方法向Spring容器中注冊(cè)了一個(gè)HandlerExceptionResolverComposite
(實(shí)現(xiàn)HandlerExceptionResolver
接口),并且默認(rèn)情況下沈条,給該Composite類添加了三個(gè)HandlerExceptionResolver
需忿,其中有一個(gè)類為ExceptionHandlerExceptionResolver -
ExceptionHandlerExceptionResolver
在InitializingBean
的回調(diào)方法afterPropertiesSet
中,調(diào)用initExceptionHandlerAdviceCache()
方法進(jìn)行異常處理器通知緩存的初始化:查找IOC容器中拍鲤,所有被@ControllerAdvice
注解的Bean贴谎,如果Bean中存在異常映射,則該Bean會(huì)作為key季稳,對(duì)應(yīng)的ExceptionHandlerMethodResolver作為value被緩存起來 -
ExceptionHandlerMethodResolver
是真正干活的類擅这,用于解析被@ExceptionHandler
注解的方法,保存異常類及對(duì)應(yīng)的異處常理方法<exceptionType, method>
景鼠。對(duì)應(yīng)到上述案例一仲翎,保存的是BizException
到handleBizException()
方法的映射關(guān)系,表明:當(dāng)業(yè)務(wù)代碼拋出BizException
時(shí)铛漓,會(huì)由handleBizException()
進(jìn)行處理
private void initExceptionHandlerAdviceCache() {
...
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
AnnotationAwareOrderComparator.sort(adviceBeans);
for (ControllerAdviceBean adviceBean : adviceBeans) {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
...
}
...
}
}
應(yīng)用啟動(dòng)完畢之后溯香,GlobalExceptionHandler
已經(jīng)生效,即exceptionHandlerAdviceCache
已經(jīng)緩存了異常處理器及其對(duì)應(yīng)的ExceptionHandlerMethodResolver
浓恶,一旦發(fā)生了異常玫坛,會(huì)從exceptionHandlerAdviceCache
里依次判斷哪個(gè)異常處理器可以用,并找到對(duì)應(yīng)的異常處理方法進(jìn)行異常的處理包晰。
接著分析異常處理的具體流程湿镀,當(dāng)一個(gè)Controller方法中拋出異常后炕吸,步驟如下:
- org.springframework.web.servlet.DispatcherServlet#doDispatch會(huì)catch住異常,并調(diào)用
processDispatchResult();
方法進(jìn)行異常的處理
// DispatcherServlet#processHandlerException
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// DispatcherServlet#processHandlerException
for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
這里的this.handlerExceptionResolvers
是在Spring Boot啟動(dòng)的過程中初始化的勉痴,其中就包含上述啟動(dòng)步驟4中的HandlerExceptionResolverComposite
赫模。因此,這里會(huì)調(diào)用HandlerExceptionResolverComposite
的resolveException
方法進(jìn)行異常的處理
-
XXXComposite
在Spring中是個(gè)組合類蒸矛,一般內(nèi)部會(huì)維護(hù)一個(gè)由Composite
父接口實(shí)例構(gòu)成的列表瀑罗,如HandlerExceptionResolverComposite
實(shí)現(xiàn)了HandlerExceptionResolver
接口,其內(nèi)部維護(hù)了一個(gè)HandlerExceptionResolver
集合雏掠。HandlerExceptionResolverComposite
的resolveException
方法同樣是迭代其內(nèi)部維護(hù)的集合斩祭,并依次調(diào)用其resolveException
方法進(jìn)行解析,
其內(nèi)部集合中有一個(gè)ExceptionHandlerExceptionResolver
實(shí)例磁玉,且首先會(huì)進(jìn)入該實(shí)例進(jìn)行處理
// HandlerExceptionResolverComposite#resolveException
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,Exception ex) {
if (this.resolvers != null) {
for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (mav != null) {
return mav;
}
}
}
return null;
}
- 根據(jù)拋出的異常類型停忿,拿到異常處理器及對(duì)應(yīng)的異常處理方法,并轉(zhuǎn)化成ServletInvocableHandlerMethod蚊伞,并執(zhí)行
invokeAndHandle
方法席赂,也即是說,最終會(huì)轉(zhuǎn)換成執(zhí)行異常處理器的異常處理方法时迫。(org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle
是Spring MVC處理Http請(qǐng)求中的重要方法颅停,篇幅原因不在此介紹其原理)
// ExceptionHandlerExceptionResolver#doResolveHandlerMethodException
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
...
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
...
// ExceptionHandlerExceptionResolver#getExceptionHandlerMethod
// 這段邏輯是@ExceptionHandler寫在Controller類里的處理方式,這種方式不通用也不常用掠拳,不做介紹
...
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
ControllerAdviceBean advice = entry.getKey();
// @ControllerAdvice注解可以指定僅攔截某些類癞揉,這里判斷handlerType是否在其作用域內(nèi)
if (advice.isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = entry.getValue();
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
}
}
}
根據(jù)異常,找到異常處理方法
// ExceptionHandlerMethodResolver#resolveMethodByExceptionType
public Method resolveMethod(Exception exception) {
Method method = resolveMethodByExceptionType(exception.getClass());
if (method == null) {
Throwable cause = exception.getCause();
if (cause != null) {
method = resolveMethodByExceptionType(cause.getClass());
}
}
return method;
}
// ExceptionHandlerMethodResolver#resolveMethodByExceptionType
public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
Method method = this.exceptionLookupCache.get(exceptionType);
if (method == null) {
// 核心方法
method = getMappedMethod(exceptionType);
this.exceptionLookupCache.put(exceptionType, (method != null ? method : NO_METHOD_FOUND));
}
return (method != NO_METHOD_FOUND ? method : null);
}
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>();
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
if (mappedException.isAssignableFrom(exceptionType)) {
matches.add(mappedException);
}
}
// 如果找到多個(gè)匹配的異常溺欧,就排序之后取第一個(gè)(最優(yōu)的)
if (!matches.isEmpty()) {
Collections.sort(matches, new ExceptionDepthComparator(exceptionType));
return this.mappedMethods.get(matches.get(0));
}
else {
return null;
}
}
案例中喊熟,我們的mappedException
有兩個(gè):BizException
與Exception
,都滿足mappedException.isAssignableFrom(exceptionType)
條件姐刁,均會(huì)被加入matches中芥牌,經(jīng)過排序之后,"最匹配"的BizException
會(huì)排在matchs集合的第一個(gè)位置聂使,所以會(huì)選擇它所對(duì)應(yīng)的異常處理方法返回壁拉。因此,"最匹配"的關(guān)鍵點(diǎn)就在于比較器ExceptionDepthComparator
柏靶,根據(jù)類名弃理,可以推測(cè)其出比較的依據(jù)是目標(biāo)異常類與待排序異常類的"深度"。
舉個(gè)例子屎蜓,假設(shè)目標(biāo)異常類為BizException
痘昌,而待排序的集合中分別有BizException
、RuntimeException
、Exception
控汉,那么他們之間的深度分別為0笔诵,1,2姑子,因此,排序之后测僵,集合中的BizException
與目標(biāo)異常類BizException
最為匹配街佑,排在了集合中首位,RuntimeException
次匹配捍靠,排在了集合的第二位沐旨,Exception
最不匹配,排在集合的第三位榨婆。
public int compare(Class<? extends Throwable> o1, Class<? extends Throwable> o2) {
int depth1 = getDepth(o1, this.targetException, 0);
int depth2 = getDepth(o2, this.targetException, 0);
return (depth1 - depth2);
}
private int getDepth(Class<?> declaredException, Class<?> exceptionToMatch, int depth) {
if (exceptionToMatch.equals(declaredException)) {
// Found it!
return depth;
}
// If we've gone as far as we can go and haven't found it...
if (exceptionToMatch == Throwable.class) {
return Integer.MAX_VALUE;
}
return getDepth(declaredException, exceptionToMatch.getSuperclass(), depth + 1);
}
總結(jié):
Spring Boot應(yīng)用啟動(dòng)時(shí)磁携,會(huì)掃描被
@ControllerAdvice
注解的Bean,找到其內(nèi)部被@ExceptionHandler
注解的方法良风,解析其所能處理的異常類谊迄,并緩存到exceptionHandlerAdviceCache
當(dāng)HTTP請(qǐng)求在Controller中發(fā)生異常,會(huì)被DispatcherServlet捕獲烟央,并調(diào)用
ExceptionHandlerExceptionResolver#resolveException
進(jìn)行異常的解析统诺,解析的過程依賴exceptionHandlerAdviceCache
進(jìn)行真正的異常處理方法的查找,找到之后封裝成ServletInvocableHandlerMethod
疑俭,然后被Spring進(jìn)行調(diào)用粮呢,也即是會(huì)回調(diào)到我們的異常處理器的異常處理方法之中,即處理了異常钞艇。
注:
本文限于篇幅原因啄寡,不會(huì)面面俱到,只重點(diǎn)分析統(tǒng)一異常處理器的生效過程哩照,以及作用過程挺物,摘出其中重點(diǎn)的代碼進(jìn)行分析而忽略了其中的一些分支情況,讀者們可自行跟蹤代碼看看其中的細(xì)節(jié)處理葡秒。