专栏名称: ImportNew
伯乐在线旗下账号,专注Java技术分享,包括Java基础技术、进阶技能、架构设计和Java技术领域动态等。
目录
相关文章推荐
芋道源码  ·  今年后端这工资是认真的吗? ·  昨天  
芋道源码  ·  这 30 款 IDEA 宝贝插件 yyds ·  2 天前  
芋道源码  ·  提高系统吞吐量的一把利器:DeferredR ... ·  3 天前  
芋道源码  ·  又来了一款神级搜索引擎,差别真的挺大! ·  4 天前  
芋道源码  ·  2024年,劝搞Java的不要轻易跳槽! ·  1 周前  
51好读  ›  专栏  ›  ImportNew

Spring MVC REST 异常处理最佳实践( 下 )

ImportNew  · 公众号  · Java  · 2017-09-21 12:01

正文

(点击上方公众号,可快速关注)


来源:ImportNew - yuanzhang,

如有好文章投稿,请点击 → 这里了解详情


在本系列文章的第一部分里,我们讨论了异常发生时,该返回给 REST API 调用者的异常表示(格式)的最佳实践。


在本文(第二部分)中,我们将展示如何在使用 Spring MVC 编写的 REST API 中产生那些异常表述信息。


Spring 异常处理


Spring MVC 有两个主要方式来处理在调用 MVC 控制器(译注:Controller,下文统一为控制器)时抛出的异常:HandlerExceptionResolver 和 @ExceptionHandler 注解。


HandlerExceptionResolvers 对于用一种统一的方法来处理异常来说非常的理想,框架组件为你带来性能的提升。@ExceptionHandler 注解在你想手动展示一些基于业务逻辑的特定异常时则是个很好的选择。


它们的机制都非常强大:我们的普通代码可以以其含有的所有面向对象的益处,通过类型安全的方式抛出预期的异常来明确指示为何失败。然后我们可以让分解器和/或处理器来执行从异常到 HTTP 的必要翻译工作。你当然知道关注点分离(separation of concerns)的好处。


对于我们的 REST 异常处理而言,我们想要通过最佳实践的方式将所有的异常以一致的方式呈现。由于这个一致性(要求),采用HandlerExceptionResolver是非常合适的。 我们将呈献一个 REST 友好的 HandlerExceptionResolver 实现,它可在任何 REST API 中使用。


RestExceptionHandler


虽然大多数现有的 HandlerExceptionResolver 实现适用于基于 Spring MVC 的用户界面,但是对于 REST 异常处理它们并不是十分理想。为了实现第一部分中异常表示的格式,我们创建了自己的 HandlerExceptionResolver 实现,称为 RestExceptionHandler


提示:本文讨论的代码,是一个 Spring MVC REST 应用,可通过 Stormpath 在 Github) 上的 spring-mvc-rest-exhandler 源获取。


当你的 Spring MVC REST 控制器抛出异常时,RestExceptionHandler 将会:


  1. 将异常转换为一个 RestError 实例。RestError 的实现包含第一部分中讨论过的所有 REST 异常表示属性。

  2. 基于构建的 RestError 在 HTTP 响应中设置合适的 HTTP 响应状态码

  3. 将 RestError 表示提供给 HTTP 响应体。在默认情况下,我们提供一个 JSON 消息体,正如第一部分中的例子所示。


马上我们将会涉及 RestExceptionHandler 的底层工作细节。现在让我们通过一个示例项目看看 RestExceptionHandler 的实践,这样你就清楚它到底是如何运作的。


示例应用


你可以检出 Github 上的 spring-mvc-rest-exhandler 项目,它包含了主要的 RestExceptionHandler 实现和一个附加的示例应用。


https://github.com/stormpath/spring-mvc-rest-exhandler


在检出项目之后,你可以通过 Maven 来构建:


(在项目根目录执行):


$ mvn clean install


该命令会构建 RestExceptionHandler 库(一个 .jar 文件)以及一个单独的示例 web 应用(一个 .war 文件)。你可以在示例目录中运行该应用:


$ cd example

$ mvn jetty:run


以上命令将在本机启动 Jetty web 服务器,端口为 8080。


端点


应用启动后,你可以访问以下两个 REST 端点:


http://localhost:8080/v1/users/jsmith


http://localhost:8080/v1/users/djones


但这些只是运行正常的示例资源。本文我们真正在意的是:一个异常表示该是什么样子?


/v1/ 路径下的其他任何资源都会展示一个 HTTP 404 Not Found 的异常,并且包含在我们期望的 Rest 异常表示体中。试着访问下面这个 URL:


http://localhost:8080/v1/users/doesNotExist


你将会看到我们漂亮的 REST 异常表示!干净漂亮……


但这是如何运作的?由于所访问资源不存在,异常被抛出,然后某个东西将这个异常转换成了漂亮的 JSON 异常消息。让我们看看这是怎么发生的。


MVC 控制器


示例应用中有两个控制器 —— UserController 和 DefaultController。


UserController


UserController 的源码表明它是一个很简单的 Spring MVC 控制器。它模拟了一个成功查找两个示例用户的功能并在用户无法被找到时抛出一个自定义(应用指定)的 UnknownResourceException。


我们期望 UnknownResourceException 通过我们的 RestExceptionHandler 配置(我们很快会涉及到),自动转换 HTTP 404 (Not Found)为一个漂亮的异常表示。


DefaultController


DefaultController 源码表明它是一个基础设施组件的作用。通过 @RequestMapping, 我们能看到在 Spring 无法找到一个更明确的控制器时,Spring会调用这个默认的控制器。


DefaultController 十分简单:它在任何场景下始终抛出一个 UnknownResourceException。这对 REST 应用来说是有益的,因为在没有其他端点可以为一个请求服务时,我们总想展示一个相关的异常消息。


我们看到了 MVC 控制器如期的抛出漂亮的类型安全的异常。现在我们来看看RestExceptionHandler如何将这些异常转换 HTTP 异常消息体以及你可以如何定制它的行为。


RestExceptionHandler 的 Spring 配置


这里有一个基础的 RestExceptionHandler Spring bean 定义:


 

 

   

     

     

     

       

         

         

         

         

       

     

   

 


如果你仔细看,你会发现这个关于 RestExceptionHandler 配置的详细示例和它并没有太多直接关系。这里有两个属性:order 和 errorResolver。(我们也可以配置其他属性,例如 HttpMessageConverter 和其他的,但那已超出本文的讨论范围)。


Order


order 属性在你想要配置其他的HandlerExceptionResolvers而需链式使用 RestExceptionHandler 的功能时比较有用。


例如,这使你能配置一个 AnnotationMethodHandlerExceptionResolver bean (例如 order 0),这样你就可以在自定义异常处理策略中使用 @ExceptionHandler 注解,然后让 RestExceptionHandler (例如 order 100)作为所有其他异常的默认处理器。本示例应用的 rest-spring.xml 文件阐释了这个技术。


然而,显而易见的是 errorResolver 属性是这个配置的重点。接下来我们将涉及到。


RestErrorResolver


RestExceptionHandler 将运行时异常到 RestError 分解逻辑委托给 RestErrorResolver 实例。RestErrorResolver 知道如何返回一个表示 REST异常表示的RestError 实例。


在上面的 Spring XML 配置示例中,RestErrorResolver 的实现为 DefaultRestErrorResolver。DefaultRestErrorResolver依赖于一组映射定义来解析异常到对应的RestError实例。


针对每个映射定义项:


映射键可以是异常的全限定名中出现的任何字符(或这该异常的父类)。


映射值是RestError 定义,一个以逗号分隔的字符串。字符串通过启发式解析来决定如何构建一个RestError 实例。


当 DefaultRestErrorResolver 在运行时遇到一个异常是,它会根据映射定义检测该异常,如果找到匹配的映射,则返回对应的RestError 实例。


对于 Spring 的特定异常和其他熟知的异常 DefaultRestErrorResolver 已经有一些默认映射,但是这些定义可以在你配置 bean 的时候进行覆盖。


逗号分隔值的定义被启发式解析为 RestError 的属性。按位次顺序,定义可以包括:



非明确定义的根据位次进行处理。也就是说,以下定义是等价的:


明确的(一行,换行是为了格式化):


status=500, code=10023, msg=error.notFound,


moreInfoUrl=http://foo.com/docs/api/10023


不明确的:


500, 10023, error.notFound, http://foo.com/docs/api/10023


后面这个比较简洁而且更加方便。


另外,支持两个特殊值:_exmsg 和 _msg:


_exmsg 表明 message 属性应该反射为运行时异常消息,例如 exception.getMessage()


_msg 只对 developerMessage 属性有效,它表明 developerMessage 的值和 message 的值一样。


最后,需要注意的是,由于这些定义都是简单的键/值对,如果你的项目有很多这样的定义,最好将他们定义到一个 .properties 文件中,而不是在 Spring 的 XML 中。可以在启动时使用一些轻微的胶水代码或配置来读取那个文件,并作为 Map 放到 DefaultRestErrorResolver 中。


应用异常映射示例


在示例应用的 rest-servlet.xml 文件中,有两个示例异常映射定义



第一个可以概述为:“如果遇到 UnknownResourceException 则返回一个 HTTP 状态码为 404 的 RestError实例,其 message 属性默认为该异常的 message。”(_exmsg 表明应该使用 exception.getMessage() 作为 message 属性的值)。



第二个可以概述为:“如果遇到任何无法处理的 Throwable 则返回一个 HTTP 状态码为 500 的 RestError 实例, 以及一个 message 值,该值通过一个可用的 MessageSource 根据异常消息键 error.internal 获取返回”。(RestError的 message 值将会和调用 messageSource.getMessage(“error.internal”, null, “error.internal”, locale) 结果一样)。


通过翻译消息码为制定语言的消息,DefaultRestErrorResolver 实现甚至支持国际化(i18n)。它实现了 MessageSourceAware 来自动在 Spring 应用中获取任何已注册的 MessageSource 实例。它还允许配置一个 LocaleResolver 在解析特定本地化消息时使用。这允许你根据 REST 请求的本地信息得到特定语言的异常消息。太棒了!


总结


此处呈现的新的 RestExceptionHandler 非常灵活,并且支持非常个性化的异常数据,根据最佳实践的 REST 异常表示法,甚至可以返回国际化消息。


本文的代码和示例应用许可为非常商业友好的 Apache 2 许可,与 Spring 框架的许可相同。因此,我们希望 Spring 的开发人员会将这些组件合入 Spring 的未来版本中,同时如果他们有兴趣的话,我们很乐意尽最大努力协助他们。


本系列:



看完本文有收获?请转发分享给更多人

关注「ImportNew」,看技术干货