QaAaASASXADDSafsfsadasdasdasdasd``飒飒的的# 27.1.9 Error Handling

首先先引入spring.io上比Spring Boot Reference文档更为详细的博客Exception Handling in Spring MVC

Blog Content "Exception Handling in Spring MVC"

为了更好地处理异常,我们不能只在抛出异常的controller里handle异常,需要在代码中更好地分层次地处理。

处理异常的地方有一下三个:per exception, per controller or globally

Using HTTP Status Codes

一般情况下,当在处理web-request的时候抛出任何未被处理的exception(any unhandled exception)都会造成服务器返回一个HTTP 500的response。然而,任何你自己定义的异常都可以添加@ResponseStatus注解(支持HTTP规范定义的所有HTTP status code)。当一个添加了该注解的exception被一个controller方法抛出时,且没在别处被处理,那么服务器response中的HTTP status将是注解内设定的值,而不是简单的500。

举个例子,an exception for a missing order

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

下面是一个使用了该异常的controller method

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

一个熟悉的HTTP 404 response将会返回,当一个带有unknow order id的URL被这个controller method处理的时候。

Controller Based Exception Handling

Using @ExceptionHandler

@ExceptionHandler具体分析,包括注解方法的可变参数有哪些,方法的返回值可以是哪些等,见API文档

你可以在同一个controller里添加额外的@ExceptionHandler方法,来处理被@RequestMapping方法抛出的异常,这些@ExceptionHandler 方法可以:

  1. 处理那些没有被@ResponseStatus注解的异常(一般是那些不是你自己写的,jdk或者框架预定义的异常)
  2. 重定向用户前往专门用来处理显示异常的视图(View)
  3. 创建一个完全自定义的error response

下面的代码将演示以上三个选项

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

在以上这些处理异常的方法中,你可能会加入额外的处理过程,比如最常见的就是用日志log记录这些异常

@ExceptionHandler注解的处理异常的方法,可以有可变的方法签名(flexible signatures),因此你可以传递这些方法一些明显的 servlet-related 对象,比如HttpServletRequest, HttpServletResponse, HttpSessionand/or Principle, Model(一定且只是可空对象,只是为了方便你给处理异常的视图传model,你不需要在方法里new).

Important Note: Model不一定作为@ExceptionHandler方法的参数,即使是参数之一,也只是可空(不是null,只是不包含任何内容而已)的,为了给视图什么的传递一个model对象,你可以像以上代码的handleError()一样,自己new一个ModelAndView

Exceptions and Views

请千万注意当你将exceptions添加到你的model里,你的用户肯定不会想看到一个满是java exception详细信息和栈跟踪的页面,然而如果将这些异常信息放入到页面源码的注释里,那么将会特别有用。如果,你使用JSP作为视图,那么可以使用以下代码来在页面输出异常(当然也可以将异常详细放在一个hidden的<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则可以像这样

<!--
      // Hidden Exception Details  - this is not a recommendation, but here is
      // how you hide an exception in the page using Thymeleaf
      -->
    <div th:utext="'&lt;!--'" th:remove="tag"></div>
    <div th:utext="'Failed URL: ' +  ${url}" th:remove="tag">${url}</div>
    <div th:utext="'Exception: ' + ${exception.message}" th:remove="tag">${exception.message}</div>
    <ul th:remove="tag">
        <li th:each="ste : ${exception.stackTrace}" th:remove="tag"><span
            th:utext="${ste}" th:remove="tag">${ste}</span></li>
    </ul>
    <div th:utext="'--&gt;'" th:remove="tag"></div>

Global Exception Handling

Using @ControllerAdvice Classes

一个controller advice允许你将单个处理异常的方法应用到全局从而能处理整个应用controller内产生的异常,你可以把它想象成基于注解的拦截器。

一个带有@ControllerAdvice注解的类就是一个controller-advice,这个类内支持三种方法:

  • 异常处理方法,被@ExceptionHandler注解
  • Model绑定增强方法(向视图Model添加额外的属性),被@ModelAttribute注解,但是请注意,这些Model的属性对处理异常的view是没用的,Model不会传递到处理异常的视图中。
  • Binder的初始化方法(用来处理配置表单form),被@InitBinder注解

更多有关@ControllerAdvice的内容,参见附录:Spring-boot常用注解解释:@ControllerAdvice

任何你前面见到的处理异常的方法,都可以在controller-advice中定义,并且这些方法将会应用到任何controller,用来出路这些controller抛出的异常。

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

只要你的一个controller抛出了DataIntegrityViolationException异常,就会交给GlobalControllerExceptionHandler.handleConflict()处理

如果您想为任何异常设置默认处理程序,那么你的代码就需要一点小小的波澜。您需要确保注释异常由框架处理。代码看起来如下所示:

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

Going Deeper

HandlerExceptionResolver

任何定义在DispatcherServlet的application context中的实现了HandlerExceptionResolver接口的Spring bean,都会被应用去拦截和处理任何由MVC系统产生的没有被Controller处理的(如上)异常,接口如下所示:

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

参数handler指产生异常的controller(记住@Controller的实例只是其中一种被Spring MVC支持的handler,像HttpInvokerExporter,WebFlow Executor都属于handler)

在幕后,Spring MVC默认产生了三种实现上面接口的resolver

  • ExceptionHandlerExceptionResolver 用来处理异常的第一步,先寻找该异常是否在controller和controller-advices中有@ExceptionHandler注解的handle方法,找到调用之,没有则返回,继续走HandlerExceptionResolver链。

  • ResponseStatusExceptionResolver上一个类未找到处理异常方法(没有@ExceptionHandler注解的handle方法来处理),则使用此类,寻找需要处理的异常是否被@ResponseStatus注解,是则用此类处理,不是再走HandlerExceptionResolver链

  • DefaultHandlerExceptionResolver转换标准Spring exceptions并将它们转换到相应的HTTP Status Codes

处理异常是由一个链的并且是按照一定的顺序的(这个是由Spring 内部产生的HandlerExceptionResolverComposite来处理的)

注意到resolveException这个接口方法没有Model,这也就是为什么@ExceptionHandler方法不能注入Model参数的原因了。

如果你想要,你可以实现自己的HandlerExceptionResolver来自定义异常处理系统,Handlers一般都会实现Spring的Ordered,因此你可以自己定义Handlers执行的顺序。

//剩下的一些内容不继续了,可以直接看博文Exception Handling in Spring MVC

Spring Boot 简化的错误处理

Spring Boot默认提供一个/error映射用来以合适的方式处理所有的错误,并将它注册为servlet容器中全局的 错误页面。对于机器客户端(相对于浏览器而言,浏览器偏重于人的行为),它会产生一个具有详细错误,HTTP状态,异常信息的JSON响应。对于浏览器客户端,它会产生一个白色标签样式(whitelabel)的错误视图,该视图将以HTML格式显示同样的数据(可以添加一个解析为'error'的View来自定义它)。为了完全替换默认的行为,你可以实现ErrorController,并注册一个该类型的bean定义,或简单地添加一个ErrorAttributes类型的bean以使用现存的机制,只是替换显示的内容。

默认的ErrorControllerErrorAttributes详细配置见ErrorMvcAutoConfiguration

BasicErrorController可以作为自定义ErrorController的基类,如果你想添加对新context type的处理(默认处理text/html),这会很有帮助。你只需要继承BasicErrorController,添加一个public方法,并注解带有produces属性的@RequestMapping,然后创建该新类型的bean。

你也可以定义一个@ControllerAdvice去自定义某个特殊controller或exception类型的JSON文档:

@ControllerAdvice(basePackageClasses = FooController.class)
public class FooControllerAdvice extends ResponseEntityExceptionHandler {

    @ExceptionHandler(YourException.class)
    @ResponseBody
    ResponseEntity<?> handleControllerException(HttpServletRequest request, Throwable ex) {
        HttpStatus status = getStatus(request);
        return new ResponseEntity<>(new CustomErrorType(status.value(), ex.getMessage()), status);
    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return HttpStatus.valueOf(statusCode);
    }

}

在以上示例中,如果跟FooController相同package的某个controller抛出YourException,一个CustomerErrorType类型的POJO的json展示将代替ErrorAttributes展示。

自定义错误页面

如果想为某个给定的状态码展示一个自定义的HTML错误页面,你需要将文件添加到/error文件夹下。错误页面既可以是静态HTML(比如,任何静态资源文件夹下添加的),也可以是使用模板构建的,文件名必须是明确的状态码或一系列标签。

例如,映射404到一个静态HTML文件,你的目录结构可能如下:

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- public/
             +- error/
             |   +- 404.html
             +- <other public assets>

使用FreeMarker模板映射所有5xx错误,你需要如下的目录结构:

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- templates/
             +- error/
             |   +- 5xx.ftl
             +- <other templates>

对于更复杂的映射,你可以添加实现ErrorViewResolver接口的beans:

public class MyErrorViewResolver implements ErrorViewResolver {

    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request,
            HttpStatus status, Map<String, Object> model) {
        // Use the request or status to optionally return a ModelAndView
        return ...
    }

}

你也可以使用Spring MVC特性,比如@ExceptionHandler方法和@ControllerAdvice,ErrorController将处理所有未处理的异常。

映射Spring MVC以外的错误页面

对于不使用Spring MVC的应用,你可以通过ErrorPageRegistrar接口直接注册ErrorPages。该抽象直接工作于底层内嵌servlet容器,即使你没有Spring MVC的DispatcherServlet,它们仍旧可以工作。

@Bean
public ErrorPageRegistrar errorPageRegistrar(){
    return new MyErrorPageRegistrar();
}

// ...

private static class MyErrorPageRegistrar implements ErrorPageRegistrar {

    @Override
    public void registerErrorPages(ErrorPageRegistry registry) {
        registry.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));
    }

}

如果你注册一个ErrorPage,该页面需要被一个Filter处理(在一些非Spring web框架中很常见,比如Jersey,Wicket),那么该Filter需要明确注册为一个ERROR分发器(dispatcher),例如:

@Bean
public FilterRegistrationBean myFilter() {
    FilterRegistrationBean registration = new FilterRegistrationBean();
    registration.setFilter(new MyFilter());
    ...
    registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
    return registration;
}

(默认的FilterRegistrationBean不包含ERRORdispatcher类型)

WebSphere应用服务器的错误处理

当部署到一个servlet容器时,Spring Boot通过它的错误页面过滤器将带有错误状态的请求转发到恰当的错误页面。request只有在response还没提交时才能转发(forwarded)到正确的错误页面,而WebSphere应用服务器8.0及后续版本默认情况会在servlet方法成功执行后提交response,你需要设置com.ibm.ws.webcontainer.invokeFlushAfterService属性为false来关闭该行为。

results matching ""

    No results matching ""