`
喧嚣求静
  • 浏览: 573656 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

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

 
阅读更多

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

参考文档:

https://segmentfault.com/a/1190000010060540

默认行为

根据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

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

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

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

  3. http://localhost:8080/return-view

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

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

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

会发现FooControllerFooRestController返回的结果都是一个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。

为何curl text/plain资源无法获得error

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

<logger name="org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod"level="TRACE"/>

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

org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
...

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

  1. curl http://localhost:8080/return-text-plain,会隐含一个请求头Accept: */*,会匹配到FooController.returnTextPlain(produces=text/plain)方法,注意:如果请求头不是Accept: */*Accept: text/plain,那么是匹配不到FooController.returnTextPlain的。

  2. RequestMappingHandlerMapping根据url匹配到了(见AbstractHandlerMethodMapping.lookupHandlerMethod#L341)FooController.returnTextPlan(produces=text/plain)。

  3. 方法抛出了异常,forward到/error

  4. RequestMappingHandlerMapping根据url匹配到了(见AbstractHandlerMethodMapping.lookupHandlerMethod#L341)BasicErrorController的两个方法[errorHtml]BasicErrorController_errorHtml和[error]BasicErrorController_error

  5. 因为请求头Accept: */*,所以会匹配error方法上(见AbstractHandlerMethodMapping#L352RequestMappingInfo.compareToProducesRequestCondition.compareTo)。

  6. error方法返回的是ResponseEntity<Map<String, Object>>,会被HttpEntityMethodProcessor.handleReturnValue处理。

  7. HttpEntityMethodProcessor进入AbstractMessageConverterMethodProcessor.writeWithMessageConverters,发现请求要求*/*(Accept: */*),而能够产生text/plain(FooController.returnTextPlan produces=text/plain),那它会去找能够将Map转换成String的[HttpMessageConverter]HttpMessageConverter,结果是找不到。

  8. AbstractMessageConverterMethodProcessor抛出HttpMediaTypeNotAcceptableException

那么为什么浏览器访问http://localhost:8080/return-text-plain就可以呢?你只需打开浏览器的开发者模式看看请求头就会发现Accept:text/html,...,所以在第4步会匹配到BasicErrorController.errorHtml方法,那结果自然是没有问题了。

那么这个问题怎么解决呢?我会在自定义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信息,解决这个问题有两个关键点:

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

  2. 提供自定义的ErrorController

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

To do that just extend BasicErrorController and add a public method with a @RequestMapping that has a producesattribute, 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统统返回JSON

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 No converter found for return value of type: class AnotherExceptionErrorMessageAbstractMessageConverterMethodProcessor#L187 error(json)
http://localhost:8080/bar/json-a some-ex-error.html some-ex-error.html
http://localhost:8080/bar/json-b Could not find acceptable representation error(json)
http://localhost:8080/bar/tex... some-ex-error.html some-ex-error.html
http://localhost:8080/bar/tex... Could not find acceptable representation Could not find acceptable representation

注意上方表格的Could not find acceptable representation错误,产生这个的原因和之前为何curl text/plain资源无法获得error是一样的:
无法将@ExceptionHandler返回的数据转换@RequestMapping.produces所要求的格式。

所以你会发现如果使用@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/err... static resource: public/error/403.html
http://localhost:8080/loo/err... thymeleaf view: templates/error/406.html
http://localhost:8080/loo/err... Whitelabel error page
http://localhost:8080/loo/err... 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有这么几个:

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

  2. ExceptionHandlerExceptionResolver,根据@ExceptionHandler resolve

  3. ResponseStatusExceptionResolver,根据@ResponseStatus resolve

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

Exception403Exception406都有被ResponseStatusExceptionResolver处理了,而SomeException没有任何Handler处理,这样DispatcherServlet就会将这个异常往上抛至到容器处理(见DispatcherServlet#L1243),以Tomcat为例,它在StandardHostValve#L317StandardHostValve#L345会将Status Code设置成500,然后跳转到/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

解决办法有两个:

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

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

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

  2. 使用@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";
}

总结:

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

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

    2. ExceptionHandlerExceptionResolver,根据@ExceptionHandler resolve

    3. ResponseStatusExceptionResolver,根据@ResponseStatus resolve

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

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

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

  4. 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<String, Object> 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的使用,这里只是在做一些补充说明:

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

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

附录I

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

Feature Spring Boot Spring MVC
BasicErrorController Yes No
ErrorAttributes Yes No
ErrorViewResolver Yes No
@ControllerAdvice No Yes
@ExceptionHandler No Yes
@ResponseStatus No Yes
HandlerExceptionResolver No Yes
分享到:
评论

相关推荐

    spring-boot-ssm:springboot-ssm 是一个基于Spring Boot & Spring & Spring MVC & MyBatis的简单通用的项目,用于快速构建中小型API的后端服务系统

    简介 spring-boot-ssm 是一个... 统一的Response封装,统一的异常处理,基础方法的抽象封装. 代码自动生成工具 Druid 集成阿里Druid数据库连接池以及API和数据库等监控 日志管理(集成Logback) 缓存框架(集成Redis)

    spring boot资料以及项目

    在Web开发方面,Spring Boot提供了Spring MVC框架,用于处理HTTP请求。资料中可能会有RESTful API设计、WebSocket、Swagger接口文档生成等实践内容。同时,Spring Boot与Spring Security的整合能帮助你快速实现应用...

    spring boot jsp mvc jpa hibernate mysql 示例 sample

    1. **Spring Boot MVC**:Spring Boot 提供了对 Spring MVC 的集成,使得构建 Web 应用变得简单。MVC(Model-View-Controller)设计模式是 Web 开发中常用的一种架构模式,用于分离业务逻辑、数据模型和用户界面。在...

    【BAT必备】spring面试题,spring MVC面试题,spring boot面试题,spring cloud面试题

    Spring MVC是一个基于MVC设计模式的Web框架,它提供了处理HTTP请求和响应的标准方法。主要组成部分包括控制器(Controller)、模型(Model)和视图(View)。其中: - 控制器:接收用户的请求,并决定如何处理。 - ...

    spring boot+spring mvc+spring整合开发音乐小程序(含简单服务端)

    spring boot+spring mvc+spring整合开发**音乐**小程序(含简单服务端) 项目描述 spring boot+spring mvc+spring代理请求**音乐接口获取数据,然后提供给**小程序做显示 运行环境 jdk8+IntelliJ IDEA+maven ...

    Spring Boot学习 Spring Boot Spring MVC MyBatis MySQL Spring .zip

    总的来说,这个压缩包中的学习资源可能涵盖了Spring Boot的起步、配置、自动配置原理、Spring MVC的请求处理流程、MyBatis的映射和SQL执行、以及如何整合这些技术来构建一个完整的Web应用。通过学习这些内容,你可以...

    Spring Boot 2.5.0简单学习pdf资料

    Spring Boot 2.5.0 支持 Spring MVC,能够快速搭建 Web 应用程序。 MyBatis MyBatis 是一个持久化框架,提供了对数据库的访问功能。Spring Boot 2.5.0 支持 MyBatis,能够快速搭建数据库访问层。 项目开发流程 ...

    LEARNING SPRING BOOT 3.0 - THIRD EDITION

    《Learning Spring Boot 3.0 - 第三版》是...书中还会深入讨论Spring Boot的核心特性,比如自动配置、健康检查、内嵌的Tomcat或Jetty服务器、Spring MVC用于构建RESTful API,以及数据访问层的集成,包括JPA和Hibernat

    demo.zip_android_java boot_spring boot_spring mvc_zip

    7. **Spring**:Spring框架不仅包含Spring Boot和Spring MVC,还有其他模块如Spring Data(用于数据库操作)、Spring Security(提供安全控制)等。这些组件可以组合使用,构建出复杂的企业级应用。 这个压缩包可能...

    Spring Boot 2 Recipes

    访问Spring MVC和新的Spring Web Sockets,以实现更简单的Web开发 使用微服务进行Web服务开发并与Spring Boot应用程序集成 无缝添加持久性和数据层,使您的Spring Boot Web应用程序做得更多 使用Spring Boot集成企业...

    spring boot+spring cloud+spring mvc+mybatis

    Spring Boot、Spring Cloud、Spring MVC和MyBatis是Spring生态系统中的关键组成部分,它们各自扮演着不同的角色,共同构建了一个完整的微服务架构。 1. **Spring Boot**:Spring Boot简化了Spring应用程序的初始...

    Spring Boot 系列教程6-全局异常处理

    例如,可以创建一个`BusinessException`,包含错误代码和错误信息,以便在处理异常时提供更具体的上下文。 2. **使用@ControllerAdvice** `@ControllerAdvice` 是Spring MVC的一个注解,用于标记一个类为全局异常...

    Spring boot+mybatis+Spring MVC

    Spring Boot、MyBatis 和 Spring MVC 是三个在Java开发领域广泛应用的开源框架,它们共同构建了一个高效、便捷的Web应用程序开发环境。让我们深入探讨这三个框架的核心功能和它们如何协同工作。 **Spring Boot** ...

    本科毕业设计+课程设计+基于spring boot+MVC开发的C语言在线学习平台+JSP、MySQL、HTML5、CS

    基于Spring Boot & Spring MVC开发的教学辅助系统。 &gt; &gt; Version 1.0.5 #### DONE - 用户登录 - 邮箱绑定、解绑 - 密码修改、重置 - 个人资料查看、修改 - 课程资料上传、下载 - 课程视频上传、下载、观看 - 课程...

    spring boot 异常处理方案

    Spring Boot支持i18n,可以在处理异常时结合使用以提供多语言的错误信息。 通过研究和实践这个开源项目,开发者能够掌握Spring Boot中的异常处理机制,提升应用程序的健壮性和用户体验。了解并运用这些知识点,可以...

    Spring Boot 2.X 实战教程.pdf

    本课程内容包括Spring简介、Spring Boot简介、安装JDK、安装Maven、第一个Spring Boot程序(使用Spring Initializr构建、Spring Boot代码讲解、安装Notepad++)、构建系统、代码、配置、三种方式运行程序、安装...

    最全最经典spring-mvc教程

    错误处理和异常处理也是Spring MVC中的重要部分,通过@ControllerAdvice和@ExceptionHandler可以全局处理异常,提供统一的错误页面。 最后,测试是任何应用程序开发的重要环节。Spring MVC提供了MockMVC,可以在不...

    Spring MVC 到 Spring Boot 的简化之路.docx

    Spring MVC 到 Spring Boot 的简化之路,这是一段旨在提高开发效率和降低复杂性的旅程。在传统的Spring MVC项目中,开发者需要处理繁琐的配置,包括pom.xml中的依赖管理、web.xml、spring.xml以及springmvc.xml等...

    Spring Boot 进阶笔记(详细全面) 中文PDF完整版.pdf

    创建 Spring Boot 项目通常有两种方式: 1. **手动创建 Maven 项目并添加依赖**:适合对 Spring Boot 结构有一定了解的开发者,需要手动管理依赖和配置。 2. **使用 Spring Initializr**:通过 IntelliJ IDEA 或其他...

    spring boot+mybatis+spring mvc+bootstrap开发停车位管理系统(调用百度地图API).rar

    本文将详细讲解如何使用Spring Boot、MyBatis、Spring MVC和Bootstrap技术栈开发一个停车位管理系统,并结合百度地图API实现车位的定位与导航功能。这些技术是现代Web开发中常用且强大的工具,它们各自承担着不同的...

Global site tag (gtag.js) - Google Analytics