春季MVC异常处理非常重要,以确保不向客户端发送服务器异常。今天我们将探讨使用@ExceptionHandler、@ControllerAdvice和HandlerExceptionResolver的Spring异常处理。任何Web应用程序都需要良好的异常处理设计,因为我们不希望在我们的应用程序抛出未处理的异常时提供容器生成的页面。
Spring异常处理
拥有明确定义的异常处理方法对于任何Web应用程序框架来说都是一个巨大的加分点,话虽如此,Spring MVC框架在我们的Web应用程序中处理异常和错误方面做得很好。Spring MVC框架提供以下方式来帮助我们实现强大的异常处理。
- 基于控制器的 – 我们可以在控制器类中定义异常处理方法。我们只需要用
@ExceptionHandler
注解这些方法。此注解接受异常类作为参数。因此,如果我们为异常类定义了其中之一,那么由请求处理程序方法抛出的所有异常都将被处理。这些异常处理方法就像其他请求处理程序方法一样,我们可以构建错误响应并响应不同的错误页面。我们也可以发送 JSON 错误响应,我们将在我们的示例中稍后讨论。如果定义了多个异常处理程序方法,则使用最接近异常类的处理程序方法。例如,如果我们为 IOException 和 Exception 定义了两个处理程序方法,并且我们的请求处理程序方法引发了 IOException,则将执行 IOException 的处理程序方法。 - 全局异常处理程序 – 异常处理是一个横切关注点,它应该为我们应用程序中的所有切入点进行处理。我们已经了解了Spring AOP,这就是为什么 Spring 提供了
@ControllerAdvice
注解,我们可以将其与任何类一起使用来定义我们的全局异常处理程序。全局控制器建议中的处理程序方法与基于控制器的异常处理程序方法相同,并且在控制器类无法处理异常时使用。 - HandlerExceptionResolver – 对于一般的异常,大多数情况下我们会提供静态页面。Spring框架提供了
HandlerExceptionResolver
接口,我们可以实现该接口来创建全局异常处理器。这种额外定义全局异常处理器的原因是,Spring框架还提供了默认实现类,我们可以在Spring Bean配置文件中定义这些类,以获得Spring框架的异常处理优势。SimpleMappingExceptionResolver
是默认的实现类,它允许我们配置exceptionMappings,我们可以指定哪个资源用于处理特定异常。我们还可以重写它,以创建我们自己的全局处理器,带有我们应用程序特定的更改,例如记录异常消息。
让我们创建一个Spring MVC项目,我们将研究基于控制器的、基于AOP的和基于异常解析器的异常和错误处理方法的实现。我们还将编写一个异常处理器方法,该方法将返回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>
I have updated Spring Framework, AspectJ, Jackson and slf4j versions to use the latest one.
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>
大部分内容用于将Spring框架集成到我们的Web应用程序中,除了为404错误定义的error-page。因此,当我们的应用程序抛出404错误时,将使用此页面作为响应。此配置由容器在我们的Spring Web应用程序抛出404错误代码时使用。
Spring异常处理 – 模型类
I have defined Employee bean as model class, however we will be using it in our application just to return valid response in specific scenario. We will be deliberately throwing different types of exceptions in most of the cases.
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{
//故意引发不同类型的异常
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");
//返回404错误代码
}
}
请注意,对于 SQLException,我将返回 database_error.jsp 作为响应页面,并将 HTTP 状态码设置为 200。对于 IOException,我们将返回 void,并将状态码设置为 404,因此在这种情况下将使用我们的错误页面。正如您所看到的,我在这里没有处理任何其他类型的异常,那部分我留给了 HandlerExceptionResolver 实现。
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的Beans。与异常处理相关的唯一部分是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下载应用程序并进行更多的实践学习。