Servlet傳統異常處理
Servlet規范規定了當web應用發生異常時必須能夠指明, 並確定了該如何處理, 規定了錯誤信息應該包含的內容和展示頁面的方式.(詳細可以參考servlet規范文檔)
處理方式
- 處理狀態碼
<error-code>
- 處理異常信息
<exception-type>
- 處理服務地址
<location>
Spring MVC 處理方式
所有的請求必然以某種方式轉化為響應.
- Spring中特定的異常將自動映射為特定的HTTP狀態碼
- 使用
@ResponseStatus
注解可以映射某一異常到特定的HTTP狀態碼 - Controller方法上可以使用
@ExceptionHandler
注解使其用來處理異常 - 使用
@ControllerAdvice
方式可以統一的方式處理全局異常
Spring boot 方式
- 實現ErrorPageRegistrar: 確定是頁面處理的路徑必須固定,優點是比較通用
- 注冊ErrorPage
- 實現ErrorPage對應的服務
源碼分析
一.接口HandlerExceptionResolver
該接口定義了Spring中該如何處理異常. 它只有一個方法resolveException()
, 接口源碼如下:
// 由對象實現的接口,這些對象可以解決在處理程序映射或執行期間引發的異常,在典型的情況下是錯誤視圖。在應用程序上下文中,實現器通常被注冊為bean。
// 錯誤視圖類似於JSP錯誤頁面,但是可以與任何類型的異常一起使用,包括任何已檢查的異常,以及針對特定處理程序的潛在細粒度映射。
public interface HandlerExceptionResolver {
@Nullable
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
Spring 為該接口提供了若干實現類如下:
HandlerExceptionResolverComposite 委托給其他HandlerExceptionResolver的實例列表 AbstractHandlerExceptionResolver 抽象基類 AbstractHandlerMethodExceptionResolver 支持HandlerMethod處理器的抽象基類 ExceptionHandlerExceptionResolver 通過 @ExceptionHandler 注解的方式實現的異常處理 DefaultHandlerExceptionResolver 默認實現, 處理spring預定義的異常並將其對應到錯誤碼 ResponseStatusExceptionResolver 通過 @ResponseStatus 注解映射到錯誤碼的異常 SimpleMappingExceptionResolver 允許將異常類映射到視圖名
二. DefaultHandlerExceptionResolver
這個類是Spring提供的默認實現, 用於將一些常見異常映射到特定的狀態碼. 這些狀態碼定義在接口HttpServletResponse
中, 下面是幾個狀態碼的代碼片段
public interface HttpServletResponse extends ServletResponse {
...
public static final int SC_OK = 200;
public static final int SC_MOVED_PERMANENTLY = 301;
public static final int SC_MOVED_TEMPORARILY = 302;
public static final int SC_FOUND = 302;
public static final int SC_UNAUTHORIZED = 401;
public static final int SC_INTERNAL_SERVER_ERROR = 500;
...
}
實際上, DefaultHandlerExceptionResolver
中並沒有直接實現接口的resolveException
方法, 而是實現了抽象類AbstractHandlerExceptionResolver
的doResolveException()
方法, 后者則在實現了接口的方法中委托給抽象方法doResolveException
, 這個方法由子類去實現.
AbstractHandlerExceptionResolver
的resolveException
方法代碼如下:
@Override
@Nullable
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
// 判斷是否當前解析器可用於handler
if (shouldApplyTo(request, handler)) {
prepareResponse(ex, response);
ModelAndView result = doResolveException(request, response, handler, ex);
if (result != null) {
// Print warn message when warn logger is not enabled...
if (logger.isWarnEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
logger.warn("Resolved [" + ex + "]" + (result.isEmpty() ? "" : " to " + result));
}
// warnLogger with full stack trace (requires explicit config)
logException(ex, request);
}
return result;
}
else {
return null;
}
}
接下來我們看DefaultHandlerExceptionResolver
實現的doResolveException
方法. 代碼如下;
@Override
@Nullable
protected ModelAndView doResolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
try {
if (ex instanceof HttpRequestMethodNotSupportedException) {
return handleHttpRequestMethodNotSupported(
(HttpRequestMethodNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof HttpMediaTypeNotSupportedException) {
return handleHttpMediaTypeNotSupported(
(HttpMediaTypeNotSupportedException) ex, request, response, handler);
}
....
else if (ex instanceof NoHandlerFoundException) {
return handleNoHandlerFoundException(
(NoHandlerFoundException) ex, request, response, handler);
}
.....
}
catch (Exception handlerEx) {
if (logger.isWarnEnabled()) {
logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx);
}
}
return null;
}
可以看到代碼中使用了大量的分支語句, 實際上是將方法傳入的異常類型通過instanceof運算符測試, 通過測試的轉化為特定的異常. 並調用處理該異常的特定方法. 我們挑一個比如處理NoHandlerFoundException
這個異常類的方法, 這個方法將異常映射為404錯誤.
protected ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex,
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
pageNotFoundLogger.warn(ex.getMessage());
response.sendError(HttpServletResponse.SC_NOT_FOUND); //設置為404錯誤
return new ModelAndView(); //返回個空視圖
}
上面分析了Spring默認的異常處理實現類DefaultHandlerExceptionResolver
.它處理的異常是Spring預定義的幾種常見異常, 它將異常對應到HTTP的狀態碼. 而對於不屬於這些類型的其他異常, 我們可以使用ResponseStatusExceptionResolver
來處理, 將其對應到HTTP狀態碼.
三. ResponseStatusExceptionResolver
如何使用?
@GetMapping("/responseStatus")
@ResponseBody
public String responseStatus() throws MyException {
throw new MyException();
}
@ResponseStatus(code = HttpStatus.BAD_GATEWAY)
public class MyException extends Exception{}
只需要在異常上使用@ResponseStatus
注解即可將特定的自定義異常對應到Http的狀態碼.
四. ExceptionHandlerExceptionResolver
使用類似於普通的controller方法, 使用@ExceptionHandler
注解的方法將作為處理該注解參數中異常的handler. 比如, 在一個controller中, 我們定義一個處理NPE的異常處理handler方法, 可以用來處理該controller中拋出的NPE. 代碼如下:
@GetMapping("/npe1")
@ResponseBody
public String npe1() throws NullPointerException {
throw new NullPointerException();
}
@GetMapping("/npe2")
@ResponseBody
public String npe2() throws NullPointerException {
throw new NullPointerException();
}
@ExceptionHandler(value = {NullPointerException.class})
@ResponseBody
public String npehandler(){
return "test npe handler";
}
無論是請求/npe1還是請求/npe2, 系統都會拋出異常, 並交給對應的處理程序npehandler
去處理. 使用@ExceptionHandler(value = {NullPointerException.class})
注解的方法可以處理本controller范圍內的所有方法排除的npe異常, 如果要將其作為應用中所有controller的異常處理器, 就要將其定義在@ControllerAdvice
注解的類中.
@ControllerAdvice
public class ControllerAdvicer {
@ExceptionHandler(value = {NullPointerException.class})
@ResponseBody
public String npehandler(){
return "test npe handler in advice";
}
}
要了解其原理, 需要查看ExceptionHandlerExceptionResolver
中的方法doResolveHandlerMethodException
@Override
@Nullable
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
// 獲取異常對用的處理器, 就是@ExceptionHandler注解的方法包裝, 注意參數handlerMethod, 在方法內部, 它將用來獲取所在Controller的信息
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
if (exceptionHandlerMethod == null) {
return null;
}
if (this.argumentResolvers != null) {
exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
ServletWebRequest webRequest = new ServletWebRequest(request, response);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
try {
if (logger.isDebugEnabled()) {
logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod);
}
Throwable cause = exception.getCause();
// 調用異常處理handler的方法.
if (cause != null) {
// Expose cause as provided argument as well
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod);
}
else {
// Otherwise, just the given exception as-is
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
}
}
catch (Throwable invocationEx) {
// Any other than the original exception is unintended here,
// probably an accident (e.g. failed assertion or the like).
if (invocationEx != exception && logger.isWarnEnabled()) {
logger.warn("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx);
}
// Continue with default processing of the original exception...
return null;
}
if (mavContainer.isRequestHandled()) {
return new ModelAndView();
}
else {
ModelMap model = mavContainer.getModel();
HttpStatus status = mavContainer.getStatus();
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status);
mav.setViewName(mavContainer.getViewName());
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
if (model instanceof RedirectAttributes) {
Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
return mav;
}
}
可以看到在兩個中文注釋的地方, 其一是方法的開始部分獲取到了異常的handler, 其二是調用這個handler的方法. 調用方法應該很好理解, 我們接下來查看方法getExceptionHandlerMethod
.
// 找到給定異常對應的@ExceptionHandler注解方法, 默認先在controller類的繼承結構中查找, 否則繼續在@ControllerAdvice注解的 bean中查找.
@Nullable
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
@Nullable HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = null;
if (handlerMethod != null) {
// Local exception handler methods on the controller class itself.
// To be invoked through the proxy, even in case of an interface-based proxy.
handlerType = handlerMethod.getBeanType();
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
if (resolver == null) {
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());
}
}
// 在@ControllerAdvice注解的類中遍歷查找
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
ControllerAdviceBean advice = entry.getKey();
if (advice.isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = entry.getValue();
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
}
}
}
return null;
}
我們可以看到,它會首先查找controller中的方法, 如果找不到才去查找@ControllerAdvice注解的bean. 也就是說controller中的handler的優先級要高於advice.
上面我們了解了幾個Exceptionresolver的使用, 並通過源代碼簡單看了他們各自處理的原理. 但這些Resolver如何加載我們還不知道, 接下來我們重點看下他們是如何加載進去的.
四. ExceptionResolver的加載
在本系列的上一篇Spring系列(六) Spring Web MVC 應用構建分析中, 我們大致提到了DispatcherServlet的啟動調用關系如下:
整理下調用關系: DispatcherServlet
initHandlerMappings <-- initStrategies <-- onRefresh <--
FrameworkServlet
initWebApplicationContext <-- initServletBean <--
HttpServletBean
init <--
GenericServlet
init(ServletConfig config)
最后的GenericServlet
是servlet Api的.
正是在initStrategies
方法中, DispatcherServlet
做了啟動的一系列工作, 除了initHandlerMappings
還可以看到一個initHandlerExceptionResolvers
的方法, 其源碼如下:
// 初始化HandlerExceptionResolver, 如果沒有找到任何命名空間中定義的bean, 默認沒有任何resolver
private void initHandlerExceptionResolvers(ApplicationContext context) {
this.handlerExceptionResolvers = null;
if (this.detectAllHandlerExceptionResolvers) {
// 找到所有ApplicationContext中定義的 HandlerExceptionResolvers 包括在上級上下文中.
Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
// 保持有序.
AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
}
}
else {
try {
HandlerExceptionResolver her =
context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
this.handlerExceptionResolvers = Collections.singletonList(her);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, no HandlerExceptionResolver is fine too.
}
}
// 確保有Resolver, 否則使用默認的
if (this.handlerExceptionResolvers == null) {
this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerExceptionResolvers declared in servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
好了, 現在我們加載了應用程序中所有定義的Resolver. 當有請求到達時, DispatcherServlet
的doDispatch
方法使用請求特定的handler處理, 當handler發生異常時, 變量dispatchException
的值賦值為拋出的異常, 並委托給方法processDispatchResult
doDispatch的代碼, 只摘錄出與本議題有關的.
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
....
try {
ModelAndView mv = null;
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
}catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
....
}
// 處理handler的結果
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
// 異常處理
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);
}
}
// handler是否返回了view
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
從processDispatchResult
方法中可以看到, 如果參數exception
不為null, 則會處理異常, 對於ModelAndViewDefiningException
類型的異常單獨處理, 對於其他類型的異常, 轉交給processHandlerException
方法處理, 這個方法就是異常處理邏輯的核心.
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// Success and error responses may use different content types
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
// 使用注冊的Resolver處理
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
// We might still need view name translation for a plain error model...
if (!exMv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
exMv.setViewName(defaultViewName);
}
}
if (logger.isTraceEnabled()) {
logger.trace("Using resolved error view: " + exMv, ex);
}
if (logger.isDebugEnabled()) {
logger.debug("Using resolved error view: " + exMv);
}
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
}
throw ex;
}
從上面代碼可以看到, this.handlerExceptionResolvers
就是在程序啟動時初始化注冊的, spring通過遍歷Resolver列表的方式處理異常, 如果返回結果不為null, 說明處理成功, 就跳出循環.
總結
Spring的異常解析器實現全部繼承自接口ResponseStatusExceptionResolver
, 上面我們詳細了解了該接口在Spring中的幾種實現, 比如處理預定義異常的DefaultHandlerExceptionResolver
, 可以映射異常到狀態碼的ResponseStatusExceptionResolver
, 還有功能更為強大的ExceptionHandlerExceptionResolver
. 同時也簡單了解了其使用方式,使用@ExceptionHandler
來將方法標記為異常處理器, 結合@ControllerAdvice
處理全局異常.
最后我們探究了異常處理器的加載和處理方式, 我們知道了其通過 DispatcherServlet
的初始化方法initHandlerMappings
完成加載器列表的注冊初始化, 並且在具體處理請求的doDispatch
中檢測異常, 最終processDispatchResult
方法委托給processHandlerException
, 該方法循環注冊的異常處理器列表完成處理過程.