原文轉載 :http://mp.weixin.qq.com/s?__biz=MjM5NzMyMjAwMA==&mid=2651478615&idx=2&sn=0f42544c5374527d7fd662d0af131e5e&chksm=bd2536288a52bf3e732b7fea504319bf906a4f8ae77c3f6ec0da9e0fefef96d4c9bee7714a2f&mpshare=1&scene=23&srcid=0609vqASUeqv3NsIAH7PlAhb#rd
Spring MVC中異常處理的類體系結構
下圖中,我畫出了Spring MVC中,跟異常處理相關的主要類和接口。
在Spring MVC中,所有用於處理在請求映射和請求處理過程中拋出的異常的類,都要實現HandlerExceptionResolver接口。AbstractHandlerExceptionResolver實現該接口和Orderd接口,是HandlerExceptionResolver類的實現的基類。ResponseStatusExceptionResolver等具體的異常處理類均在AbstractHandlerExceptionResolver之上,實現了具體的異常處理方式。一個基於Spring MVC的Web應用程序中,可以存在多個實現了HandlerExceptionResolver的異常處理類,他們的執行順序,由其order屬性決定, order值越小,越是優先執行, 在執行到第一個返回不是null的ModelAndView的Resolver時,不再執行後續的尚未執行的Resolver的異常處理方法。。
下面我逐個介紹一下SpringMVC提供的這些異常處理類的功能。
DefaultHandlerExceptionResolver
HandlerExceptionResolver接口的默認實現,基本上是Spring MVC內部使用,用來處理Spring定義的各種標準異常,將其轉化爲相對應的HTTP Status Code。其處理的異常類型有:
handleNoSuchRequestHandlingMethod
handleHttpRequestMethodNotSupported
handleHttpMediaTypeNotSupported
handleMissingServletRequestParameter
handleServletRequestBindingException
handleTypeMismatch
handleHttpMessageNotReadable
handleHttpMessageNotWritable
handleMethodArgumentNotValidException
handleMissingServletRequestParameter
handleMissingServletRequestPartException
handleBindException
ResponseStatusExceptionResolver
用來支持ResponseStatus的使用,處理使用了ResponseStatus註解的異常,根據註解的內容,返回相應的HTTP Status Code和內容給客戶端。如果Web應用程序中配置了ResponseStatusExceptionResolver,那麼我們就可以使用ResponseStatus註解來註解我們自己編寫的異常類,並在Controller中拋出該異常類,之後ResponseStatusExceptionResolver就會自動幫我們處理剩下的工作。
這是一個自己編寫的異常,用來表示訂單不存在:
@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的響應。
AnnotationMethodHandlerExceptionResolver和ExceptionHandlerExceptionResolver
用來支持ExceptionHandler註解,使用被ExceptionHandler註解所標記的方法來處理異常。其中AnnotationMethodHandlerExceptionResolver在3.0版本中開始提供,ExceptionHandlerExceptionResolver在3.1版本中開始提供,從3.2版本開始,Spring推薦使用ExceptionHandlerExceptionResolver。
如果配置了AnnotationMethodHandlerExceptionResolver和ExceptionHandlerExceptionResolver這兩個異常處理bean之一,那麼我們就可以使用ExceptionHandler註解來處理異常。
下面是幾個ExceptionHandler註解的使用例子:
@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
提供了將異常映射爲視圖的能力,高度可定製化。其提供的能力有:
-
根據異常的類型,將異常映射到視圖;
-
可以爲不符合處理條件沒有被處理的異常,指定一個默認的錯誤返回;
-
處理異常時,記錄log信息;
-
指定需要添加到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的。
-
Spring MVC有兩種加載異常處理類的方式,一種是根據類型,這種情況下,會加載ApplicationContext下所有實現了ExceptionResolver接口的bean,並根據其order屬性排序,依次調用;一種是根據名字,這種情況下會加載ApplicationContext下,名字爲handlerExceptionResolver的bean。
-
不管使用那種加載方式,如果在ApplicationContext中沒有找到異常處理bean,那麼Spring MVC會加載默認的異常處理bean。
-
默認的異常處理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的。
-
Spring MVC把請求映射和處理過程放到try catch中,捕獲到異常後,使用異常處理bean進行處理。
-
所有異常處理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提供了很多選擇和非常靈活的使用方式,下面是一些使用建議:
-
如果自定義異常類,考慮加上ResponseStatus註解;
-
對於沒有ResponseStatus註解的異常,可以通過使用ExceptionHandler+ControllerAdvice註解,或者通過配置SimpleMappingExceptionResolver,來爲整個Web應用提供統一的異常處理。
-
如果應用中有些異常處理方式,只針對特定的Controller使用,那麼在這個Controller中使用ExceptionHandler註解。
-
不要使用過多的異常處理方式,不然的話,維護起來會很苦惱,因爲異常的處理分散在很多不同的地方。