Spring MVC的异常处理(译)

原文链接

使用Http状态码

通常,处理Web请求时引发的任何未处理的异常都会导致服务器返回HTTP 500响应。但是,您自己编写的任何异常都可以使用@ResponseStatus注解(它支持HTTP规范定义的所有HTTP状态代码)。当从一个控制器方法抛出一个带注解的异常,而不在其他地方处理时,
它会自动导致相应的HTTP响应返回指定的状态码。
例如,这里有一个订单不存在的异常。

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

这里是抛出它的控制器方法:

 @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";
 }

如果此方法处理的URL包含未知的订单ID,则会返回HTTP 404响应。

基于Controller的异常处理

使用@ExceptionHandler

你能将@ExceptionHandler添加到任何控制器来处理由同一控制器中的请求处理@RequestMapping抛出的异常。
这些方法可以:
1.处理没有@ResponseStatus注解的异常(通常是你没有写的预定义异常)
2.将用户重定向到专用的error视图
3.构建一个完全自定义的error响应

下面的控制器演示了这三个选项:

@Controller
public class ExceptionHandlingController {

  // @RequestHandler methods
  ...
  
  // Exception handling methods
  
  // Convert a predefined exception to an HTTP Status code
  @ResponseStatus(value=HttpStatus.CONFLICT,
                  reason="Data integrity violation")  // 409
  @ExceptionHandler(DataIntegrityViolationException.class)
  public void conflict() {
    // Nothing to do
  }
  
  // Specify name of a specific view that will be used to display the error:
  @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";
  }

  // Total control - setup a model and return the view name yourself. Or
  // consider subclassing ExceptionHandlerExceptionResolver (see below).
  @ExceptionHandler(Exception.class)
  public ModelAndView handleError(HttpServletRequest req, Exception ex) {
    logger.error("Request: " + req.getRequestURL() + " raised " + ex);

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

在这些方法中,您都可以选择进行额外的处理-最常见的例子是打印异常到日志文件。
你可以传入servlet相关的对象到异常处理的方法,例如HttpServletRequestHttpServletResponseHttpSession Principle

重要提示:Model可能不是任意@ExceptionHandler方法的参数。相反,使用ModelAndView在方法内部设置模型,如上面的handleError()所示。

异常和视图

向模型添加异常时要小心。用户不希望看到包含Java异常详细信息和堆栈跟踪的Web页面。但是,将页面中的异常详细信息作为注释来发布可能很有用。如果使用JSP,你可以做这样来输出异常和相应的堆栈跟踪(使用隐藏的<div>是另一种方法)。

<h1>Error Page</h1>
<p>Application has encountered an error. Please contact support on ...</p>
    
<!--
  Failed URL: ${url}
  Exception:  ${exception.message}
      <c:forEach items="${exception.stackTrace}" var="ste">    ${ste} 
  </c:forEach>
-->

使用Thymeleaf模板可以参照demo中的support.html页面 。结果看起来像这样。

support-page-example.png

全局异常处理

使用@ControllerAdvice

@ControllerAdvice允许你在整个应该用程序中使用相同的异常处理技术,而不仅仅是一个单独的控制器。你可以将它们看成注解驱动的拦截器。
任何使用@ControllerAdvice注解的类都将成为controller-advice,并支持三种类型的方法:

  • 用@ExceptionHandler注释的异常处理方法。
  • 使用@ModelAttribute注解的模型增强方法(用于向模型添加其他数据)。请注意,这些属性不适用于异常处理视图。
  • 使用@InitBinder注解的绑定初始化方法(用于配置表单处理)。

上面看到的任何异常处理程序都可以在 controller-advice类中定义-但现在它们适用于从任何控制器抛出的异常。这有一个简单的例子:

@ControllerAdvice
class GlobalControllerExceptionHandler {
    @ResponseStatus(HttpStatus.CONFLICT)  // 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void handleConflict() {
        // Nothing to do
    }
}

如果你想有处理任何异常的默认方法。你需要确保被注解的异常都能被这个框架处理。代码看起来像这样:

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

  @ExceptionHandler(value = Exception.class)
  public ModelAndView
  defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
    // If the exception is annotated with @ResponseStatus rethrow it and let
    // the framework handle it - like the OrderNotFoundException example
    // at the start of this post.
    // AnnotationUtils is a Spring Framework utility class.
    if (AnnotationUtils.findAnnotation
                (e.getClass(), ResponseStatus.class) != null)
      throw e;

    // Otherwise setup and send the user to a default error-view.
    ModelAndView mav = new ModelAndView();
    mav.addObject("exception", e);
    mav.addObject("url", req.getRequestURL());
    mav.setViewName(DEFAULT_ERROR_VIEW);
    return mav;
  }
}

深入

HandlerExceptionResolver

在DispatcherServlet的应用程序上下文中声明的任何实现HandlerExceptionResolver的Spring bean将被用于截获和处理在MVC系统中引发并且不由Controller处理的任何异常。接口看起来像这样:

public interface HandlerExceptionResolver {
    ModelAndView resolveException(HttpServletRequest request, 
            HttpServletResponse response, Object handler, Exception ex);
}

handler引用了产生异常的controller(请记住,@Controller实例只是Spring MVC支持的一种类型的处理程序。例如:HttpInvokerExporter和WebFlow执行程序也是类型的处理程序)。
在幕后,MVC默认创建三个这样的解析器。正是这些解析器实现了上面讨论的行为。

  • ExceptionHandlerExceptionResolver匹配未被@ExceptionHandler注解捕获的异常
  • ResponseStatusExceptionResolver查找由@ResponseStatus注解的未捕获的异常(如第1节中所述)
  • DefaultHandlerExceptionResolver转换标准的Spring异常并转换它们 到HTTP状态代码(我没有提到过,因为它在Spring MVC的内部)

它们被串联起来,在 顺序排列的列表中进行处理(Spring内部创建一个专用的HandlerExceptionResolverComposite来做这件事)。
请注意,resolveException的方法签名不包含模型。这就是为什么@ExceptionHandler方法不能与模型一起注入的原因。
如果你愿意,你可以实现自己的HandlerExceptionResolver来设置你自己的自定义异常处理系统。处理程序通常实现Spring的Ordered接口,因此你可以定义处理程序运行的顺序。

SimpleMappingExceptionResolver

Spring很早就提供了一个简单但方便的HandlerExceptionResolver实现,你可能已经发现它已经在你的应用程序中被使用 - SimpleMappingExceptionResolver。它提供了以下选项:

  • 将异常类名映射到视图名称 - 只需指定类名,不需要包。
  • 为任何其他地方未处理的异常指定默认(fallback)错误页面。
  • 打印日志(默认情况不启用)。
  • 设置要添加到模型的异常属性的名称,所以它可以在View中使用 (如JSP)。默认情况下,该属性被命名为exception。设置为null以禁用。请记住,从@ExceptionHandler方法返回的视图无法访问该异常,但定义到SimpleMappingExceptionResolver的视图可以执行此操作。
    这是使用XML的典型配置:
<bean id="simpleMappingExceptionResolver" class=
     "org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
  <property name="exceptionMappings">
    <map>
       <entry key="DatabaseException" value="databaseError"/>
       <entry key="InvalidCreditCardException" value="creditCardError"/>
    </map>
  </property>

  <!-- See note below on how this interacts with Spring Boot -->
  <property name="defaultErrorView" value="error"/>
  <property name="exceptionAttribute" value="ex"/>
        
  <!-- Name of logger to use to log exceptions. Unset by default, 
         so logging is disabled unless you set a value. -->
  <property name="warnLogCategory" value="example.MvcLogger"/>
</bean>

或者使用Java配置:

@Configuration
@EnableWebMvc  // Optionally setup Spring MVC defaults (if you aren't using
               // Spring Boot & haven't specified @EnableWebMvc elsewhere)
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);  // None by default
    r.setDefaultErrorView("error");    // No default
    r.setExceptionAttribute("ex");     // Default is "exception"
    r.setWarnLogCategory("example.MvcLogger");     // No default
    return r;
  }
  ...
}

defaultErrorView属性特别有用,因为它确保任何未捕获的异常生成一个合适的应用程序定义的错误页面。(大多数应用程序服务器的默认设置是显示Java堆栈跟踪 - 这是用户永远不会看到的)。

扩展SimpleMappingExceptionResolver

扩展SimpleMappingExceptionResolver一般是由于一下几个原因:

  • 使用构造函数直接设置属性 - 例如启用异常记录并设置logger使用
  • 通过重写buildLogMessage来重写默认为log消息。默认实现总是返回这个固定文本:Handler execution resulted in exception
  • 重写doResolveException为错误视图提供更多信息

例如

public class MyMappingExceptionResolver extends SimpleMappingExceptionResolver {
  public MyMappingExceptionResolver() {
    // Enable logging by providing the name of the logger to use
    setWarnLogCategory(MyMappingExceptionResolver.class.getName());
  }

  @Override
  public String buildLogMessage(Exception e, HttpServletRequest req) {
    return "MVC exception: " + e.getLocalizedMessage();
  }
    
  @Override
  protected ModelAndView doResolveException(HttpServletRequest req,
        HttpServletResponse resp, Object handler, Exception ex) {
    // Call super method to get the ModelAndView
    ModelAndView mav = super.doResolveException(req, resp, handler, ex);
        
    // Make the full URL available to the view - note ModelAndView uses
    // addObject() but Model uses addAttribute(). They work the same. 
    mav.addObject("url", request.getRequestURL());
    return mav;
  }
}

这些代码在demo应用的ExampleSimpleMappingExceptionResolver

扩展ExceptionHandlerExceptionResolver

重写它的doResolveHandlerMethodException方法来扩展ExceptionHandlerExceptionResolver 。它具有几乎相同的签名(它仅仅使用HandlerMethod 替换Handler)。
为了确保它能用,设置order属性的值(在新类的构造函数中)小于MAX_INT让它能在默认的ExceptionHandlerExceptionResolver实例之前运行(创建自己的处理程序实例比试图修改/替换Spring创建的更容易)。有关更多信息,请参阅演示应用程序中的ExampleExceptionHandlerExceptionResolver

Errors and REST

RESTful GET请求也可能会产生异常,我们已经看到如何返回标准的HTTP错误响应代码。但是,如果要返回有关错误的信息,该怎么办?这很容易做到。首先定义一个错误类:

public class ErrorInfo {
    public final String url;
    public final String ex;

    public ErrorInfo(String url, Exception ex) {
        this.url = url;
        this.ex = ex.getLocalizedMessage();
    }
}

现在你能返回一个这样的实例:

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MyBadDataException.class)
@ResponseBody ErrorInfo
handleBadRequest(HttpServletRequest req, Exception ex) {
    return new ErrorInfo(req.getRequestURL(), ex);
} 

什么时候使用?

像往常一样,Spring喜欢给你提供选择,所以你应该怎么做?这里有一些经验。如果您对XML配置或注解有偏好,这也没关系。

  • 对于你写的异常,考虑给他们添加@ResponseStatus。
  • 对于@ControllerAdvice类上实现@ExceptionHandler方法或者使用SimpleMappingExceptionResolver的其他所有异常。你可能已经为你的应用程序配置了SimpleMappingExceptionResolver,在这种情况下,向它添加新的异常类比实现@ControllerAdvice更容易。
  • 对于控制器特定的异常处理,将@ExceptionHandler方法添加到您的控制器。
  • 警告:在同一个应用程序中,要小心混合太多这些选项。如果同一个异常被处理多次,你可能无法得到期望的结果。控制器上的@ExceptionHandler方法始终在任何@ControllerAdvice实例上的那些方法之前选中。未定义处理controller-advices的顺序。

示例程序

演示应用程序可以在 github上找到。它使用Spring Boot和Thymeleaf构建了一个简单的Web应用程序。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容