资讯专栏INFORMATION COLUMN

Spring Boot & Spring MVC 异常处理的N种方法

h9911 / 1480人阅读

摘要:定制特定异常返回结果根据官方文档的例子,可以使用和对特定异常返回特定的结果。下面是用浏览器和访问的结果无输出注意上方表格的错误,产生这个的原因前面已经讲过。不过需要注意的是,无法通过设定,由或者容器决定里一律是。

github:https://github.com/chanjarste...

参考文档:

Spring Boot 1.5.4.RELEASE Documentation

Spring framework 4.3.9.RELEASE Documentation

Exception Handling in Spring MVC

默认行为

根据Spring Boot官方文档的说法:

For machine clients it will produce a JSON response with details of the error, the HTTP status and the exception message. For browser clients there is a ‘whitelabel’ error view that renders the same data in HTML format

也就是说,当发生异常时:

如果请求是从浏览器发送出来的,那么返回一个Whitelabel Error Page

如果请求是从machine客户端发送出来的,那么会返回相同信息的json

你可以在浏览器中依次访问以下地址:

http://localhost:8080/return-model-and-view

http://localhost:8080/return-view-name

http://localhost:8080/return-view

http://localhost:8080/return-text-plain

http://localhost:8080/return-json-1

http://localhost:8080/return-json-2

会发现FooController和FooRestController返回的结果都是一个Whitelabel Error Page也就是html。

但是如果你使用curl访问上述地址,那么返回的都是如下的json

{
  "timestamp": 1498886969426,
  "status": 500,
  "error": "Internal Server Error",
  "exception": "me.chanjar.exception.SomeException",
  "message": "...",
  "trace": "...",
  "path": "..."
}

但是有一个URL除外:http://localhost:8080/return-text-plain,它不会返回任何结果,原因稍后会有说明。

本章节代码在me.chanjar.boot.def,使用DefaultExample运行。

注意:我们必须在application.properties添加server.error.include-stacktrace=always才能够得到stacktrace。

Spring MVC处理请求的总体流程

分析为何浏览器访问都Whitelabel Error Page

分析为何curl text/plain资源却没有返回结果

如果你在logback-spring.xml里一样配置了这么一段:

那么你就能在日志文件里发现这么一个异常:

... TRACE 13387 --- [nio-8080-exec-2] .w.s.m.m.a.ServletInvocableHandlerMethod : Invoking "org.springframework.boot.autoconfigure.web.BasicErrorController.error" with arguments [org.apache.catalina.core.ApplicationHttpRequest@1408b81]
... TRACE 13387 --- [nio-8080-exec-2] .w.s.m.m.a.ServletInvocableHandlerMethod : Method [org.springframework.boot.autoconfigure.web.BasicErrorController.error] returned [<500 Internal Server Error,{timestamp=Thu Nov 09 13:20:15 CST 2017, status=500, error=Internal Server Error, exception=me.chanjar.exception.SomeException, message=No message available, trace=..., path=/return-text-plain, {}>]
... TRACE 13387 --- [nio-8080-exec-2] .w.s.m.m.a.ServletInvocableHandlerMethod : Error handling return value [type=org.springframework.http.ResponseEntity] [value=<500 Internal Server Error,{timestamp=Thu Nov 09 13:20:15 CST 2017, status=500, error=Internal Server Error, exception=me.chanjar.exception.SomeException, message=No message available, trace=..., path=/return-text-plain, {}>]
HandlerMethod details: 
Controller [org.springframework.boot.autoconfigure.web.BasicErrorController]
Method [public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)]
org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
...

要理解这个异常是怎么来的,那我们来简单分析以下Spring MVC的处理过程:

那么这个问题怎么解决呢?我会在自定义ErrorController里说明。

自定义Error页面

前面看到了,Spring Boot针对浏览器发起的请求的error页面是Whitelabel Error Page,下面讲解如何自定义error页面。

注意2:自定义Error页面不会影响machine客户端的输出结果

方法1

根据Spring Boot官方文档,如果想要定制这个页面只需要:

to customize it just add a View that resolves to ‘error’

这句话讲的不是很明白,其实只要看ErrorMvcAutoConfiguration.WhitelabelErrorViewConfiguration的代码就知道,只需注册一个名字叫做errorView类型的Bean就行了。

本例的CustomDefaultErrorViewConfiguration注册将error页面改到了templates/custom-error-page/error.html上。

本章节代码在me.chanjar.boot.customdefaulterrorview,使用CustomDefaultErrorViewExample运行。

方法2

方法2比方法1简单很多,在Spring官方文档中没有说明。其实只需要提供error View所对应的页面文件即可。

比如在本例里,因为使用的是Thymeleaf模板引擎,所以在classpath /templates放一个自定义的error.html就能够自定义error页面了。

本章节就不提供代码了,有兴趣的你可以自己尝试。

自定义Error属性

前面看到了不论error页面还是error json,能够得到的属性就只有:timestamp、status、error、exception、message、trace、path。

如果你想自定义这些属性,可以如Spring Boot官方文档所说的:

simply add a bean of type ErrorAttributes to use the existing mechanism but replace the contents

ErrorMvcAutoConfiguration.errorAttributes提供了DefaultErrorAttributes,我们也可以参照这个提供一个自己的CustomErrorAttributes覆盖掉它。

如果使用curl访问相关地址可以看到,返回的json里的出了修改过的属性,还有添加的属性:

{
  "exception": "customized exception",
  "add-attribute": "add-attribute",
  "path": "customized path",
  "trace": "customized trace",
  "error": "customized error",
  "message": "customized message",
  "timestamp": 1498892609326,
  "status": 100
}

本章节代码在me.chanjar.boot.customerrorattributes,使用CustomErrorAttributesExample运行。

自定义ErrorController

在前面提到了curl http://localhost:8080/return-text-plain得不到error信息,解决这个问题有两个关键点:

请求的时候指定Accept头,避免匹配到BasicErrorController.error方法。比如:curl -H "Accept: text/plain" http://localhost:8080/return-text-plain

提供自定义的ErrorController提供一个path=/error procudes=text/plain的方法。

其实还有另一种方式:提供一个Object->String转换的HttpMessageConverter,这个方法本文不展开。

下面将如何提供自定义的ErrorController。按照Spring Boot官方文档的说法:

To do that just extend BasicErrorController and add a public method with a @RequestMapping that has a produces attribute, and create a bean of your new type.

所以我们提供了一个CustomErrorController,并且通过CustomErrorControllerConfiguration将其注册为Bean。

本章节代码在me.chanjar.boot.customerrorcontroller,使用CustomErrorControllerExample运行。

ControllerAdvice定制特定异常返回结果

根据Spring Boot官方文档的例子,可以使用@ControllerAdvice和@ExceptionHandler对特定异常返回特定的结果。

我们在这里定义了一个新的异常:AnotherException,然后在BarControllerAdvice中对SomeException和AnotherException定义了不同的@ExceptionHandler:

SomeException都返回到controlleradvice/some-ex-error.html

AnotherException统统返回ResponseEntity

在BarController中,所有*-a都抛出SomeException,所有*-b都抛出AnotherException。下面是用浏览器和curl访问的结果:

url Browser curl
http://localhost:8080/bar/html-a some-ex-error.html some-ex-error.html
http://localhost:8080/bar/html-b error(json) error(json)
http://localhost:8080/bar/json-a some-ex-error.html some-ex-error.html
http://localhost:8080/bar/json-b error(json) error(json)
http://localhost:8080/bar/text-plain-a some-ex-error.html some-ex-error.html
http://localhost:8080/bar/text-plain-b Could not find acceptable representation(White Error Page) Could not find acceptable representation(无输出)

注意上方表格的Could not find acceptable representation错误,产生这个的原因前面已经讲过。

不过需要注意的是流程稍微有点不同,在前面的例子里的流程是这样的:

访问url

抛出异常

forward到 /error

BasicErrorController.error方法返回的ResponseEntity没有办法转换成String

本章节例子的异常是这样的:

访问url

抛出异常

@ExceptionHandler处理

AnotherException的@ExceptionHander返回的ResponseEntity没有办法转换成String,被算作没有被处理成功

forward到 /error

BasicErrorController.error方法返回的ResponseEntity没有办法转换成String

所以你会发现如果使用@ExceptionHandler,那就得自己根据请求头Accept的不同而输出不同的结果了,办法就是定义一个void @ExceptionHandler,具体见@ExceptionHandler javadoc。

定制不同Status Code的错误页面

Spring Boot 官方文档提供了一种简单的根据不同Status Code跳到不同error页面的方法,见这里。

我们可以将不同的Status Code的页面放在classpath: public/errorclasspath: templates/error目录下,比如400.html5xx.html400.ftl5xx.ftl

打开浏览器访问以下url会获得不同的结果:

url Result
http://localhost:8080/loo/error-403 static resource: public/error/403.html
http://localhost:8080/loo/error-406 thymeleaf view: templates/error/406.html
http://localhost:8080/loo/error-600 Whitelabel error page
http://localhost:8080/loo/error-601 thymeleaf view: templates/error/6xx.html

注意/loo/error-600返回的是Whitelabel error page,但是/loo/error-403loo/error-406能够返回我们期望的错误页面,这是为什么?先来看看代码。

loo/error-403中,我们抛出了异常Exception403

@ResponseStatus(HttpStatus.FORBIDDEN)
public class Exception403 extends RuntimeException

loo/error-406中,我们抛出了异常Exception406

@ResponseStatus(NOT_ACCEPTABLE)
public class Exception406 extends RuntimeException

注意到这两个异常都有@ResponseStatus注解,这个是注解标明了这个异常所对应的Status Code。
但是在loo/error-600中抛出的SomeException没有这个注解,而是尝试在Response.setStatus(600)来达到目的,但结果是失败的,这是为什么呢?:

@RequestMapping("/error-600")
public String error600(HttpServletRequest request, HttpServletResponse response) throws SomeException {
  request.setAttribute(WebUtils.ERROR_STATUS_CODE_ATTRIBUTE, 600);
  response.setStatus(600);
  throw new SomeException();
}

要了解为什么就需要知道Spring MVC对于异常的处理机制,下面简单讲解一下:

Spring MVC处理异常的地方在DispatcherServlet.processHandlerException,这个方法会利用HandlerExceptionResolver来看异常应该返回什么ModelAndView

目前已知的HandlerExceptionResolver有这么几个:

DefaultErrorAttributes,只负责把异常记录在Request attributes中,name是org.springframework.boot.autoconfigure.web.DefaultErrorAttributes.ERROR

ExceptionHandlerExceptionResolver,根据@ExceptionHandler resolve

ResponseStatusExceptionResolver,根据@ResponseStatus resolve

DefaultHandlerExceptionResolver,负责处理Spring MVC标准异常

Exception403Exception406都有被ResponseStatusExceptionResolver处理了,而SomeException没有任何Handler处理,这样DispatcherServlet就会将这个异常往上抛至到容器处理(见DispatcherServlet#L1243),以Tomcat为例,它在StandardHostValve#L317、StandardHostValve#L345会将Status Code设置成500,然后forward到/error,结果就是BasicErrorController处理时就看到Status Code=500,然后按照500去找error page找不到,就只能返回White error page了。

实际上,从Request的attributes角度来看,交给BasicErrorController处理时,和容器自己处理时,有几个相关属性的内部情况时这样的:

Attribute name When throw up to Tomcat Handled by HandlerExceptionResolver
DefaultErrorAttributes.ERROR Has value Has Value
DispatcherServlet.EXCEPTION No value Has Value
javax.servlet.error.exception Has value No Value

PS. DefaultErrorAttributes.ERROR = org.springframework.boot.autoconfigure.web.DefaultErrorAttributes.ERROR

PS. DispatcherServlet.EXCEPTION = org.springframework.web.servlet.DispatcherServlet.EXCEPTION

解决办法有两个:

SomeException添加@ResponseStatus,但是这个方法有两个局限:

如果这个异常不是你能修改的,比如在第三方的Jar包里

如果@ResponseStatus使用HttpStatus作为参数,但是这个枚举定义的Status Code数量有限

使用@ExceptionHandler,不过得注意自己决定view以及status code

第二种解决办法的例子loo/error-601,对应的代码:

@RequestMapping("/error-601")
public String error601(HttpServletRequest request, HttpServletResponse response) throws AnotherException {
  throw new AnotherException();
}

@ExceptionHandler(AnotherException.class)
String handleAnotherException(HttpServletRequest request, HttpServletResponse response, Model model)
    throws IOException {
  // 需要设置Status Code,否则响应结果会是200
  response.setStatus(601);
  model.addAllAttributes(errorAttributes.getErrorAttributes(new ServletRequestAttributes(request), true));
  return "error/6xx";
}

总结:

没有被HandlerExceptionResolverresolve到的异常会交给容器处理。已知的实现有(按照顺序):

DefaultErrorAttributes,只负责把异常记录在Request attributes中,name是org.springframework.boot.autoconfigure.web.DefaultErrorAttributes.ERROR

ExceptionHandlerExceptionResolver,根据@ExceptionHandler resolve

ResponseStatusExceptionResolver,根据@ResponseStatus resolve

DefaultHandlerExceptionResolver,负责处理Spring MVC标准异常

@ResponseStatus用来规定异常对应的Status Code,其他异常的Status Code由容器决定,在Tomcat里都认定为500(StandardHostValve#L317、StandardHostValve#L345)

@ExceptionHandler处理的异常不会经过BasicErrorController,需要自己决定如何返回页面,并且设置Status Code(如果不设置就是200)

BasicErrorController会尝试根据Status Code找error page,找不到的话就用Whitelabel error page

本章节代码在me.chanjar.boot.customstatuserrorpage,使用CustomStatusErrorPageExample运行。

利用ErrorViewResolver来定制错误页面

前面讲到BasicErrorController会根据Status Code来跳转对应的error页面,其实这个工作是由DefaultErrorViewResolver完成的。

实际上我们也可以提供自己的ErrorViewResolver来定制特定异常的error页面。

@Component
public class SomeExceptionErrorViewResolver implements ErrorViewResolver {

  @Override
  public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map model) {
    return new ModelAndView("custom-error-view-resolver/some-ex-error", model);
  }

}

不过需要注意的是,无法通过ErrorViewResolver设定Status Code,Status Code由@ResponseStatus或者容器决定(Tomcat里一律是500)。

本章节代码在me.chanjar.boot.customerrorviewresolver,使用CustomErrorViewResolverExample运行。

@ExceptionHandler 和 @ControllerAdvice

前面的例子中已经有了对@ControllerAdvice和@ExceptionHandler的使用,这里只是在做一些补充说明:

@ExceptionHandler配合@ControllerAdvice用时,能够应用到所有被@ControllerAdvice切到的Controller

@ExceptionHandler在Controller里的时候,就只会对那个Controller生效

最佳实践

前面讲了那么多种方式,那么在Spring MVC中处理异常的最佳实践是什么?在回答这个问题前我先给出一个好的异常处理应该是什么样子的:

返回的异常信息能够适配各种Accept,比如Accept:text/html返回html页面,Accept:application/json返回json。

统一的异常信息schema,且可自定义,比如只包含timestamperrormessage等信息。

能够自定义部分信息,比如可以自定义errormessage的内容。

要达成以上目标我们可以采取的方法:

达成第1条:自定义ErrorController,扩展BasicErrorController,支持更多的Accept类型。

达成第2条:自定义ErrorAttributes

达成第3条:

使用@ResponseStatusResponseStatusException(since 5.0)

前一种方式不适用时,自定义ErrorAttributes,在里面写代码,针对特定异常返回特定信息。推荐使用配置的方式来做,比如配置文件里写XXXException的message是YYYY。

Spring MVC对于从Controller抛出的异常是不打印到console的,解决办法是提供一个HandlerExceptionResolver,比如这样:

@Order(Ordered.HIGHEST_PRECEDENCE)
public class ErrorLogger implements HandlerExceptionResolver {

  private static final Logger LOGGER = LoggerFactory.getLogger(ErrorLogger.class);

  @Override
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
      Exception ex) {
    LOGGER.error("Exception happened at [{}]: {}", request.getRequestURI(), ExceptionUtils.getStackTrace(ex));
    return null;
  }

}
附录I

下表列出哪些特性是Spring Boot的,哪些是Spring MVC的:

Feature Spring Boot Spring MVC
BasicErrorController Yes
ErrorAttributes Yes
ErrorViewResolver Yes
@ControllerAdvice Yes
@ExceptionHandler Yes
@ResponseStatus Yes
HandlerExceptionResolver Yes

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/67301.html

相关文章

  • Spring体系常用项目一览

    摘要:的面向的异常遵从通用的异常层次结构。比如以前常用的框架,现在常用的框架包含许多项目,下面挑一些最常用的出来总结一下。状态是流程中事件发生的地点,在流程中通过转移的方式从一个状态到另一个状态,流程的当前状况称为流程数据。 如今做Java尤其是web几乎是避免不了和Spring打交道了,但是Spring是这样的大而全,新鲜名词不断产生,学起来给人一种凌乱的感觉,我就在这里总结一下,理顺头绪...

    OnlyLing 评论0 收藏0
  • Java 最常见 200+ 面试题全解析:面试必备(附答案)

    摘要:的简称,运行环境,为的运行提供了所需环境。分割字符串,返回一个分割后的字符串数组。线程安全是线程安全的,而是非线程安全的。迭代器取代了集合框架中的,迭代器允许调用者在迭代过程中移除元素。 本文分为十九个模块,分别是: Java 基础、容器、多线程、反射、对象拷贝、Java Web 、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Clou...

    hufeng 评论0 收藏0
  • Spring Boot 2.x 系列教程:WebFlux REST API 全局异常处理 Error

    摘要:挺多人咨询的,异常处理用切面注解去实现去全局异常处理。全局异常处理类,代码如下代码解析如下抽象类是用来处理全局错误时进行扩展和实现注解标记的切面排序,值越小拥有越高的优先级,这里设置优先级偏高。 本文内容 为什么要全局异常处理? WebFlux REST 全局异常处理实战 小结 摘录:只有不断培养好习惯,同时不断打破坏习惯,我们的行为举止才能够自始至终都是正确的。 一、为什么要全局...

    BicycleWarrior 评论0 收藏0
  • Spring-Boot学习笔记

    摘要:学习笔记使用很容易创建一个独立运行运行内嵌容器准生产级别的基于框架的项目,使用你可以不用或者只需要很少的配置。异常消息如果这个错误是由异常引起的。错误发生时请求的路径。 Spring-Boot 1.5 学习笔记 使用Spring Boot很容易创建一个独立运行(运行jar,内嵌Servlet容器)、准生产级别的基于Spring框架的项目,使用Spring Boot你可以不用或者只需要很...

    curlyCheng 评论0 收藏0

发表评论

0条评论

h9911

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<