领先一步
VMware提供培训和认证,以加速您的进步。
了解更多注意:2018年4月修订
Spring MVC 提供了几种互补的异常处理方法,但在教授 Spring MVC 时,我经常发现我的学生对此感到困惑或不自在。
今天,我将向您展示可用的各种选项。我们的目标是尽可能不要在控制器方法中显式处理异常。它们是跨领域问题,最好在专用代码中单独处理。
有三个选项:按异常、按控制器或全局处理。
可在 http://github.com/paulc4/mvc-exceptions 找到一个演示应用程序,其中展示了此处讨论的要点。有关详细信息,请参见下面的示例应用程序。
注意:演示应用程序已改进和更新(2018年4月),以使用 Spring Boot 2.0.1,并且(希望)更容易使用和理解。我还修复了一些损坏的链接(感谢您的反馈,抱歉耽误了这么久)。
Spring Boot 允许使用最少的配置来设置 Spring 项目,如果您的应用程序的年龄不到几年,则您可能正在使用它。
Spring MVC 没有提供开箱即用的默认(回退)错误页面。设置默认错误页面的最常见方法始终是SimpleMappingExceptionResolver
(实际上从 Spring V1 开始)。我们稍后将讨论这一点。
但是 Spring Boot确实提供了一个回退错误处理页面。
启动时,Spring Boot 尝试查找/error
的映射。按照惯例,以/error
结尾的 URL 映射到相同名称的逻辑视图:error
。在演示应用程序中,此视图依次映射到error.html
Thymeleaf 模板。(如果使用 JSP,则根据InternalResourceViewResolver
的设置,它将映射到error.jsp
)。实际映射将取决于您或 Spring Boot 设置的ViewResolver
(如果有)。
如果找不到/error
的视图解析器映射,Spring Boot 将定义其自己的回退错误页面——所谓的“白标错误页面”(一个最小页面,仅包含 HTTP 状态信息和任何错误详细信息,例如来自未捕获异常的消息)。在示例应用程序中,如果您将error.html
模板重命名为例如error2.html
,然后重新启动,您将看到它正在使用。
如果您正在发出 RESTful 请求(HTTP 请求指定了除 HTML 之外的所需响应类型),Spring Boot 将返回与它放入“白标”错误页面中相同的错误信息的 JSON 表示。
$> curl -H "Accept: application/json" https://127.0.0.1:8080/no-such-page
{"timestamp":"2018-04-11T05:56:03.845+0000","status":404,"error":"Not Found","message":"No message available","path":"/no-such-page"}
Spring Boot 还为容器设置了一个默认错误页面,相当于web.xml
中的<error-page>
指令(尽管实现方式大相径庭)。在 Spring MVC 框架之外抛出的异常(例如来自 servlet 过滤器)仍然由 Spring Boot 回退错误页面报告。示例应用程序还显示了此示例。
本文末尾将更深入地讨论 Spring Boot 错误处理。
本文其余部分适用于您是否使用 Spring Boot 与 Spring 一起使用。.
急于求成的 REST 开发人员可以选择直接跳到关于自定义 REST 错误响应的部分。但是,他们应该阅读全文,因为大部分内容同样适用于所有 Web 应用程序,无论是否为 REST。
通常,处理 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 响应。
您可以向任何控制器添加额外的(@ExceptionHandler
)方法,以专门处理在同一控制器中由请求处理(@RequestMapping
)方法抛出的异常。此类方法可以
@ResponseStatus
注解的异常(通常是您没有编写的预定义异常)以下控制器演示了这三个选项
@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 相关对象,例如HttpServletRequest
、HttpServletResponse
、HttpSession
和/或Principle
。
重要说明:Model
不能是任何@ExceptionHandler
方法的参数。而是使用ModelAndView
在方法内部设置模型,如上面的handleError()
所示。
在将异常添加到模型时要小心。您的用户不想看到包含 Java 异常详细信息和堆栈跟踪的网页。您可能有明确禁止将任何异常信息放入错误页面的安全策略。另一个确保覆盖 Spring Boot 白标错误页面的原因。
确保以有用的方式记录异常,以便您的支持和开发团队可以在事件发生后对其进行分析。
请记住,以下方法可能很方便,但在生产环境中不是最佳实践.
隐藏页面源代码中的异常详细信息作为注释可能很有用,以帮助测试。如果使用 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 等效项,请参阅演示应用程序中的support.html。结果如下所示。
控制器建议允许您使用完全相同的异常处理技术,但将其应用于整个应用程序,而不仅仅是单个控制器。您可以将它们视为注释驱动的拦截器。
任何用@ControllerAdvice
注释的类都成为控制器建议,并支持三种类型的使用方法
@ExceptionHandler
注释的异常处理方法。@ModelAttribute
。请注意,异常处理视图无法访问这些属性。
@InitBinder
.
我们只关注异常处理——有关@ControllerAdvice
方法的更多信息,请搜索在线手册。
上面看到的任何异常处理程序都可以在控制器建议类中定义——但现在它们适用于从任何控制器抛出的异常。这是一个简单的例子
@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;
}
}
在DispatcherServlet
的应用程序上下文中声明的任何实现HandlerExceptionResolver
的Spring Bean都将用于拦截和处理MVC系统中引发的任何未由控制器处理的异常。接口如下所示
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex);
}
handler
指的是生成异常的控制器(记住,@Controller
实例只是Spring MVC支持的一种类型的处理程序。例如:HttpInvokerExporter
和WebFlow执行器也是处理程序的类型)。
在幕后,MVC默认创建三个这样的解析器。正是这些解析器实现了上面讨论的行为。
ExceptionHandlerExceptionResolver
将未捕获的异常与处理程序(控制器)和任何控制器建议上的合适的@ExceptionHandler
方法匹配。ResponseStatusExceptionResolver
查找由@ResponseStatus
注释的未捕获异常(如第1节所述)。DefaultHandlerExceptionResolver
转换标准的Spring异常并将它们转换为HTTP状态码(我没有在上面提到这一点,因为它在Spring MVC内部)。这些是按列出的顺序链接和处理的——Spring在内部创建一个专用bean(HandlerExceptionResolverComposite
)来执行此操作。
请注意,resolveException
的方法签名不包含Model
。这就是为什么不能使用模型注入@ExceptionHandler
方法的原因。
如果需要,可以实现自己的HandlerExceptionResolver
来设置自己的自定义异常处理系统。处理程序通常实现Spring的Ordered
接口,以便可以定义处理程序运行的顺序。
Spring长期以来提供了一个简单但方便的HandlerExceptionResolver
实现,您可能已经在应用程序中使用了它——SimpleMappingExceptionResolver
。它提供以下选项:
exception
属性的名称,以便可以在视图内使用它(例如JSP)。默认情况下,此属性名为exception
。设置为null
以禁用。请记住,从@ExceptionHandler
方法返回的视图无法访问异常,但定义为SimpleMappingExceptionResolver
的视图可以访问异常。
这是一个使用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;
}
...
}
或者使用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>
defaultErrorView属性特别有用,因为它确保任何未捕获的异常都会生成合适的应用程序定义的错误页面。(大多数应用程序服务器的默认行为是显示Java堆栈跟踪——您的用户绝不应该看到这一点)。Spring Boot提供了另一种方法来使用其“白标”错误页面来实现相同的功能。
出于多种原因,扩展SimpleMappingExceptionResolver
是很常见的
buildLogMessage
来覆盖默认日志消息。默认实现始终返回此固定文本: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;
}
}
此代码在演示应用程序中,位于ExampleSimpleMappingExceptionResolver
也可以扩展ExceptionHandlerExceptionResolver
并以相同的方式覆盖其doResolveHandlerMethodException
方法。它具有几乎相同的方法签名(它只是采用新的HandlerMethod
而不是Handler
)。
为了确保它被使用,还要将继承的order属性(例如在新类的构造函数中)设置为小于MAX_INT
的值,以便它在默认的ExceptionHandlerExceptionResolver实例之前运行(创建自己的处理程序实例比尝试修改/替换Spring创建的实例更容易)。有关更多信息,请参阅演示应用程序中的ExampleExceptionHandlerExceptionResolver。
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();
}
}
现在可以从处理程序返回一个实例作为@ResponseBody
,如下所示
@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
实例上的方法选择。控制器建议的处理顺序是未定义的。可以在github上找到一个演示应用程序。它使用Spring Boot和Thymeleaf来构建一个简单的Web应用程序。
该应用程序已修改两次(2014年10月,2018年4月),并且(希望)更好理解也更容易使用。其基本原理保持不变。它使用Spring Boot V2.0.1和Spring V5.0.5,但代码也适用于Spring 3.x和4.x。
演示程序运行在Cloud Foundry上,地址为 http://mvc-exceptions-v2.cfapps.io/。
该应用程序引导用户浏览5个演示页面,重点介绍不同的异常处理技术。
@ExceptionHandler
方法的控制器,用于处理它自己的异常。SimpleMappingExceptionResolver
处理异常。SimpleMappingExceptionResolver
以便进行比较。应用程序中最重要的文件的描述及其与每个演示程序的关系,可以在项目的README.md中找到。
主页为index.html,其中
每个演示页面都包含多个链接,所有这些链接都会故意引发异常。每次您都需要使用浏览器的后退按钮返回到演示页面。
感谢Spring Boot,您可以将此演示程序作为Java应用程序运行(它运行嵌入式Tomcat容器)。要运行应用程序,您可以使用以下方法之一(第二种方法得益于Spring Boot Maven插件):
mvn exec:java
mvn spring-boot:run
您可以选择其中一种。主页URL将为https://127.0.0.1:8080。
在演示应用程序中,我还展示了如何创建一个“支持就绪”的错误页面,其中堆栈跟踪隐藏在HTML源代码中(作为注释)。理想情况下,支持应该从日志中获取此信息,但生活并不总是理想的。无论如何,此页面确实显示了底层错误处理方法handleError
如何创建它自己的ModelAndView
,以便在错误页面中提供额外信息。参见
ExceptionHandlingController.handleError()
,位于githubGlobalControllerExceptionHandler.handleError()
,位于githubSpring Boot允许使用最少的配置来设置Spring项目。Spring Boot在检测到类路径上的某些关键类和包时会自动设置合理的默认值。例如,如果它看到您正在使用Servlet环境,它将使用最常用的视图解析器、处理器映射等设置Spring MVC。如果它看到JSP和/或Thymeleaf,它将设置这些视图技术。
Spring Boot如何支持本文开头描述的默认错误处理?
/error
。BasicErrorController
来处理对/error
的任何请求。控制器将错误信息添加到内部模型,并返回error
作为逻辑视图名称。View
对象提供默认错误页面(使其独立于您可能使用的任何视图解析系统)。BeanNameViewResolver
,以便/error
可以映射到同名的View
。ErrorMvcAutoConfiguration
类,您会看到defaultErrorView
作为名为error
的bean返回。这是BeanNameViewResolver
找到的View bean。“Whitelabel”错误页面故意设计得很简陋。您可以覆盖它:
src/main/resources/templates/error.html
(此位置由Spring Boot属性spring.thymeleaf.prefix
设置——其他支持的服务器端视图技术(如JSP或Mustache)也存在类似的属性)。error
的bean。2. 或通过设置属性禁用Spring Boot的“Whitelabel”错误页面:server.error.whitelabel.enabled
设置为false
。您的容器将使用默认错误页面。
按照约定,Spring Boot属性通常设置在application.properties
或application.yml
中。
如果您已经使用SimpleMappingExceptionResolver
来设置默认错误视图,该怎么办?很简单,使用setDefaultErrorView()
定义与Spring Boot使用的视图相同的视图:error
。
请注意,在演示中,SimpleMappingExceptionResolver
的defaultErrorView
属性故意设置为defaultErrorPage
而不是error
,这样您就可以看到处理程序何时生成错误页面以及Spring Boot何时负责。通常情况下,两者都应设置为error
。
在Spring框架外部抛出的异常(例如,来自servlet过滤器的异常)也会由Spring Boot的回退错误页面报告。
为此,Spring Boot必须为容器注册一个默认错误页面。在Servlet 2中,有一个<error-page>
指令,您可以将其添加到您的web.xml
中来执行此操作。遗憾的是,Servlet 3没有提供等效的Java API。相反,Spring Boot执行以下操作:
捕获进一步发生的异常并进行处理。