El manejo de excepciones de Spring MVC es muy importante para asegurarse de no enviar excepciones del servidor al cliente. Hoy veremos el manejo de excepciones de Spring usando @ExceptionHandler , @ControllerAdvice y HandlerExceptionResolver. Cualquier aplicación web requiere un buen diseño para el manejo de excepciones porque no queremos mostrar una página generada por un contenedor cuando nuestra aplicación lanza una excepción no controlada.
Manejo de excepciones en Spring
Tener un enfoque de manejo de excepciones bien definido es una gran ventaja para cualquier marco de aplicación web. Dicho esto, el marco Spring MVC ofrece buenos resultados en lo que respecta al manejo de excepciones y errores en nuestras aplicaciones web. El marco Spring MVC ofrece las siguientes formas de ayudarnos a lograr un manejo sólido de excepciones.
- Basado en el controlador : podemos definir métodos de manejo de excepciones en nuestras clases de controlador. Todo lo que necesitamos es anotar estos métodos con
@ExceptionHandleruna anotación. Esta anotación toma la clase Exception como argumento. Entonces, si hemos definido uno de estos para la clase Exception, entonces todas las excepciones lanzadas por nuestro método de manejo de solicitudes se habrán manejado. Estos métodos de manejo de excepciones son como otros métodos de manejo de solicitudes y podemos crear una respuesta de error y responder con diferentes páginas de error. También podemos enviar una respuesta de error JSON, que veremos más adelante en nuestro ejemplo. Si hay varios métodos de manejo de excepciones definidos, entonces se usa el método de manejo que esté más cerca de la clase Exception. Por ejemplo, si tenemos dos métodos de manejo definidos para IOException y Exception y nuestro método de manejo de solicitudes lanza IOException, entonces se ejecutará el método de manejo para IOException. - Controlador de excepciones global : el control de excepciones es una preocupación transversal, debe realizarse para todos los puntos de corte de nuestra aplicación. Ya hemos analizado Spring AOP y es por eso que Spring proporciona
@ControllerAdviceuna anotación que podemos usar con cualquier clase para definir nuestro controlador de excepciones global. Los métodos de controlador en Global Controller Advice son los mismos que los métodos de controlador de excepciones basados en el controlador y se usan cuando la clase del controlador no puede controlar la excepción. - HandlerExceptionResolver : para excepciones genéricas, la mayoría de las veces servimos páginas estáticas. Spring Framework proporciona
HandlerExceptionResolveruna interfaz que podemos implementar para crear un controlador de excepciones global. La razón detrás de esta forma adicional de definir el controlador de excepciones global es que Spring Framework también proporciona clases de implementación predeterminadas que podemos definir en nuestro archivo de configuración de bean de Spring para obtener los beneficios del manejo de excepciones de Spring Framework.SimpleMappingExceptionResolveres la clase de implementación predeterminada, nos permite configurar exceptionMappings donde podemos especificar qué recurso usar para una excepción en particular. También podemos anularlo para crear nuestro propio controlador global con los cambios específicos de nuestra aplicación, como el registro de mensajes de excepción.
Creemos un proyecto Spring MVC en el que analizaremos la implementación de enfoques de manejo de errores y excepciones basados en controladores, AOP y Exception Resolver. También escribiremos un método de manejo de excepciones que devolverá una respuesta JSON. Si no tienes experiencia con JSON en Spring, lee el Tutorial de Spring Restful JSON . Nuestro proyecto final se verá como la siguiente imagen, analizaremos todos los componentes de nuestra aplicación uno por uno.
Dependencias de Maven para el manejo de excepciones en Spring
Además de las dependencias estándar de Spring MVC, también necesitaríamos la dependencia JSON de Jackson para la compatibilidad con JSON. Nuestro archivo pom.xml final se ve como se muestra a continuación.
?xml version="1.0" encoding="UTF-8"?project 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"modelVersion4.0.0/modelVersiongroupIdcom.journaldev.spring/groupIdartifactIdSpringExceptionHandling/artifactIdnameSpringExceptionHandling/namepackagingwar/packagingversion1.0.0-BUILD-SNAPSHOT/versionpropertiesjava-version1.6/java-versionorg.springframework-version4.0.2.RELEASE/org.springframework-versionorg.aspectj-version1.7.4/org.aspectj-versionorg.slf4j-version1.7.5/org.slf4j-versionjackson.databind-version2.2.3/jackson.databind-version/propertiesdependencies!-- Jackson --dependencygroupIdcom.fasterxml.jackson.core/groupIdartifactIdjackson-databind/artifactIdversion${jackson.databind-version}/version/dependency!-- Spring --dependencygroupIdorg.springframework/groupIdartifactIdspring-context/artifactIdversion${org.springframework-version}/versionexclusions!-- Exclude Commons Logging in favor of SLF4j --exclusiongroupIdcommons-logging/groupIdartifactIdcommons-logging/artifactId/exclusion/exclusions/dependencydependencygroupIdorg.springframework/groupIdartifactIdspring-webmvc/artifactIdversion${org.springframework-version}/version/dependency!-- AspectJ --dependencygroupIdorg.aspectj/groupIdartifactIdaspectjrt/artifactIdversion${org.aspectj-version}/version/dependency!-- Logging --dependencygroupIdorg.slf4j/groupIdartifactIdslf4j-api/artifactIdversion${org.slf4j-version}/version/dependencydependencygroupIdorg.slf4j/groupIdartifactIdjcl-over-slf4j/artifactIdversion${org.slf4j-version}/versionscoperuntime/scope/dependencydependencygroupIdorg.slf4j/groupIdartifactIdslf4j-log4j12/artifactIdversion${org.slf4j-version}/versionscoperuntime/scope/dependencydependencygroupIdlog4j/groupIdartifactIdlog4j/artifactIdversion1.2.15/versionexclusionsexclusiongroupIdjavax.mail/groupIdartifactIdmail/artifactId/exclusionexclusiongroupIdjavax.jms/groupIdartifactIdjms/artifactId/exclusionexclusiongroupIdcom.sun.jdmk/groupIdartifactIdjmxtools/artifactId/exclusionexclusiongroupIdcom.sun.jmx/groupIdartifactIdjmxri/artifactId/exclusion/exclusionsscoperuntime/scope/dependency!-- @Inject --dependencygroupIdjavax.inject/groupIdartifactIdjavax.inject/artifactIdversion1/version/dependency!-- Servlet --dependencygroupIdjavax.servlet/groupIdartifactIdservlet-api/artifactIdversion2.5/versionscopeprovided/scope/dependencydependencygroupIdjavax.servlet.jsp/groupIdartifactIdjsp-api/artifactIdversion2.1/versionscopeprovided/scope/dependencydependencygroupIdjavax.servlet/groupIdartifactIdjstl/artifactIdversion1.2/version/dependency!-- Test --dependencygroupIdjunit/groupIdartifactIdjunit/artifactIdversion4.7/versionscopetest/scope/dependency/dependenciesbuildpluginspluginartifactIdmaven-eclipse-plugin/artifactIdversion2.9/versionconfigurationadditionalProjectnaturesprojectnatureorg.springframework.ide.eclipse.core.springnature/projectnature/additionalProjectnaturesadditionalBuildcommandsbuildcommandorg.springframework.ide.eclipse.core.springbuilder/buildcommand/additionalBuildcommandsdownloadSourcestrue/downloadSourcesdownloadJavadocstrue/downloadJavadocs/configuration/pluginplugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-compiler-plugin/artifactIdversion2.5.1/versionconfigurationsource1.6/sourcetarget1.6/targetcompilerArgument-Xlint:all/compilerArgumentshowWarningstrue/showWarningsshowDeprecationtrue/showDeprecation/configuration/pluginplugingroupIdorg.codehaus.mojo/groupIdartifactIdexec-maven-plugin/artifactIdversion1.2.1/versionconfigurationmainClassorg.test.int1.Main/mainClass/configuration/plugin/plugins/build/project
He actualizado las versiones de Spring Framework, AspectJ, Jackson y slf4j para usar la última.
Descriptor de implementación de manejo de excepciones de Spring MVC
Nuestro archivo web.xml se ve como se muestra a continuación.
?xml version="1.0" encoding="UTF-8"?web-app version="2.5" 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-paramparam-namecontextConfigLocation/param-nameparam-value/WEB-INF/spring/root-context.xml/param-value/context-param!-- Creates the Spring Container shared by all Servlets and Filters --listenerlistener-classorg.springframework.web.context.ContextLoaderListener/listener-class/listener!-- Processes application requests --servletservlet-nameappServlet/servlet-nameservlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-classinit-paramparam-namecontextConfigLocation/param-nameparam-value/WEB-INF/spring/spring.xml/param-value/init-paramload-on-startup1/load-on-startup/servletservlet-mappingservlet-nameappServlet/servlet-nameurl-pattern//url-pattern/servlet-mappingerror-pageerror-code404/error-codelocation/resources/404.jsp/location/error-page/web-app
La mayor parte de la parte es para conectar Spring Framework a nuestra aplicación web, excepto la página de error definida para el error 404. Por lo tanto, cuando nuestra aplicación genere un error 404, esta página se utilizará como respuesta. Esta configuración la utiliza el contenedor cuando nuestra aplicación web Spring genere un código de error 404.
Manejo de excepciones en Spring: clases modelo
He definido el bean Employee como clase modelo, sin embargo, lo usaremos en nuestra aplicación solo para devolver una respuesta válida en un escenario específico. Lanzaremos deliberadamente diferentes tipos de excepciones en la mayoría de los casos.
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;}}
Dado que también devolveremos una respuesta JSON, creemos un bean Java con detalles de excepción que se enviará como respuesta.
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;}}
Manejo de excepciones de Spring: clase de excepción personalizada
Creemos una clase de excepción personalizada para que la utilice nuestra aplicación.
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") //404public class EmployeeNotFoundException extends Exception {private static final long serialVersionUID = -3332292346834265371L;public EmployeeNotFoundException(int id){super("EmployeeNotFoundException withvertical-align: inherit;"Tenga en cuenta que podemos usar@ResponseStatusanotaciones con clases de excepción para definir el código HTTP que enviará nuestra aplicación cuando nuestra aplicación genere este tipo de excepción y sea manejada por nuestras implementaciones de manejo de excepciones. Como puede ver, estoy configurando el estado HTTP como 404 y tenemos una página de error definida para esto, por lo que nuestra aplicación debería usar la página de error para este tipo de excepción si no estamos devolviendo ninguna vista. También podemos anular el código de estado en nuestro método de manejo de excepciones, piense en él como el código de estado HTTP predeterminado cuando nuestro método de manejo de excepciones no está devolviendo ninguna página de vista como respuesta.Controlador de manejo de excepciones de Spring MVC Clase controladora de excepciones
Veamos nuestra clase controladora donde lanzaremos diferentes tipos de excepciones.
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;@Controllerpublic 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 exceptionif(id==1){throw new EmployeeNotFoundException(id);}else if(id==2){throw new SQLException("SQLException,IOException,Pankaj");emp.setId(id);model.addAttribute("employee", emp);return "home";}else {throw new Exception("Generic Exception,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;}}Tenga en cuenta que, para el controlador EmployeeNotFoundException, estoy devolviendo ModelAndView y, por lo tanto, el código de estado http se enviará como OK (200). Si hubiera devuelto void, entonces el código de estado http se habría enviado como 404. Analizaremos este tipo de implementación en nuestra implementación del controlador de excepciones global. Dado que solo estoy manejando EmployeeNotFoundException en el controlador, todas las demás excepciones lanzadas por nuestro controlador serán manejadas por el controlador de excepciones global.
@ControllerAdvice y @ExceptionHandler
Aquí está nuestra clase controladora de excepciones global. Observe que la clase está anotada con la anotación @ControllerAdvice . Además, los métodos están anotados con la anotación @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;@ControllerAdvicepublic 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}}Tenga en cuenta que para SQLException, estoy devolviendo database_error.jsp como página de respuesta con el código de estado http como 200. Para IOException, estamos devolviendo void con el código de estado como 404, por lo que nuestra página de error se utilizará en este caso. Como puede ver, no estoy manejando ningún otro tipo de excepción aquí, esa parte la he dejado para la implementación de HandlerExceptionResolver.
Resolvedor de excepciones de Handler
Simplemente ampliamos SimpleMappingExceptionResolver y anulamos uno de los métodos, pero podemos anular su método más importante
resolveExceptionpara registrar y enviar diferentes tipos de páginas de vista. Pero eso es lo mismo que usar la implementación de ControllerAdvice, así que lo dejaré. Lo usaremos para configurar la página de vista para todas las demás excepciones que no manejamos respondiendo con una página de error genérica.Archivo de configuración de manejo de excepciones de Spring
Nuestro archivo de configuración de bean de primavera se ve como el siguiente: código spring.xml:
?xml version="1.0" encoding="UTF-8"?beans:beans 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.xsdhttps://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsdhttps://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:beanbeans:property name="prefix" value="/WEB-INF/views/" /beans:property name="suffix" value=".jsp" //beans:beanbeans:beanbeans:property name="exceptionMappings"beans:mapbeans:entry key="Exception" value="generic_error"/beans:entry/beans:map/beans:propertybeans:property name="defaultErrorView" value="generic_error"//beans:bean!-- Configure to plugin JSON as request and response in method handler --beans:beanbeans:property name="messageConverters"beans:listbeans:ref bean="jsonMessageConverter"//beans:list/beans:property/beans:bean!-- Configure bean to convert JSON to POJO and vice versa --beans:bean/beans:beancontext:component-scan base-package="com.journaldev.spring" //beans:beansObserve los beans configurados para admitir JSON en nuestra aplicación web. La única parte relacionada con el manejo de excepciones es la definición del bean simpleMappingExceptionResolver, donde definimos generic_error.jsp como la página de visualización para la clase Exception. Esto garantiza que cualquier excepción no manejada por nuestra aplicación no resulte en el envío de una página de error generada por el servidor como respuesta.
Manejo de excepciones de páginas de visualización JSP en Spring MVC
Es hora de mirar la última parte de nuestra aplicación, nuestras páginas de vista que se utilizarán en nuestra aplicación. Código home.jsp:
%@ taglib uri="https://java.sun.com/jsp/jstl/core" prefix="c" %%@ page session="false" %htmlheadtitleHome/title/headbodyh3Hello ${employee.name}!/h3brh4Your ID is ${employee.id}/h4 /body/htmlhome.jsp se utiliza para responder con datos válidos, es decir, cuando obtenemos un id como 10 en la solicitud del cliente. Código 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"htmlheadmeta http-equiv="Content-Type" content="text/html; charset=UTF-8"title404 Error Page/title/headbodyh2Resource Not Found Error Occured, please contact support./h2/body/html404.jsp se utiliza para generar una vista para el código de estado http 404. Para nuestra implementación, esta debería ser la respuesta cuando obtenemos un ID como 3 en la solicitud del cliente. Código 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" %htmlheadmeta http-equiv="Content-Type" content="text/html; charset=UTF-8"titleError Page/title/headbodyh2Application Error, please contact support./h2h3Debug Information:/h3Requested URL= ${url}brbrException= ${exception.message}brbrstrongException Stack Trace/strongbrc:forEach items="${exception.stackTrace}" var="ste"${ste}/c:forEach/body/htmlerror.jsp se utiliza cuando el método de manejo de solicitudes de nuestra clase controladora genera una excepción EmployeeNotFoundException. Deberíamos obtener esta página como respuesta cuando el valor de id sea 1 en la solicitud del cliente. Código 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"htmlheadmeta http-equiv="Content-Type" content="text/html; charset=UTF-8"titleDatabase Error Page/title/headbodyh2Database Error, please contact support./h2/body/htmldatabase_error.jsp se utiliza cuando nuestra aplicación lanza una excepción SQLException, tal como se configura en la clase GlobalExceptionHandler. Deberíamos obtener esta página como respuesta cuando el valor de id sea 2 en la solicitud del cliente. Código 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"htmlheadmeta http-equiv="Content-Type" content="text/html; charset=UTF-8"titleGeneric Error Page/title/headbodyh2Unknown Error Occured, please contact support./h2/body/htmlEsta debería ser la página que se utilizará como respuesta cuando se produzca una excepción que no sea manejada por nuestro código de aplicación y el bean simpleMappingExceptionResolver se encarga de ello. Deberíamos obtener esta página como respuesta cuando el valor de id en la solicitud del cliente sea distinto de 1, 2, 3 o 10.
Ejecución de la aplicación de manejo de excepciones Spring MVC
Simplemente implemente la aplicación en el contenedor de servlets que está usando, estoy usando Apache Tomcat 7 para este ejemplo. Las imágenes a continuación muestran las diferentes páginas de respuesta devueltas por nuestra aplicación según el valor de id. ID=10, respuesta válida. ID=1, controlador de excepción basado en controlador usado ID=2, controlador de excepción global usado con vista como respuesta ID=3, página de error 404 usada ID=4, simpleMappingExceptionResolver usado para vista de respuesta Como puede ver, obtuvimos la respuesta esperada en todos los casos.
Respuesta JSON del controlador de excepciones de Spring
Ya casi hemos terminado nuestro tutorial, excepto la última parte, donde explicaré cómo enviar una respuesta JSON desde los métodos del controlador de excepciones. Nuestra aplicación tiene todas las dependencias JSON y jsonMessageConverter está configurado, todo lo que necesitamos es implementar el método del controlador de excepciones. Para simplificar, reescribiré el método handleEmployeeNotFoundException() de EmployeeController para que devuelva una respuesta JSON. Simplemente actualice el método del controlador de excepciones de EmployeeController con el código que se muestra a continuación e implemente la aplicación nuevamente.
@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;}Ahora, cuando usamos id como 1 en la solicitud del cliente, obtenemos la siguiente respuesta JSON como se muestra en la siguiente imagen. Eso es todo sobre el manejo de excepciones de Spring y el manejo de excepciones de Spring MVC. Descargue la aplicación desde la siguiente URL y experimente con ella para obtener más información.
Descargar Proyecto de manejo de excepciones de Spring