本文講講mvc的異常處理機制,方便查閱以及編寫合理的異常響應方式
入口例子
很簡單,根據之前的文章,我們只需要復寫WebMvcConfigurer接口的異常添加方法即可,如下
1.創建簡單的異常處理類,本例針對綁定異常
package com.example.demo.web.validation;
import com.example.demo.web.model.ResEntity;
import com.google.gson.Gson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author nanco
* -------------
* resolve bindexception
* -------------
* @create 18/9/9
*/
public class SimpleExceptionResolver extends AbstractHandlerExceptionResolver {
private static final Logger EXCEPTION_LOG = LoggerFactory.getLogger(SimpleExceptionResolver.class);
private final Map<String, List<String>> errorResultMap = new HashMap<>(2);
private final String ERROR_KEY = "error_result";
private Gson gson = new Gson();
@Override
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// only process BindException,unless return null to allow the next handler understanding the exception
if (BindException.class.isInstance(ex)) {
ResEntity resEntity = new ResEntity();
try {
BindException bindException = BindException.class.cast(ex);
List<ObjectError> allErrors = bindException.getAllErrors();
List<String> resMessages = new ArrayList<>(allErrors.size());
allErrors.stream().forEach(error -> {
resMessages.add(error.getDefaultMessage());
});
errorResultMap.put(ERROR_KEY, resMessages);
resEntity.setData(errorResultMap);
response.getOutputStream().write(gson.toJson(resEntity).getBytes());
} catch (IOException e) {
EXCEPTION_LOG.error("process BindException fail.", e);
}
return new ModelAndView();
}
return null;
}
}
2.實現WebMvcConfigurer接口后復寫其中的extendHandlerExceptionResolvers()方法
package com.example.demo.web.config;
import com.example.demo.web.validation.SimpleExceptionResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* @author nanco
* -------------
* color the mvc config
* -------------
* @create 2018/9/5
**/
@Configuration
public class BootWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
// response first
resolvers.add(0, new SimpleExceptionResolver());
}
}
上述簡單的代碼便會對系統拋出的BindException異常進行針對性的處理,從而返回合乎格式的響應體。當然這只是一小部分,筆者可以稍微從源碼的角度來分析下spring的異常機制
源碼層
查閱過DispatcherServlet源碼的都知道,當出現異常的時候,則會嘗試調用HandlerExceptionResolver解析器去根據異常進行視圖渲染或者直接返回對應的錯誤信息。筆者按步驟來進行簡單分析,從WebMvcConfigurationSupport入手
1.異常解析器注冊
@Bean
public HandlerExceptionResolver handlerExceptionResolver() {
List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
// 優先加載用戶自定義的異常解析器,也可通過WebMvcConfigurer來復寫
configureHandlerExceptionResolvers(exceptionResolvers);
// 當用戶沒有復寫上述方法后,采取默認的異常解析器
if (exceptionResolvers.isEmpty()) {
addDefaultHandlerExceptionResolvers(exceptionResolvers);
}
// 擴增異常解析器,可見上文中的例子
extendHandlerExceptionResolvers(exceptionResolvers);
HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
composite.setOrder(0);
composite.setExceptionResolvers(exceptionResolvers);
return composite;
}
2.直接看下spring內置的默認異常解析器吧,參考addDefaultHandlerExceptionResolvers()方法
protected final void addDefaultHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
// 1.異常的方法處理,跟@RequestMapping注解的方法調用類似
ExceptionHandlerExceptionResolver exceptionHandlerResolver = createExceptionHandlerExceptionResolver();
exceptionHandlerResolver.setContentNegotiationManager(mvcContentNegotiationManager());
exceptionHandlerResolver.setMessageConverters(getMessageConverters());
exceptionHandlerResolver.setCustomArgumentResolvers(getArgumentResolvers());
exceptionHandlerResolver.setCustomReturnValueHandlers(getReturnValueHandlers());
if (jackson2Present) {
exceptionHandlerResolver.setResponseBodyAdvice(
Collections.singletonList(new JsonViewResponseBodyAdvice()));
}
if (this.applicationContext != null) {
exceptionHandlerResolver.setApplicationContext(this.applicationContext);
}
exceptionHandlerResolver.afterPropertiesSet();
exceptionResolvers.add(exceptionHandlerResolver);
// 2.攜帶@ResponseStatus注解的解析器
ResponseStatusExceptionResolver responseStatusResolver = new ResponseStatusExceptionResolver();
responseStatusResolver.setMessageSource(this.applicationContext);
exceptionResolvers.add(responseStatusResolver);
// 3.默認的異常解析器,針對spring的內置異常作下簡單的response
exceptionResolvers.add(new DefaultHandlerExceptionResolver());
}
筆者主要關注ExceptionHandlerExceptionResolver和ResponseStatusExceptionResolver解析器,那就分塊來簡單的講解把
ExceptionHandlerExceptionResolver
初始化狀態的代碼就不羅列了,讀者直接閱讀源碼就知道,筆者此處作下初始化的總結
尋找所有的攜帶
@ControllerAdvice
注解的bean,包裝成ExceptionHandlerMethodResolver方法解析器,由此來從中挑選出攜帶@ExceptionHandler
注解的方法集合對第一條中所得的方法集合,讀取其中
@ExceptionHandler
注解的值(Throwable實現類);無則讀取對應方法實現了Throwable異常接口的參數集合。即得出exceptionTypes集合對上述的exceptionTypes集合依次與對應的method形成映射,即方便針對指定的異常可以調用相應的方法來返回結果
對上述滿足條件的ControllerAdvice ,結合ExceptionHandlerMethodResolver裝入exceptionHandlerAdviceCache屬性map中
封裝參數解析器集合與返回值解析器集合,和處理
@RequestMapping
的操作一樣
具體的解析過程,筆者此處點一下,方便與上文對照着看,直接看關鍵的getExceptionHandlerMethod()方法
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
@Nullable HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = null;
if (handlerMethod != null) {
// 獲取出現異常類方法的所在類
handlerType = handlerMethod.getBeanType();
// 優先判斷如果此類直接返回的是異常類,則嘗試尋找解析器
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
if (resolver == null) {
// 查找異常所在類是否有符合的@ExceptionHandler語法方法
resolver = new ExceptionHandlerMethodResolver(handlerType);
this.exceptionHandlerCache.put(handlerType, resolver);
}
// 得到映射的方法
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
}
// For advice applicability check below (involving base packages, assignable types
// and annotation presence), use target class instead of interface-based proxy.
if (Proxy.isProxyClass(handlerType)) {
handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
}
}
// 進入@ControlleAdvice的語法環境了,判斷拋異常的所在類,ControllerAdvice是否支持
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
ControllerAdviceBean advice = entry.getKey();
// 如果@ControllerAdvice注解無任何的屬性配置,則默認是支持的
if (advice.isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = entry.getValue();
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
}
}
}
return null;
}
最終就是根據Exception的類型找尋符合條件的method,然后按照@RequestMapping
注解的處理方式得到相應的視圖對象供視圖解析器去渲染
ResponseStatusExceptionResolver
針對攜帶@ResponseStatus
注解的異常類來返回響應體的,簡單的看下代碼吧
@Override
@Nullable
protected ModelAndView doResolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
try {
// 直接返回的是ResponseStatusException類型的異常則直接處理
if (ex instanceof ResponseStatusException) {
return resolveResponseStatusException((ResponseStatusException) ex, request, response, handler);
}
// 讀取異常類上攜帶的@ResponseStatus注解,有則返回結果
ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
if (status != null) {
return resolveResponseStatus(status, request, response, handler, ex);
}
// 遞歸調用下
if (ex.getCause() instanceof Exception) {
ex = (Exception) ex.getCause();
return doResolveException(request, response, handler, ex);
}
}
catch (Exception resolveEx) {
logger.warn("ResponseStatus handling resulted in exception", resolveEx);
}
// 無符合條件的,直接返回null,調用下一個異常解析器
return null;
}
最終調用的也就是HttpServletResponse#sendError(int statusCode,String reason)方法直接返回結果
DispatcherServlet異常處理邏輯
此處還是貼下重要的代碼片段,加深印象,直接查閱processHandlerException()方法
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
....
if (this.handlerExceptionResolvers != null) {
// 對異常解析器集合進行遍歷
for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
// ModelAndView對象不為null則直接跳出,否則采取下一個異常解析器
if (exMv != null) {
break;
}
}
}
}
....
溫馨提示:
- 根據上述代碼的邏輯可見,用戶在自定義相應的異常解析器時,需要注意如果滿足解析指定的異常,則最后返回不為null的視圖對象(return new ModelAndView()),以免其跑至下一個異常解析器,影響服務執行結果。
- 遍歷的異常解析器順序此處提一下,其采取的是簡單的ArrayList集合來保持順序,所以用戶如果想自己的異常解析器保持較高的優先級,則可以采取List接口的add(int index, T value)方法添加或者直接實現HandlerExceptionResolver並設置order屬性來保持即可
結語
了解異常解析器的加載機制以及運行邏輯,方便我們寫出合乎spring邏輯的代碼,以此保證代碼的整潔性。