Reading Time: 22 minutes

Trong thế giới phát triển phần mềm, việc xử lý ngoại lệ (Exception Handling) không chỉ là một yêu cầu kỹ thuật mà còn là một yếu tố then chốt quyết định chất lượng và sự ổn định của ứng dụng.

Bài blog hôm nay sẽ đi sâu vào các phương pháp xử lý ngoại lệ mạnh mẽ mà Spring MVC cung cấp, bao gồm @ExceptionHandler, @ControllerAdviceHandlerExceptionResolver. Chúng ta sẽ cùng nhau khám phá cách sử dụng những công cụ này để xây dựng một hệ thống xử lý lỗi linh hoạt, có khả năng phục hồi cao, đảm bảo rằng mọi ngoại lệ được quản lý một cách có trật tự.

Xử lý ngoại lệ trong Spring Framework

Có một cách tiếp cận xử lý ngoại lệ được định nghĩa rõ ràng là một điểm cộng rất lớn cho bất kỳ framework ứng dụng web nào. Spring MVC Framework thực hiện rất tốt điều này bằng việc cung cấp các cách sau để giúp chúng ta đạt được khả năng xử lý ngoại lệ hiệu quả:

  • Xử lý Ngoại lệ theo Controller (Controller Based): Chúng ta có thể định nghĩa các phương thức xử lý ngoại lệ ngay trong các lớp Controller của mình bằng cách sử dụng annotation @ExceptionHandler. Annotation này nhận lớp Exception làm đối số. Các phương thức xử lý ngoại lệ này cũng giống như các phương thức xử lý yêu cầu khác và chúng ta có thể xây dựng phản hồi lỗi và trả về một trang lỗi khác. Chúng ta cũng có thể gửi phản hồi lỗi dưới dạng JSON, điều này sẽ được xem xét sau trong ví dụ của chúng ta. Nếu có nhiều phương thức xử lý ngoại lệ được định nghĩa, thì phương thức xử lý gần nhất với lớp Exception sẽ được sử dụng. Ví dụ, nếu có handler cho IOExceptionException, khi IOException được ném ra, thì phương thức xử lý cho IOException sẽ được thực thi.
  • Xử lý Ngoại lệ toàn cục (Global Exception Handler): Xử lý ngoại lệ là một vấn đề xuyên suốt (cross-cutting concern), nó nên được thực hiện cho tất cả các điểm cắt (pointcuts) trong ứng dụng của chúng ta. Chúng ta đã từng tìm hiểu về Spring AOP và đó là lý do tại sao Spring cung cấp @ControllerAdvice. Annotation này có thể được sử dụng với bất kỳ lớp nào để định nghĩa bộ xử lý ngoại lệ toàn cục của chúng ta. Các phương thức xử lý trong Global Controller Advice giống như các phương thức xử lý ngoại lệ theo Controller và được sử dụng khi lớp Controller không thể xử lý ngoại lệ.
  • HandlerExceptionResolver: Đối với các ngoại lệ chung, thông thường ta sẽ hiển thị các trang tĩnh thông báo lỗi. Spring Framework cung cấp giao diện HandlerExceptionResolver mà chúng ta có thể triển khai để tạo bộ xử lý ngoại lệ toàn cục. Lý do đằng sau cách bổ sung này để định nghĩa bộ xử lý ngoại lệ toàn cục là Spring framework cũng cung cấp các lớp triển khai mặc định mà chúng ta có thể định nghĩa trong tệp cấu hình bean của Spring để nhận các lợi ích xử lý ngoại lệ của Spring framework. SimpleMappingExceptionResolver là lớp triển khai mặc định, nó cho phép chúng ta cấu hình exceptionMappings nơi chúng ta có thể chỉ định tài nguyên nào sẽ sử dụng cho một ngoại lệ cụ thể. Chúng ta cũng có thể ghi đè nó để tạo bộ xử lý toàn cục của riêng mình với các thay đổi dành riêng cho ứng dụng của chúng ta, chẳng hạn như ghi log các thông báo ngoại lệ.

Để minh họa rõ hơn, chúng ta sẽ xây dựng một dự án Spring MVC nhỏ để xem xét cách triển khai các phương pháp xử lý ngoại lệ và lỗi dựa trên Controller, dựa trên @ControllerAdvice (AOP Based), và dựa trên HandlerExceptionResolver. Chúng ta cũng sẽ viết một phương thức xử lý ngoại lệ trả về phản hồi JSON. Nếu bạn mới làm quen với JSON trong Spring, hãy đọc Spring Restful JSON Tutorial. Cấu trúc dự án của chúng ta sẽ trông như hình minh họa, chúng ta sẽ xem xét tất cả các thành phần của ứng dụng từng cái một.

Spring Exception Handling Maven Dependencies

Ngoài các dependencies Spring MVC tiêu chuẩn, chúng ta cũng sẽ cần dependency Jackson JSON để hỗ trợ JSON. Tệp pom.xml cuối cùng của chúng ta trông như sau:

<?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>

Lưu ý rằng tôi đã cập nhật các phiên bản của Spring Framework, AspectJ, Jackson và slf4j để sử dụng các phiên bản mới nhất tại thời điểm viết bài gốc.

Spring MVC Exception Handling Deployment Descriptor

Tệp web.xml của chúng ta trông như sau:

<?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>

Phần lớn tệp này dùng để tích hợp Spring Framework vào ứng dụng web của chúng ta, ngoại trừ thẻ <error-page> được định nghĩa cho lỗi 404. Cấu hình này sẽ được container sử dụng khi ứng dụng web Spring của chúng ta trả về mã lỗi 404, đảm bảo rằng thay vì hiển thị trang lỗi mặc định của máy chủ, một trang tùy chỉnh /resources/404.jsp sẽ được sử dụng làm phản hồi.

Spring Exception Handling – Model Classes

Tôi đã định nghĩa bean Employee làm lớp model, mặc dù chúng ta sẽ chỉ sử dụng nó trong ứng dụng để trả về phản hồi hợp lệ trong một kịch bản cụ thể. Trong hầu hết các trường hợp, chúng ta sẽ cố ý ném ra các loại ngoại lệ khác nhau.

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;
	}
}

Vì chúng ta cũng sẽ trả về phản hồi JSON, hãy tạo một Java bean với các chi tiết ngoại lệ sẽ được gửi làm phản hồi.

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 String.format("Error: %s", message); // Thêm format để rõ ràng hơn
	}
	public void setMessage(String message) {
		this.message = message;
	}
}

Spring Exception Handling – Custom Exception Class

Hãy tạo một lớp ngoại lệ tùy chỉnh để ứng dụng của chúng ta sử dụng.

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);
	}
}

Lưu ý: Chúng ta có thể sử dụng @ResponseStatus với các lớp ngoại lệ để định nghĩa mã HTTP mà ứng dụng sẽ trả về khi loại exception đó được ném ra. Như bạn có thể thấy, tôi đang đặt trạng thái HTTP là 404 và một trang lỗi được định nghĩa cho điều này (404.jsp), vì vậy ứng dụng của chúng ta sẽ sử dụng trang lỗi này nếu chúng ta không trả về bất kỳ view nào. Chúng ta cũng có thể ghi đè mã trạng thái trong phương thức xử lý ngoại lệ của mình, hãy coi đây là mã trạng thái HTTP mặc định khi phương thức xử lý ngoại lệ của chúng ta không trả về trang view nào làm phản hồi.

Spring MVC Exception Handling Controller Class Exception Handler

Hãy xem lớp Controller của chúng ta, nơi chúng ta sẽ ném ra các loại ngoại lệ khác nhau.

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{
		//cố ý ném ra các loại ngoại lệ khác nhau
		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;
	}
}

Lưu ý: Đối với trình xử lý EmployeeNotFoundException, tôi đang trả về ModelAndView và do đó mã trạng thái HTTP sẽ được gửi là OK (200). Nếu nó trả về void, thì mã trạng thái HTTP sẽ được gửi là 404. Chúng ta sẽ xem xét loại triển khai này trong triển khai trình xử lý ngoại lệ toàn cục của chúng ta. Vì tôi chỉ xử lý EmployeeNotFoundException trong controller này, tất cả các ngoại lệ khác được ném ra bởi controller của chúng ta sẽ được xử lý bởi trình xử lý ngoại lệ toàn cục.

@ControllerAdvice@ExceptionHandler

Đây là lớp Controller xử lý ngoại lệ toàn cục của chúng ta. Lưu ý rằng lớp được đánh dấu bằng annotation @ControllerAdvice. Ngoài ra, các phương thức được đánh dấu bằng annotation @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
	}
}

Lưu ý: Đối với SQLException, tôi đang trả về database_error.jsp làm trang phản hồi với mã trạng thái HTTP là 200. Đối với IOException, chúng ta đang trả về void với mã trạng thái là 404, vì vậy trang error-page của chúng ta sẽ được sử dụng trong trường hợp này. Như bạn có thể thấy, tôi không xử lý bất kỳ loại ngoại lệ nào khác ở đây, phần đó tôi đã dành cho việc triển khai HandlerExceptionResolver.

HandlerExceptionResolver

Chúng ta chỉ đơn giản là mở rộng SimpleMappingExceptionResolver và ghi đè một trong các phương thức, nhưng chúng ta có thể ghi đè phương thức quan trọng nhất của nó là resolveException để ghi log và gửi các loại trang view khác nhau. Nhưng điều đó cũng giống như việc sử dụng triển khai @ControllerAdvice, vì vậy tôi sẽ bỏ qua nó. Chúng ta sẽ sử dụng nó để cấu hình trang view cho tất cả các ngoại lệ khác không được chúng ta xử lý bằng cách phản hồi bằng một trang lỗi chung.

Spring Exception Handling Configuration File

Tệp cấu hình Spring bean của chúng ta trông như sau. Mã 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="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
		<beans:property name="exceptionMappings">
			<beans:map>
				<beans:entry key="java.lang.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>

Lưu ý: Các bean được cấu hình để hỗ trợ JSON trong ứng dụng web của chúng ta. Phần duy nhất liên quan đến xử lý ngoại lệ là định nghĩa bean simpleMappingExceptionResolver nơi chúng ta định nghĩa generic_error.jsp làm trang view cho lớp Exception. Điều này đảm bảo rằng bất kỳ ngoại lệ nào không được ứng dụng của chúng ta xử lý sẽ không dẫn đến việc gửi trang lỗi do máy chủ tạo ra làm phản hồi.

Spring MVC Exception Handling JSP View Pages

Đã đến lúc xem xét phần cuối cùng của ứng dụng của chúng ta, các trang view sẽ được sử dụng. Mã 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 được sử dụng để phản hồi với dữ liệu hợp lệ, tức là khi chúng ta nhận được id là 10 trong yêu cầu của client. Mã 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 được sử dụng để tạo view cho mã trạng thái HTTP 404. Đối với việc triển khai của chúng ta, đây phải là phản hồi khi chúng ta nhận được id là 3 trong yêu cầu của client (do IOException được xử lý bởi @ControllerAdvice và trả về void với @ResponseStatus(HttpStatus.NOT_FOUND)). Mã 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}<br>
</c:forEach>

</body>
</html>

error.jsp được sử dụng khi phương thức xử lý yêu cầu của lớp Controller của chúng ta ném ra EmployeeNotFoundException (trường hợp id=1). Chúng ta sẽ nhận được trang này làm phản hồi. Mã 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 được sử dụng khi ứng dụng của chúng ta ném ra SQLException, như đã cấu hình trong lớp GlobalExceptionHandler (trường hợp id=2). Chúng ta sẽ nhận được trang này làm phản hồi. Mã 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>

Đây sẽ là trang phản hồi khi bất kỳ ngoại lệ nào xảy ra mà không được code ứng dụng của chúng ta xử lý và bean simpleMappingExceptionResolver sẽ đảm nhiệm việc đó. Chúng ta sẽ nhận được trang này làm phản hồi khi giá trị id trong yêu cầu của client là bất cứ thứ gì khác ngoài 1, 2, 3 hoặc 10.

Chạy ứng dụng Spring MVC Exception Handling

Chỉ cần triển khai ứng dụng vào servlet container mà bạn đang sử dụng. Tôi đang dùng Apache Tomcat 7 cho ví dụ này. Dưới đây là các kết quả phản hồi dự kiến từ ứng dụng của chúng ta dựa trên giá trị id:

  • ID=10: Phản hồi hợp lệ, hiển thị trang home.jsp với thông tin nhân viên.
  • ID=1: Trình xử lý ngoại lệ dựa trên controller (@ExceptionHandler trong EmployeeController) được sử dụng, hiển thị trang error.jsp.
  • ID=2: Trình xử lý ngoại lệ toàn cục (@ControllerAdvice cho SQLException) được sử dụng với view là phản hồi, hiển thị trang database_error.jsp.
  • ID=3: Lỗi 404 được trả về (do @ControllerAdvice cho IOException trả về void@ResponseStatus(HttpStatus.NOT_FOUND)), hiển thị trang 404.jsp (cấu hình trong web.xml).
  • ID=4 (hoặc bất kỳ số nào khác không được xử lý): SimpleMappingExceptionResolver được sử dụng cho trang phản hồi, hiển thị trang generic_error.jsp.

Như bạn có thể thấy, chúng ta đã nhận được phản hồi mong đợi trong tất cả các trường hợp.

Phản hồi JSON từ Spring Exception Handler

Chúng ta gần như hoàn tất hướng dẫn của mình, ngoại trừ phần cuối cùng mà tôi sẽ giải thích cách gửi phản hồi JSON từ các phương thức xử lý ngoại lệ. Ứng dụng của chúng ta đã có tất cả các dependencies JSON và jsonMessageConverter đã được cấu hình, tất cả những gì chúng ta cần là triển khai phương thức xử lý ngoại lệ. Để đơn giản, tôi sẽ viết lại phương thức handleEmployeeNotFoundException() của EmployeeController để trả về phản hồi JSON. Chỉ cần cập nhật phương thức xử lý ngoại lệ của EmployeeController bằng đoạn code dưới đây và triển khai lại ứng dụng.

@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;
	}

Bây giờ, khi chúng ta sử dụng id là 1 trong yêu cầu của client, chúng ta sẽ nhận được phản hồi JSON sau:

{
  "url": "<http://localhost:8080/SpringExceptionHandling/emp/1>",
  "message": "Error: EmployeeNotFoundException with id=1"
}

Kết luận

Trong bài viết này, chúng ta đã khám phá chi tiết các chiến lược xử lý ngoại lệ trong Spring MVC, từ cách xử lý cục bộ bằng @ExceptionHandler ngay trong Controller, đến việc triển khai các bộ xử lý lỗi toàn cục thông qua @ControllerAdvice và cuối cùng là tận dụng HandlerExceptionResolver để quản lý các ngoại lệ chung một cách hiệu quả. Mỗi phương pháp đều có ưu điểm và kịch bản sử dụng riêng, cho phép chúng ta linh hoạt kiểm soát luồng lỗi và cung cấp phản hồi phù hợp cho người dùng, dù đó là một trang lỗi HTML tùy chỉnh hay một đối tượng JSON chi tiết.

Hãy thực hành và thử nghiệm với các ví dụ code trong bài để hiểu rõ hơn về cách các cơ chế này hoạt động.

Trong thế giới phát triển phần mềm, việc xử lý ngoại lệ (Exception Handling) không chỉ là một yêu cầu kỹ thuật mà còn là một yếu tố then chốt quyết định chất lượng và sự ổn định của ứng dụng.

Bài blog hôm nay sẽ đi sâu vào các phương pháp xử lý ngoại lệ mạnh mẽ mà Spring MVC cung cấp, bao gồm @ExceptionHandler, @ControllerAdviceHandlerExceptionResolver. Chúng ta sẽ cùng nhau khám phá cách sử dụng những công cụ này để xây dựng một hệ thống xử lý lỗi linh hoạt, có khả năng phục hồi cao, đảm bảo rằng mọi ngoại lệ được quản lý một cách có trật tự.

Xử lý ngoại lệ trong Spring Framework

Có một cách tiếp cận xử lý ngoại lệ được định nghĩa rõ ràng là một điểm cộng rất lớn cho bất kỳ framework ứng dụng web nào. Spring MVC Framework thực hiện rất tốt điều này bằng việc cung cấp các cách sau để giúp chúng ta đạt được khả năng xử lý ngoại lệ hiệu quả:

  • Xử lý Ngoại lệ theo Controller (Controller Based): Chúng ta có thể định nghĩa các phương thức xử lý ngoại lệ ngay trong các lớp Controller của mình bằng cách sử dụng annotation @ExceptionHandler. Annotation này nhận lớp Exception làm đối số. Các phương thức xử lý ngoại lệ này cũng giống như các phương thức xử lý yêu cầu khác và chúng ta có thể xây dựng phản hồi lỗi và trả về một trang lỗi khác. Chúng ta cũng có thể gửi phản hồi lỗi dưới dạng JSON, điều này sẽ được xem xét sau trong ví dụ của chúng ta. Nếu có nhiều phương thức xử lý ngoại lệ được định nghĩa, thì phương thức xử lý gần nhất với lớp Exception sẽ được sử dụng. Ví dụ, nếu có handler cho IOExceptionException, khi IOException được ném ra, thì phương thức xử lý cho IOException sẽ được thực thi.
  • Xử lý Ngoại lệ toàn cục (Global Exception Handler): Xử lý ngoại lệ là một vấn đề xuyên suốt (cross-cutting concern), nó nên được thực hiện cho tất cả các điểm cắt (pointcuts) trong ứng dụng của chúng ta. Chúng ta đã từng tìm hiểu về Spring AOP và đó là lý do tại sao Spring cung cấp @ControllerAdvice. Annotation này có thể được sử dụng với bất kỳ lớp nào để định nghĩa bộ xử lý ngoại lệ toàn cục của chúng ta. Các phương thức xử lý trong Global Controller Advice giống như các phương thức xử lý ngoại lệ theo Controller và được sử dụng khi lớp Controller không thể xử lý ngoại lệ.
  • HandlerExceptionResolver: Đối với các ngoại lệ chung, thông thường ta sẽ hiển thị các trang tĩnh thông báo lỗi. Spring Framework cung cấp giao diện HandlerExceptionResolver mà chúng ta có thể triển khai để tạo bộ xử lý ngoại lệ toàn cục. Lý do đằng sau cách bổ sung này để định nghĩa bộ xử lý ngoại lệ toàn cục là Spring framework cũng cung cấp các lớp triển khai mặc định mà chúng ta có thể định nghĩa trong tệp cấu hình bean của Spring để nhận các lợi ích xử lý ngoại lệ của Spring framework. SimpleMappingExceptionResolver là lớp triển khai mặc định, nó cho phép chúng ta cấu hình exceptionMappings nơi chúng ta có thể chỉ định tài nguyên nào sẽ sử dụng cho một ngoại lệ cụ thể. Chúng ta cũng có thể ghi đè nó để tạo bộ xử lý toàn cục của riêng mình với các thay đổi dành riêng cho ứng dụng của chúng ta, chẳng hạn như ghi log các thông báo ngoại lệ.

Để minh họa rõ hơn, chúng ta sẽ xây dựng một dự án Spring MVC nhỏ để xem xét cách triển khai các phương pháp xử lý ngoại lệ và lỗi dựa trên Controller, dựa trên @ControllerAdvice (AOP Based), và dựa trên HandlerExceptionResolver. Chúng ta cũng sẽ viết một phương thức xử lý ngoại lệ trả về phản hồi JSON. Nếu bạn mới làm quen với JSON trong Spring, hãy đọc Spring Restful JSON Tutorial. Cấu trúc dự án của chúng ta sẽ trông như hình minh họa, chúng ta sẽ xem xét tất cả các thành phần của ứng dụng từng cái một.

Spring Exception Handling Maven Dependencies

Ngoài các dependencies Spring MVC tiêu chuẩn, chúng ta cũng sẽ cần dependency Jackson JSON để hỗ trợ JSON. Tệp pom.xml cuối cùng của chúng ta trông như sau:

<?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>

Lưu ý rằng tôi đã cập nhật các phiên bản của Spring Framework, AspectJ, Jackson và slf4j để sử dụng các phiên bản mới nhất tại thời điểm viết bài gốc.

Spring MVC Exception Handling Deployment Descriptor

Tệp web.xml của chúng ta trông như sau:

<?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>

Phần lớn tệp này dùng để tích hợp Spring Framework vào ứng dụng web của chúng ta, ngoại trừ thẻ <error-page> được định nghĩa cho lỗi 404. Cấu hình này sẽ được container sử dụng khi ứng dụng web Spring của chúng ta trả về mã lỗi 404, đảm bảo rằng thay vì hiển thị trang lỗi mặc định của máy chủ, một trang tùy chỉnh /resources/404.jsp sẽ được sử dụng làm phản hồi.

Spring Exception Handling – Model Classes

Tôi đã định nghĩa bean Employee làm lớp model, mặc dù chúng ta sẽ chỉ sử dụng nó trong ứng dụng để trả về phản hồi hợp lệ trong một kịch bản cụ thể. Trong hầu hết các trường hợp, chúng ta sẽ cố ý ném ra các loại ngoại lệ khác nhau.

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;
	}
}

Vì chúng ta cũng sẽ trả về phản hồi JSON, hãy tạo một Java bean với các chi tiết ngoại lệ sẽ được gửi làm phản hồi.

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 String.format("Error: %s", message); // Thêm format để rõ ràng hơn
	}
	public void setMessage(String message) {
		this.message = message;
	}
}

Spring Exception Handling – Custom Exception Class

Hãy tạo một lớp ngoại lệ tùy chỉnh để ứng dụng của chúng ta sử dụng.

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);
	}
}

Lưu ý: Chúng ta có thể sử dụng @ResponseStatus với các lớp ngoại lệ để định nghĩa mã HTTP mà ứng dụng sẽ trả về khi loại exception đó được ném ra. Như bạn có thể thấy, tôi đang đặt trạng thái HTTP là 404 và một trang lỗi được định nghĩa cho điều này (404.jsp), vì vậy ứng dụng của chúng ta sẽ sử dụng trang lỗi này nếu chúng ta không trả về bất kỳ view nào. Chúng ta cũng có thể ghi đè mã trạng thái trong phương thức xử lý ngoại lệ của mình, hãy coi đây là mã trạng thái HTTP mặc định khi phương thức xử lý ngoại lệ của chúng ta không trả về trang view nào làm phản hồi.

Spring MVC Exception Handling Controller Class Exception Handler

Hãy xem lớp Controller của chúng ta, nơi chúng ta sẽ ném ra các loại ngoại lệ khác nhau.

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{
		//cố ý ném ra các loại ngoại lệ khác nhau
		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;
	}
}

Lưu ý: Đối với trình xử lý EmployeeNotFoundException, tôi đang trả về ModelAndView và do đó mã trạng thái HTTP sẽ được gửi là OK (200). Nếu nó trả về void, thì mã trạng thái HTTP sẽ được gửi là 404. Chúng ta sẽ xem xét loại triển khai này trong triển khai trình xử lý ngoại lệ toàn cục của chúng ta. Vì tôi chỉ xử lý EmployeeNotFoundException trong controller này, tất cả các ngoại lệ khác được ném ra bởi controller của chúng ta sẽ được xử lý bởi trình xử lý ngoại lệ toàn cục.

@ControllerAdvice@ExceptionHandler

Đây là lớp Controller xử lý ngoại lệ toàn cục của chúng ta. Lưu ý rằng lớp được đánh dấu bằng annotation @ControllerAdvice. Ngoài ra, các phương thức được đánh dấu bằng annotation @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
	}
}

Lưu ý: Đối với SQLException, tôi đang trả về database_error.jsp làm trang phản hồi với mã trạng thái HTTP là 200. Đối với IOException, chúng ta đang trả về void với mã trạng thái là 404, vì vậy trang error-page của chúng ta sẽ được sử dụng trong trường hợp này. Như bạn có thể thấy, tôi không xử lý bất kỳ loại ngoại lệ nào khác ở đây, phần đó tôi đã dành cho việc triển khai HandlerExceptionResolver.

HandlerExceptionResolver

Chúng ta chỉ đơn giản là mở rộng SimpleMappingExceptionResolver và ghi đè một trong các phương thức, nhưng chúng ta có thể ghi đè phương thức quan trọng nhất của nó là resolveException để ghi log và gửi các loại trang view khác nhau. Nhưng điều đó cũng giống như việc sử dụng triển khai @ControllerAdvice, vì vậy tôi sẽ bỏ qua nó. Chúng ta sẽ sử dụng nó để cấu hình trang view cho tất cả các ngoại lệ khác không được chúng ta xử lý bằng cách phản hồi bằng một trang lỗi chung.

Spring Exception Handling Configuration File

Tệp cấu hình Spring bean của chúng ta trông như sau. Mã 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="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
		<beans:property name="exceptionMappings">
			<beans:map>
				<beans:entry key="java.lang.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>

Lưu ý: Các bean được cấu hình để hỗ trợ JSON trong ứng dụng web của chúng ta. Phần duy nhất liên quan đến xử lý ngoại lệ là định nghĩa bean simpleMappingExceptionResolver nơi chúng ta định nghĩa generic_error.jsp làm trang view cho lớp Exception. Điều này đảm bảo rằng bất kỳ ngoại lệ nào không được ứng dụng của chúng ta xử lý sẽ không dẫn đến việc gửi trang lỗi do máy chủ tạo ra làm phản hồi.

Spring MVC Exception Handling JSP View Pages

Đã đến lúc xem xét phần cuối cùng của ứng dụng của chúng ta, các trang view sẽ được sử dụng. Mã 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 được sử dụng để phản hồi với dữ liệu hợp lệ, tức là khi chúng ta nhận được id là 10 trong yêu cầu của client. Mã 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 được sử dụng để tạo view cho mã trạng thái HTTP 404. Đối với việc triển khai của chúng ta, đây phải là phản hồi khi chúng ta nhận được id là 3 trong yêu cầu của client (do IOException được xử lý bởi @ControllerAdvice và trả về void với @ResponseStatus(HttpStatus.NOT_FOUND)). Mã 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}<br>
</c:forEach>

</body>
</html>

error.jsp được sử dụng khi phương thức xử lý yêu cầu của lớp Controller của chúng ta ném ra EmployeeNotFoundException (trường hợp id=1). Chúng ta sẽ nhận được trang này làm phản hồi. Mã 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 được sử dụng khi ứng dụng của chúng ta ném ra SQLException, như đã cấu hình trong lớp GlobalExceptionHandler (trường hợp id=2). Chúng ta sẽ nhận được trang này làm phản hồi. Mã 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>

Đây sẽ là trang phản hồi khi bất kỳ ngoại lệ nào xảy ra mà không được code ứng dụng của chúng ta xử lý và bean simpleMappingExceptionResolver sẽ đảm nhiệm việc đó. Chúng ta sẽ nhận được trang này làm phản hồi khi giá trị id trong yêu cầu của client là bất cứ thứ gì khác ngoài 1, 2, 3 hoặc 10.

Chạy ứng dụng Spring MVC Exception Handling

Chỉ cần triển khai ứng dụng vào servlet container mà bạn đang sử dụng. Tôi đang dùng Apache Tomcat 7 cho ví dụ này. Dưới đây là các kết quả phản hồi dự kiến từ ứng dụng của chúng ta dựa trên giá trị id:

  • ID=10: Phản hồi hợp lệ, hiển thị trang home.jsp với thông tin nhân viên.
  • ID=1: Trình xử lý ngoại lệ dựa trên controller (@ExceptionHandler trong EmployeeController) được sử dụng, hiển thị trang error.jsp.
  • ID=2: Trình xử lý ngoại lệ toàn cục (@ControllerAdvice cho SQLException) được sử dụng với view là phản hồi, hiển thị trang database_error.jsp.
  • ID=3: Lỗi 404 được trả về (do @ControllerAdvice cho IOException trả về void@ResponseStatus(HttpStatus.NOT_FOUND)), hiển thị trang 404.jsp (cấu hình trong web.xml).
  • ID=4 (hoặc bất kỳ số nào khác không được xử lý): SimpleMappingExceptionResolver được sử dụng cho trang phản hồi, hiển thị trang generic_error.jsp.

Như bạn có thể thấy, chúng ta đã nhận được phản hồi mong đợi trong tất cả các trường hợp.

Phản hồi JSON từ Spring Exception Handler

Chúng ta gần như hoàn tất hướng dẫn của mình, ngoại trừ phần cuối cùng mà tôi sẽ giải thích cách gửi phản hồi JSON từ các phương thức xử lý ngoại lệ. Ứng dụng của chúng ta đã có tất cả các dependencies JSON và jsonMessageConverter đã được cấu hình, tất cả những gì chúng ta cần là triển khai phương thức xử lý ngoại lệ. Để đơn giản, tôi sẽ viết lại phương thức handleEmployeeNotFoundException() của EmployeeController để trả về phản hồi JSON. Chỉ cần cập nhật phương thức xử lý ngoại lệ của EmployeeController bằng đoạn code dưới đây và triển khai lại ứng dụng.

@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;
	}

Bây giờ, khi chúng ta sử dụng id là 1 trong yêu cầu của client, chúng ta sẽ nhận được phản hồi JSON sau:

{
  "url": "<http://localhost:8080/SpringExceptionHandling/emp/1>",
  "message": "Error: EmployeeNotFoundException with id=1"
}

Kết luận

Trong bài viết này, chúng ta đã khám phá chi tiết các chiến lược xử lý ngoại lệ trong Spring MVC, từ cách xử lý cục bộ bằng @ExceptionHandler ngay trong Controller, đến việc triển khai các bộ xử lý lỗi toàn cục thông qua @ControllerAdvice và cuối cùng là tận dụng HandlerExceptionResolver để quản lý các ngoại lệ chung một cách hiệu quả. Mỗi phương pháp đều có ưu điểm và kịch bản sử dụng riêng, cho phép chúng ta linh hoạt kiểm soát luồng lỗi và cung cấp phản hồi phù hợp cho người dùng, dù đó là một trang lỗi HTML tùy chỉnh hay một đối tượng JSON chi tiết.

Hãy thực hành và thử nghiệm với các ví dụ code trong bài để hiểu rõ hơn về cách các cơ chế này hoạt động.

0 Bình luận

Đăng nhập để thảo luận

Chuyên mục Hướng dẫn

Tổng hợp các bài viết hướng dẫn, nghiên cứu và phân tích chi tiết về kỹ thuật, các xu hướng công nghệ mới nhất dành cho lập trình viên.

Đăng ký nhận bản tin của chúng tôi

Hãy trở thành người nhận được các nội dung hữu ích của CyStack sớm nhất

Xem chính sách của chúng tôi Chính sách bảo mật.

Đăng ký nhận Newsletter

Nhận các nội dung hữu ích mới nhất