Spring MVC异常处理– @ ControllerAdvice,@ ExceptionHandler,HandlerExceptionResolver

时间:2020-02-23 14:35:54  来源:igfitidea点击:

Spring MVC异常处理对于确保不向客户端发送服务器异常非常重要。
今天,我们将研究使用@ ExceptionHandler,@ ControllerAdvice和HandlerExceptionResolver的Spring异常处理。
任何Web应用程序都需要对异常处理进行良好的设计,因为当我们的应用程序抛出任何未处理的异常时,我们不想提供容器生成的页面。

Spring异常处理

对于任何Web应用程序框架而言,拥有定义良好的异常处理方法都是一个巨大的优势,也就是说,Spring MVC框架在处理Web应用程序中的异常和错误处理方面提供了很好的支持。

Spring MVC Framework提供了以下方法来帮助我们实现可靠的异常处理。

  • 基于控制器–我们可以在控制器类中定义异常处理程序方法。
    我们需要的是用@ExceptionHandler注释对这些方法进行注释。
    该注释将Exception类作为参数。
    因此,如果我们为Exception类定义了其中之一,则请求处理程序方法抛出的所有异常都将得到处理,这些异常处理程序方法与其他请求处理程序方法一样,可以构建错误响应并在不同的错误页面进行响应。
    我们还可以发送JSON错误响应,我们将在示例中稍后进行介绍。
    如果定义了多个异常处理程序方法,则使用最接近Exception类的处理程序方法。
    例如,如果我们为IOException和Exception定义了两个处理程序方法,并且我们的请求处理程序方法抛出IOException,则将执行IOException的处理程序方法。

  • 全局异常处理程序–异常处理是一个跨领域的问题,应该对我们应用程序中的所有切入点进行处理。
    我们已经研究过Spring AOP,这就是为什么Spring提供了@ControllerAdvice批注,我们可以将其与任何类一起使用来定义全局异常处理程序.Global Controller Advice中的处理程序方法与基于Controller的异常处理程序方法相同,并且在controller时使用类无法处理异常。

  • HandlerExceptionResolver –对于通用异常,大多数时候我们提供静态页面。
    Spring框架提供了" HandlerExceptionResolver"接口,我们可以通过该接口来创建全局异常处理程序。
    这种另外的定义全局异常处理程序的方法的原因是,Spring框架还提供了默认实现类,我们可以在我们的Spring bean配置文件中定义这些默认实现类,以获取Spring框架异常处理的好处。
    配置exceptionMappings,我们可以其中指定用于特定异常的资源。
    我们还可以覆盖它,以使用我们特定于应用程序的更改(例如,记录异常消息)来创建自己的全局处理程序。

让我们创建一个Spring MVC项目,其中我们将研究基于控制器,基于AOP和基于异常解析器的异常和错误处理方法的实现。
我们还将编写一个异常处理程序方法,该方法将返回JSON响应。
如果您不熟悉Spring的JSON,请阅读Spring Restful JSON教程。

我们的最终项目将如下图所示,我们将逐一查看应用程序的所有组件。

Spring异常处理Maven依赖项

除了标准的Spring MVC依赖关系之外,我们还需要Hymanson依赖关系来支持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.theitroad.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>
		<Hymanson.databind-version>2.2.3</Hymanson.databind-version>
	</properties>
	<dependencies>
		<!-- Hymanson -->
		<dependency>
			<groupId>com.fasterxml.Hymanson.core</groupId>
			<artifactId>Hymanson-databind</artifactId>
			<version>${Hymanson.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,Hymanson和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>

除了为404错误定义的错误页面外,大部分内容都是用于为我们的Web应用程序插入Spring Framework。
因此,当我们的应用程序抛出404错误时,此页面将用作响应。
当我们的Spring Web应用程序抛出404错误代码时,容器将使用此配置。

Spring异常处理–模型类

我已经将Employee bean定义为模型类,但是我们将在应用程序中使用它只是为了在特定情况下返回有效响应。
在大多数情况下,我们将故意抛出不同类型的异常。

package com.theitroad.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.theitroad.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.theitroad.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.theitroad.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.theitroad.spring.exceptions.EmployeeNotFoundException;
import com.theitroad.spring.model.Employee;
import com.theitroad.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("hyman");
			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.theitroad.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,我将返回http_状态代码为200的database_error.jsp作为响应页面。

对于IOException,我们返回的状态代码为404的void,因此在这种情况下将使用错误页面。

如您所见,我在这里不处理任何其他类型的异常,这部分我留给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.theitroad.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.MappingHymanson2HttpMessageConverter">
	</beans:bean>
	
	<context:component-scan base-package="com.theitroad.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>

Exception= ${exception.message}<br>

Exception Stack Trace<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>

当我们的应用程序抛出GlobalExceptionHandler类中配置的SQLException时,将使用database_error.jsp。
当客户端请求中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;
	}