Spring 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 Framework 提供了
HandlerExceptionResolver
介面,我們可以實現它來創建全局例外處理器。之所以提供此額外的定義全局例外處理器的方法是,Spring 框架還提供了我們可以在 spring bean 配置文件中定義的默認實現類,以獲取 spring 框架例外處理的好處。SimpleMappingExceptionResolver
是默認的實現類,它允許我們配置 exceptionMappings,我們可以在其中指定要為特定例外使用的資源。我們還可以覆蓋它,以創建我們自己的全局處理器,其中包含我們應用程序特定的更改,例如記錄例外消息。
讓我們創建一個 Spring MVC 專案,我們將研究基於控制器的、基於 AOP 的和基於例外處理器的例外和錯誤處理方法的實現。我們還將編寫一個例外處理器方法,它將返回 JSON 響應。如果您對 Spring 中的 JSON 響應不熟悉,請閱讀 Spring Restful JSON 教程。我們的最終項目將如下圖所示,我們將逐個查看應用程序的所有組件。
Spring Exception Handling Maven Dependencies
除了標準的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>
請注意我們網絡應用程序中支持 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>
error.jsp 在我們的控制器類請求處理程序方法拋出 EmployeeNotFoundException 時使用。當客戶端請求中的 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>
database_error.jsp 在我們的應用程序拋出 SQLException 時使用,如在 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下載應用程序並進行更多操作以進行學習。