Spring MVC異常處理詳解

Spring MVC中異常處理的類體系結構

下圖中,我畫出了Spring MVC中,跟異常處理相關的主要類和接口。java

SpringMVCExceptionResolver

在Spring MVC中,全部用於處理在請求映射和請求處理過程當中拋出的異常的類,都要實現HandlerExceptionResolver接口。AbstractHandlerExceptionResolver實現該接口和Orderd接口,是HandlerExceptionResolver類的實現的基類。ResponseStatusExceptionResolver等具體的異常處理類均在AbstractHandlerExceptionResolver之上,實現了具體的異常處理方式。一個基於Spring MVC的Web應用程序中,能夠存在多個實現了HandlerExceptionResolver的異常處理類,他們的執行順序,由其order屬性決定, order值越小,越是優先執行, 在執行到第一個返回不是null的ModelAndView的Resolver時,再也不執行後續的還沒有執行的Resolver的異常處理方法。。web

下面我逐個介紹一下SpringMVC提供的這些異常處理類的功能。spring

DefaultHandlerExceptionResolver

HandlerExceptionResolver接口的默認實現,基本上是Spring MVC內部使用,用來處理Spring定義的各類標準異常,將其轉化爲相對應的HTTP Status Code。其處理的異常類型有:spring-mvc

handleNoSuchRequestHandlingMethod
handleHttpRequestMethodNotSupported
handleHttpMediaTypeNotSupported
handleMissingServletRequestParameter
handleServletRequestBindingException
handleTypeMismatch
handleHttpMessageNotReadable
handleHttpMessageNotWritable
handleMethodArgumentNotValidException
handleMissingServletRequestParameter
handleMissingServletRequestPartException
handleBindException

ResponseStatusExceptionResolver

用來支持ResponseStatus的使用,處理使用了ResponseStatus註解的異常,根據註解的內容,返回相應的HTTP Status Code和內容給客戶端。若是Web應用程序中配置了ResponseStatusExceptionResolver,那麼咱們就可使用ResponseStatus註解來註解咱們本身編寫的異常類,並在Controller中拋出該異常類,以後ResponseStatusExceptionResolver就會自動幫咱們處理剩下的工做。mvc

這是一個本身編寫的異常,用來表示訂單不存在:app

@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such Order")  // 404
    public class OrderNotFoundException extends RuntimeException {
        // ...
    }

這是一個使用該異常的Controller方法:框架

@RequestMapping(value="/orders/{id}", method=GET)
    public String showOrder(@PathVariable("id") long id, Model model) {
        Order order = orderRepository.findOrderById(id);
        if (order == null) throw new OrderNotFoundException(id);
        model.addAttribute(order);
        return "orderDetail";
    }

這樣,當OrderNotFoundException被拋出時,ResponseStatusExceptionResolver會返回給客戶端一個HTTP Status Code爲404的響應。async

AnnotationMethodHandlerExceptionResolver和ExceptionHandlerExceptionResolver

用來支持ExceptionHandler註解,使用被ExceptionHandler註解所標記的方法來處理異常。其中AnnotationMethodHandlerExceptionResolver在3.0版本中開始提供,ExceptionHandlerExceptionResolver在3.1版本中開始提供,從3.2版本開始,Spring推薦使用ExceptionHandlerExceptionResolver。
若是配置了AnnotationMethodHandlerExceptionResolver和ExceptionHandlerExceptionResolver這兩個異常處理bean之一,那麼咱們就可使用ExceptionHandler註解來處理異常。ide

下面是幾個ExceptionHandler註解的使用例子:post

@Controller
public class ExceptionHandlingController {

  // @RequestHandler methods
  ...
  
  // 如下是異常處理方法
  
  // 將DataIntegrityViolationException轉化爲Http Status Code爲409的響應
  @ResponseStatus(value=HttpStatus.CONFLICT, reason="Data integrity violation")  // 409
  @ExceptionHandler(DataIntegrityViolationException.class)
  public void conflict() {
    // Nothing to do
  }
  
  // 針對SQLException和DataAccessException返回視圖databaseError
  @ExceptionHandler({SQLException.class,DataAccessException.class})
  public String databaseError() {
    // Nothing to do.  Returns the logical view name of an error page, passed to
    // the view-resolver(s) in usual way.
    // Note that the exception is _not_ available to this view (it is not added to
    // the model) but see "Extending ExceptionHandlerExceptionResolver" below.
    return "databaseError";
  }

  // 建立ModleAndView,將異常和請求的信息放入到Model中,指定視圖名字,並返回該ModleAndView
  @ExceptionHandler(Exception.class)
  public ModelAndView handleError(HttpServletRequest req, Exception exception) {
    logger.error("Request: " + req.getRequestURL() + " raised " + exception);

    ModelAndView mav = new ModelAndView();
    mav.addObject("exception", exception);
    mav.addObject("url", req.getRequestURL());
    mav.setViewName("error");
    return mav;
  }
}

須要注意的是,上面例子中的ExceptionHandler方法的做用域,只是在本Controller類中。若是須要使用ExceptionHandler來處理全局的Exception,則須要使用ControllerAdvice註解。

@ControllerAdvice
class GlobalDefaultExceptionHandler {
    public static final String DEFAULT_ERROR_VIEW = "error";

    @ExceptionHandler(value = Exception.class)
    public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
        // 若是異常使用了ResponseStatus註解,那麼從新拋出該異常,Spring框架會處理該異常。 
        if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null)
            throw e;

        // 不然建立ModleAndView,處理該異常。
        ModelAndView mav = new ModelAndView();
        mav.addObject("exception", e);
        mav.addObject("url", req.getRequestURL());
        mav.setViewName(DEFAULT_ERROR_VIEW);
        return mav;
    }
}

SimpleMappingExceptionResolver

提供了將異常映射爲視圖的能力,高度可定製化。其提供的能力有:

  1. 根據異常的類型,將異常映射到視圖;
  2. 能夠爲不符合處理條件沒有被處理的異常,指定一個默認的錯誤返回;
  3. 處理異常時,記錄log信息;
  4. 指定須要添加到Modle中的Exception屬性,從而在視圖中展現該屬性。
@Configuration
@EnableWebMvc 
public class MvcConfiguration extends WebMvcConfigurerAdapter {
    @Bean(name="simpleMappingExceptionResolver")
    public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();

        Properties mappings = new Properties();
        mappings.setProperty("DatabaseException", "databaseError");
        mappings.setProperty("InvalidCreditCardException", "creditCardError");

        r.setExceptionMappings(mappings);  // 默認爲空
        r.setDefaultErrorView("error");    // 默認沒有
        r.setExceptionAttribute("ex"); 
        r.setWarnLogCategory("example.MvcLogger"); 
        return r;
    }
    ...
}

自定義ExceptionResolver

Spring MVC的異常處理很是的靈活,若是提供的ExceptionResolver類不能知足使用,咱們能夠實現本身的異常處理類。能夠經過繼承SimpleMappingExceptionResolver來定製Mapping的方式和能力,也能夠直接繼承AbstractHandlerExceptionResolver來實現其它類型的異常處理類。

Spring MVC是如何建立和使用這些Resolver的?

首先看Spring MVC是怎麼加載異常處理bean的。

  1. Spring MVC有兩種加載異常處理類的方式,一種是根據類型,這種狀況下,會加載ApplicationContext下全部實現了ExceptionResolver接口的bean,並根據其order屬性排序,依次調用;一種是根據名字,這種狀況下會加載ApplicationContext下,名字爲handlerExceptionResolver的bean。
  2. 無論使用那種加載方式,若是在ApplicationContext中沒有找到異常處理bean,那麼Spring MVC會加載默認的異常處理bean。
  3. 默認的異常處理bean定義在DispatcherServlet.properties中。
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\
    org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
    org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

如下代碼摘自ispatcherServlet,描述了異常處理類的加載過程:

/**
 * Initialize the HandlerMappings used by this class.
 * <p>If no HandlerMapping beans are defined in the BeanFactory for this namespace,
 * we default to BeanNameUrlHandlerMapping.
 */
private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;

    if (this.detectAllHandlerMappings) {
        // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
        Map<String, HandlerMapping> matchingBeans =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values());
            // We keep HandlerMappings in sorted order.
            OrderComparator.sort(this.handlerMappings);
        }
    }
    else {
        try {
            HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
            this.handlerMappings = Collections.singletonList(hm);
        }
        catch (NoSuchBeanDefinitionException ex) {
            // Ignore, we'll add a default HandlerMapping later.
        }
    }

    // Ensure we have at least one HandlerMapping, by registering
    // a default HandlerMapping if no other mappings are found.
    if (this.handlerMappings == null) {
        this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
        if (logger.isDebugEnabled()) {
            logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default");
        }
    }
}

而後看Spring MVC是怎麼使用異常處理bean的。

  1. Spring MVC把請求映射和處理過程放到try catch中,捕獲到異常後,使用異常處理bean進行處理。
  2. 全部異常處理bean按照order屬性排序,在處理過程當中,遇到第一個成功處理異常的異常處理bean以後,再也不調用後續的異常處理bean。

如下代碼摘自DispatcherServlet,描述了處理異常的過程。

/**
 * Process the actual dispatching to the handler.
 * <p>The handler will be obtained by applying the servlet's HandlerMappings in order.
 * The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
 * to find the first that supports the handler class.
 * <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers
 * themselves to decide which methods are acceptable.
 * @param request current HTTP request
 * @param response current HTTP response
 * @throws Exception in case of any kind of processing failure
 */
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // Determine handler for the current request.
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null || mappedHandler.getHandler() == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // Determine handler adapter for the current request.
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            // Process last-modified header, if supported by the handler.
            String method = request.getMethod();
            boolean isGet = "GET".equals(method);
            if (isGet || "HEAD".equals(method)) {
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (logger.isDebugEnabled()) {
                    logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                }
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return;
                }
            }

            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            // Actually invoke the handler.
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }

            applyDefaultViewName(request, mv);
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
    catch (Error err) {
        triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err);
    }
    finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            // Instead of postHandle and afterCompletion
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        }
        else {
            // Clean up any resources used by a multipart request.
            if (multipartRequestParsed) {
                cleanupMultipart(processedRequest);
            }
        }
    }
}


/**
 * Determine an error ModelAndView via the registered HandlerExceptionResolvers.
 * @param request current HTTP request
 * @param response current HTTP response
 * @param handler the executed handler, or {@code null} if none chosen at the time of the exception
 * (for example, if multipart resolution failed)
 * @param ex the exception that got thrown during handler execution
 * @return a corresponding ModelAndView to forward to
 * @throws Exception if no error ModelAndView found
 */
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
        Object handler, Exception ex) throws Exception {

    // Check registered HandlerExceptionResolvers...
    ModelAndView exMv = null;
    for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
        exMv = handlerExceptionResolver.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()) {
            exMv.setViewName(getDefaultViewName(request));
        }
        if (logger.isDebugEnabled()) {
            logger.debug("Handler execution resulted in exception - forwarding to resolved error view: " + exMv, ex);
        }
        WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
        return exMv;
    }

    throw ex;
}

什麼時候該使用何種ExceptionResolver?

Spring提供了不少選擇和很是靈活的使用方式,下面是一些使用建議:

  1. 若是自定義異常類,考慮加上ResponseStatus註解;
  2. 對於沒有ResponseStatus註解的異常,能夠經過使用ExceptionHandler+ControllerAdvice註解,或者經過配置SimpleMappingExceptionResolver,來爲整個Web應用提供統一的異常處理。
  3. 若是應用中有些異常處理方式,只針對特定的Controller使用,那麼在這個Controller中使用ExceptionHandler註解。
  4. 不要使用過多的異常處理方式,否則的話,維護起來會很苦惱,由於異常的處理分散在不少不一樣的地方。
相關文章
相關標籤/搜索