Spring MVC 异常处理 - @ControllerAdvice、@ExceptionHandler、HandlerExceptionResolver
Spring MVC 异常处理非常重要,它可以确保您不会将服务器异常发送到客户端。今天,我们将使用@ExceptionHandler、@ControllerAdvice和 HandlerExceptionResolver 研究 Spring 异常处理。任何Web 应用程序都需要良好的异常处理设计,因为当我们的应用程序抛出任何未处理的异常时,我们不想提供容器生成的页面。
Spring 异常处理
对于任何 Web 应用程序框架来说,拥有定义良好的异常处理方法都是一个巨大的优势,也就是说,Spring MVC 框架在我们的 Web 应用程序中的异常和错误处理方面表现良好。Spring MVC 框架提供了以下方法来帮助我们实现强大的异常处理。
- 基于控制器- 我们可以在控制器类中定义异常处理程序方法。我们需要做的就是用
@ExceptionHandler
注释对这些方法进行注释。此注释以 Exception 类为参数。因此,如果我们为 Exception 类定义了其中一个方法,那么我们的请求处理程序方法抛出的所有异常都将被处理。这些异常处理程序方法与其他请求处理程序方法一样,我们可以构建错误响应并使用不同的错误页面进行响应。我们还可以发送 JSON 错误响应,我们将在后面的示例中看到。如果定义了多个异常处理程序方法,则使用最接近 Exception 类的处理程序方法。例如,如果我们为 IOException 和 Exception 定义了两个处理程序方法,并且我们的请求处理程序方法抛出 IOException,则将执行 IOException 的处理程序方法。 - 全局异常处理程序- 异常处理是一个横切关注点,应该针对应用程序中的所有切入点进行处理。我们已经研究过Spring AOP,这就是为什么 Spring 提供
@ControllerAdvice
注释的原因,我们可以将其用于任何类来定义我们的全局异常处理程序。全局控制器建议中的处理程序方法与基于控制器的异常处理程序方法相同,并在控制器类无法处理异常时使用。 - HandlerExceptionResolver - 对于一般异常,大多数时候我们提供静态页面。Spring 框架提供了
HandlerExceptionResolver
我们可以实现的接口来创建全局异常处理程序。这种定义全局异常处理程序的额外方法背后的原因是 Spring 框架还提供了默认实现类,我们可以在 Spring bean 配置文件中定义这些默认实现类,以获得 Spring 框架异常处理的好处。SimpleMappingExceptionResolver
是默认实现类,它允许我们配置 exceptionMappings,我们可以在其中指定要为特定异常使用哪种资源。我们还可以覆盖它以使用特定于应用程序的更改(例如记录异常消息)来创建我们自己的全局处理程序。
让我们创建一个 Spring MVC 项目,我们将在其中研究基于 Controller、基于 AOP 和基于 Exception Resolver 的异常和错误处理方法的实现。我们还将编写一个返回 JSON 响应的异常处理程序方法。如果您不熟悉 Spring 中的 JSON,请阅读Spring Restful JSON 教程。我们的最终项目将如下图所示,我们将逐一查看应用程序的所有组件。
Spring 异常处理 Maven 依赖项
除了标准的 Spring MVC 依赖项之外,我们还需要Jackson JSON依赖项来支持 JSON。我们的最终 pom.xml 文件如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.journaldev.spring</groupId>
<artifactId>SpringExceptionHandling</artifactId>
<name>SpringExceptionHandling</name>
<packaging>war</packaging>
<version>1.0.0-BUILD-SNAPSHOT</version>
<properties>
<java-version>1.6</java-version>
<org.springframework-version>4.0.2.RELEASE</org.springframework-version>
<org.aspectj-version>1.7.4</org.aspectj-version>
<org.slf4j-version>1.7.5</org.slf4j-version>
<jackson.databind-version>2.2.3</jackson.databind-version>
</properties>
<dependencies>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.databind-version}</version>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${org.springframework-version}</version>
<exclusions>
<!-- Exclude Commons Logging in favor of SLF4j -->
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${org.slf4j-version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.15</version>
<exclusions>
<exclusion>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</exclusion>
<exclusion>
<groupId>javax.jms</groupId>
<artifactId>jms</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jmx</groupId>
<artifactId>jmxri</artifactId>
</exclusion>
</exclusions>
<scope>runtime</scope>
</dependency>
<!-- @Inject -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.9</version>
<configuration>
<additionalProjectnatures>
<projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
</additionalProjectnatures>
<additionalBuildcommands>
<buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
</additionalBuildcommands>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
<compilerArgument>-Xlint:all</compilerArgument>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<mainClass>org.test.int1.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
我已更新 Spring Framework、AspectJ、Jackson 和 slf4j 版本以使用最新版本。
Spring MVC 异常处理部署描述符
我们的 web.xml 文件如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="https://java.sun.com/xml/ns/javaee"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processes application requests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/spring.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<error-page>
<error-code>404</error-code>
<location>/resources/404.jsp</location>
</error-page>
</web-app>
大部分内容是为我们的 Web 应用程序插入 Spring 框架,除了为 404 错误定义的错误页面。因此,当我们的应用程序抛出 404 错误时,此页面将用作响应。当我们的 Spring Web 应用程序抛出 404 错误代码时,容器将使用此配置。
Spring 异常处理 - 模型类
我已经将 Employee bean 定义为模型类,但是我们将在应用程序中使用它只是为了在特定场景中返回有效响应。在大多数情况下,我们将故意抛出不同类型的异常。
package com.journaldev.spring.model;
public class Employee {
private String name;
private int id;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
由于我们也将返回 JSON 响应,因此让我们创建一个包含异常详细信息的 Java bean,并将其作为响应发送。
package com.journaldev.spring.model;
public class ExceptionJSONInfo {
private String url;
private String message;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Spring 异常处理 - 自定义异常类
让我们创建一个自定义异常类供我们的应用程序使用。
package com.journaldev.spring.exceptions;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="Employee Not Found") //404
public class EmployeeNotFoundException extends Exception {
private static final long serialVersionUID = -3332292346834265371L;
public EmployeeNotFoundException(int id){
super("EmployeeNotFoundException with id="+id);
}
}
请注意,我们可以使用@ResponseStatus
带有异常类的注释来定义 HTTP 代码,当我们的应用程序抛出此类异常并由我们的异常处理实现处理时,我们的应用程序将发送该代码。如您所见,我将 HTTP 状态设置为 404,并且我们为此定义了一个错误页面,因此如果我们不返回任何视图,我们的应用程序应该使用此类异常的错误页面。我们还可以在异常处理程序方法中覆盖状态代码,当我们的异常处理程序方法没有返回任何视图页面作为响应时,将其视为默认 http 状态代码。
Spring MVC 异常处理控制器类异常处理程序
让我们看看我们的控制器类,我们将在其中抛出不同类型的异常。
package com.journaldev.spring.controllers;
import java.io.IOException;
import java.sql.SQLException;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import com.journaldev.spring.exceptions.EmployeeNotFoundException;
import com.journaldev.spring.model.Employee;
import com.journaldev.spring.model.ExceptionJSONInfo;
@Controller
public class EmployeeController {
private static final Logger logger = LoggerFactory.getLogger(EmployeeController.class);
@RequestMapping(value="/emp/{id}", method=RequestMethod.GET)
public String getEmployee(@PathVariable("id") int id, Model model) throws Exception{
//deliberately throwing different types of exception
if(id==1){
throw new EmployeeNotFoundException(id);
}else if(id==2){
throw new SQLException("SQLException, id="+id);
}else if(id==3){
throw new IOException("IOException, id="+id);
}else if(id==10){
Employee emp = new Employee();
emp.setName("Pankaj");
emp.setId(id);
model.addAttribute("employee", emp);
return "home";
}else {
throw new Exception("Generic Exception, id="+id);
}
}
@ExceptionHandler(EmployeeNotFoundException.class)
public ModelAndView handleEmployeeNotFoundException(HttpServletRequest request, Exception ex){
logger.error("Requested URL="+request.getRequestURL());
logger.error("Exception Raised="+ex);
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("exception", ex);
modelAndView.addObject("url", request.getRequestURL());
modelAndView.setViewName("error");
return modelAndView;
}
}
请注意,对于 EmployeeNotFoundException 处理程序,我返回 ModelAndView,因此 http 状态代码将发送为 OK (200)。如果它返回 void,则 http 状态代码将发送为 404。我们将在全局异常处理程序实现中研究这种类型的实现。由于我在控制器中仅处理 EmployeeNotFoundException,因此我们的控制器抛出的所有其他异常都将由全局异常处理程序处理。
@ControllerAdvice和@ExceptionHandler
这是我们的全局异常处理程序控制器类。请注意,该类使用@ControllerAdvice注释进行注释。方法也使用@ExceptionHandler注释进行注释。
package com.journaldev.spring.controllers;
import java.io.IOException;
import java.sql.SQLException;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(SQLException.class)
public String handleSQLException(HttpServletRequest request, Exception ex){
logger.info("SQLException Occured:: URL="+request.getRequestURL());
return "database_error";
}
@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="IOException occured")
@ExceptionHandler(IOException.class)
public void handleIOException(){
logger.error("IOException handler executed");
//returning 404 error code
}
}
请注意,对于 SQLException,我将返回 database_error.jsp 作为响应页面,其 http 状态代码为 200。对于 IOException,我们将返回 void,其状态代码为 404,因此在这种情况下将使用我们的错误页面。如您所见,我在这里没有处理任何其他类型的异常,我将这部分留给了 HandlerExceptionResolver 实现。
异常处理程序解析器
我们只是扩展了 SimpleMappingExceptionResolver 并重写了其中一种方法,但我们可以重写它最重要的方法,resolveException
用于记录和发送不同类型的视图页面。但这与使用 ControllerAdvice 实现相同,所以我保留了它。我们将使用它来配置视图页面,以应对我们未处理的所有其他异常,方法是使用通用错误页面进行响应。
Spring 异常处理配置文件
我们的spring bean配置文件如下所示。spring.xml代码:
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="https://www.springframework.org/schema/mvc"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="https://www.springframework.org/schema/beans"
xmlns:context="https://www.springframework.org/schema/context"
xsi:schemaLocation="https://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
https://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<beans:bean id="simpleMappingExceptionResolver" class="com.journaldev.spring.resolver.MySimpleMappingExceptionResolver">
<beans:property name="exceptionMappings">
<beans:map>
<beans:entry key="Exception" value="generic_error"></beans:entry>
</beans:map>
</beans:property>
<beans:property name="defaultErrorView" value="generic_error"/>
</beans:bean>
<!-- Configure to plugin JSON as request and response in method handler -->
<beans:bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<beans:property name="messageConverters">
<beans:list>
<beans:ref bean="jsonMessageConverter"/>
</beans:list>
</beans:property>
</beans:bean>
<!-- Configure bean to convert JSON to POJO and vice versa -->
<beans:bean id="jsonMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
</beans:bean>
<context:component-scan base-package="com.journaldev.spring" />
</beans:beans>
注意在我们的 Web 应用程序中配置了支持 JSON 的 bean。与异常处理相关的唯一部分是 simpleMappingExceptionResolver bean 定义,其中我们将 generic_error.jsp 定义为 Exception 类的视图页面。这确保我们的应用程序未处理的任何异常不会导致发送服务器生成的错误页面作为响应。
Spring MVC 异常处理 JSP 视图页面
现在是时候看看我们的应用程序的最后一部分,即我们将在应用程序中使用的视图页面。home.jsp 代码:
<%@ taglib uri="https://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" %>
<html>
<head>
<title>Home</title>
</head>
<body>
<h3>Hello ${employee.name}!</h3><br>
<h4>Your ID is ${employee.id}</h4>
</body>
</html>
home.jsp 用于响应有效数据,即当我们在客户端请求中得到 id 为 10 时。404.jsp 代码:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "https://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>404 Error Page</title>
</head>
<body>
<h2>Resource Not Found Error Occured, please contact support.</h2>
</body>
</html>
404.jsp 用于生成 404 http 状态代码的视图,对于我们的实现,这应该是当我们在客户端请求中获取 id 为 3 时的响应。error.jsp 代码:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "https://www.w3.org/TR/html4/loose.dtd">
<%@ taglib uri="https://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Error Page</title>
</head>
<body>
<h2>Application Error, please contact support.</h2>
<h3>Debug Information:</h3>
Requested URL= ${url}<br><br>
Exception= ${exception.message}<br><br>
<strong>Exception Stack Trace</strong><br>
<c:forEach items="${exception.stackTrace}" var="ste">
${ste}
</c:forEach>
</body>
</html>
当我们的控制器类请求处理程序方法抛出 EmployeeNotFoundException 时,将使用 error.jsp。当客户端请求中的 id 值为 1 时,我们应该得到此页面作为响应。database_error.jsp 代码:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "https://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Database Error Page</title>
</head>
<body>
<h2>Database Error, please contact support.</h2>
</body>
</html>
当我们的应用程序抛出 SQLException 时,将使用 database_error.jsp,如 GlobalExceptionHandler 类中配置的那样。当客户端请求中的 id 值为 2 时,我们应该将此页面作为响应。generic_error.jsp 代码:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "https://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Generic Error Page</title>
</head>
<body>
<h2>Unknown Error Occured, please contact support.</h2>
</body>
</html>
当发生任何未由我们的应用程序代码处理的异常时,此页面应作为响应,simpleMappingExceptionResolver bean 会处理该异常。当客户端请求中的 id 值不是 1、2、3 或 10 时,我们应该获取此页面作为响应。
运行 Spring MVC 异常处理应用程序
只需将应用程序部署在您使用的 servlet 容器中,我在此示例中使用的是 Apache Tomcat 7。下图显示了我们的应用程序根据 id 值返回的不同响应页面。ID =10,有效响应。ID =1,使用基于控制器的异常处理程序ID=2,与视图一起使用的全局异常处理程序作为响应ID=3,使用 404 错误页面ID=4,用于响应视图的 simpleMappingExceptionResolver正如您所见,我们在所有情况下都得到了预期的响应。
Spring 异常处理程序 JSON 响应
我们的教程几乎已经完成了,除了最后一点,我将解释如何从异常处理程序方法发送 JSON 响应。我们的应用程序具有所有 JSON 依赖项,并且已配置 jsonMessageConverter,我们需要实现异常处理程序方法。为简单起见,我将重写 EmployeeController handleEmployeeNotFoundException() 方法以返回 JSON 响应。只需使用以下代码更新 EmployeeController 异常处理程序方法并再次部署应用程序。
@ExceptionHandler(EmployeeNotFoundException.class)
public @ResponseBody ExceptionJSONInfo handleEmployeeNotFoundException(HttpServletRequest request, Exception ex){
ExceptionJSONInfo response = new ExceptionJSONInfo();
response.setUrl(request.getRequestURL().toString());
response.setMessage(ex.getMessage());
return response;
}
现在,当我们在客户端请求中使用 id 作为 1 时,我们会得到以下 JSON 响应,如下图所示。这就是 Spring 异常处理和 Spring MVC 异常处理的全部内容,请从以下 URL 下载应用程序并试用它以了解更多信息。
下载 Spring 异常处理项目