Spring Boot: Gestión de errores en aplicaciones web y REST

Última actualización: 02/11/2020

logo springEn este tutorial haremos un breve repaso con un enfoque eminentemente práctico de las mejores opciones que tenemos para la gestión de excepciones en aplicaciones web, tanto “tradicionales” como REST, desarrolladas con Spring MVC. Se hará especial hincapié en las funcionalidades implementadas de forma específica por Spring Boot que facilitarán nuestro trabajo.

Proyecto de ejemplo

El proyecto de ejemplo con el que vamos a empezar a trabajar en el presente tutorial no es más que un “cascarón vacío” para una aplicación web en el que añadiremos url que lanzan excepciones para ver cómo procesarlas. Por lo tanto, tenemos un pom muy sencillo para utilizar Spring Boot 3.2 y el starter web con JSP.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.danielme.demo</groupId>
    <artifactId>spring-boot-errors</artifactId>
    <version>1.0</version>
    <packaging>war</packaging>

    <name>spring-boot-demo-errors</name>
    <description>Demo project for Spring Boot Error handling (Web and REST)</description>
    <url>https://wp.me/p2bpHB-1rp</url>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.5.RELEASE</version>
        <relativePath />
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <scope>provided</scope>
        </dependency>

    </dependencies>

    <build>

        <finalName>spring-boot-errors</finalName>

        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

La aplicación puede ejecutarse con el comando mvn spring-boot:run pero está configurada para que se pueda desplegar directamente desde Eclipse en un Tomcat 8 como cualquier aplicación web Maven, o bien utilizando el plugin Spring Tools Suite. La url raíz de la aplicación es http://localhost:8080/spring-boot-errors/, pero todas las llamadas devolverán 404 pues no hemos implementado todavía nada.

Spring Boot Whitelabel

Spring Boot por defecto configura una página html que se devuelve como respuesta en caso de que se lance cualquier excepción tanto desde el código de nuestra aplicación como desde el propio framework. Definamos una url en http://localhost:8080/spring-boot-errors/throwException que se limite a lanzar una excepción para que podamos hacer pruebas.

package com.danielme.springboot.controllers.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ForceErrorJspController {

    @GetMapping(value = "/throwException")
    public void throwException() {
        throw new IllegalArgumentException("\"I am the error message from JSP Controller\"");
    }

}

Si llamamos a esta url este es el resultado.


Cursos de programación

La página informa que no se ha definido un mapeo para la url /error y muestra el tipo de error HTTP producido (404-Not Found, 500-Internal Server Error, etc), y la traza de la excepción. Revisando el código fuente de Spring Boot en GitHub podemos ver que esta página se construye de forma programática en la clase ErrorMvcAutoConfiguration.

public void render(Map<String, ?> model, HttpServletRequest request,
				HttpServletResponse response) throws Exception {
			if (response.isCommitted()) {
				String message = getMessage(model);
				logger.error(message);
				return;
			}
			StringBuilder builder = new StringBuilder();
			Date timestamp = (Date) model.get("timestamp");
			Object message = model.get("message");
			Object trace = model.get("trace");
			if (response.getContentType() == null) {
				response.setContentType(getContentType());
			}
			builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
					"<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
					.append("<div id='created'>").append(timestamp).append("</div>")
					.append("<div>There was an unexpected error (type=")
					.append(htmlEscape(model.get("error"))).append(", status=")
					.append(htmlEscape(model.get("status"))).append(").</div>");
			if (message != null) {
				builder.append("<div>").append(htmlEscape(message)).append("</div>");
			}
			if (trace != null) {
				builder.append("<div style='white-space:pre-wrap;'>")
						.append(htmlEscape(trace)).append("</div>");
			}
			builder.append("</body></html>");
			response.getWriter().append(builder.toString());
}

Spring Boot nos permite definir nuestra propia página de error genérica y utilizarla de forma automática. Crearemos esta página de error con la tecnología que estamos utilizando para renderizar el html (JSP, Thymelef, Velocity…) que en la aplicación de ejemplo es JSP. El fichero debe llamarse error.jsp y ubicarse en la raíz del directorio en el que Spring Boot espera encontrar las páginas web, esto se define en la propiedad spring.mvc.view.prefix del fichero application.properties. En nuestro ejemplo, sería /src/main/webapp/WEB-INF/jsp/error.jsp.

<%@ page contentType="text/html; charset=UTF-8"%>

<!DOCTYPE HTML>

<html>

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="shortcut icon" type="image/png" href="favicon.ico">
    <title>Custom Error page</title>
</head>

<body>
    <h1>Custom error page</h1>
</body>

</html>

Se puede ir más allá y crear una página de error personalizada en función del código de status HTTP del error. Para ello basta con nombrar a la página con el código HTTP, por ejemplo 404.jsp, y ubicarla dentro de un subdirectorio llamado error. En nuestro ejemplo tendríamos la página en /src/main/webapp/WEB-INF/jsp/error/404.jsp. Si forzamos un error de tipo 404-Not found veremos esta página, para los demás errores se seguirá mostrando error.jsp que actúa siempre como página de error por defecto.

<%@ page contentType="text/html; charset=UTF-8"%>

<!DOCTYPE HTML>

<html>

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="shortcut icon" type="image/png" href="favicon.ico">
    <title>Custom Error page - 404</title>
</head>

<body>
    <h1>404 - NOT FOUND</h1>
</body>

</html>

Spring Boot escribe en el request los siguientes atributos que podemos recuperar en nuestras páginas de error personalizadas.

  • status: entero con el código de status HTTP.
  • error: el nombre del error HTTP.
  • message: el mensaje de error, que se corresponde con el mensaje de la excepción lanzada.
  • path: la url, relativa dentro de nuestra aplicación, cuya llamada provocó el error.
  • trace: volcado de la traza completa de la excepción.

Comprobémoslo imprimiendo estos valores en pantalla

<%@ page contentType="text/html; charset=UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

<!DOCTYPE HTML>

<html>

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="shortcut icon" type="image/png" href="favicon.ico">
    <title>Custom Error page</title>
</head>

<body>
    <h1>Custom error page</h1>

    <ul>
        <li>status: <c:out value="${requestScope.status}" /></li>
        <li>error: <c:out value="${requestScope.error}" /></li>
        <li>message: <c:out value="${requestScope.message}" /></li>
        <li>path: <c:out value="${requestScope.path}" /></li>
        <li>trace: <c:out value="${requestScope.trace}" /></li>
    </ul>

</body>

</html>

Es posible añadir a los parámetros de error parámetros adicionales especializando la clase DefaultErrorAttributes.

package com.danielme.springboot.controllers;

import java.util.Map;

import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;

@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest,
            boolean includeStackTrace) {
        Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest,
                includeStackTrace);
        errorAttributes.put("jdk", System.getProperty("java.version"));

        return errorAttributes;
    }

}
<li>jdk: <c:out value="${requestScope.jdk}" /></li>

En Spring Boot 2.3 el método que hemos sobrescrito ha sido marcado como deprecated, y en su lugar usamos el siguiente.

   @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
        errorAttributes.put("jdk", System.getProperty("java.version"));
        return errorAttributes;
    }

Vamos a suponer que nuestra aplicación no sólo sirve páginas html estándar, sino que también dispone de una pequeña API REST basada en JSON. Para las peticiones en las que se especifique el header Accept como application/json Spring Boot devuelve de forma automática un JSON con la misma información que se muestra en la whitelabel page. Para el resto de llamadas se seguirá devolviendo el html correspondiente a la whitelabel page o, en nuestro caso, error.jsp.

El siguiente controlador de tipo REST lanza una excepción.

package com.danielme.springboot.controllers.rest;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ForceErrorRestController {

    @GetMapping(value = "/api/throwException")
    public void throwException() {
        throw new IllegalArgumentException("\"I am the error message from Rest Controller\"");
    }
}

Probamos su respuesta utilizando por ejemplo Postman.

Por defecto en la respuesta JSON no se muestra la traza de la excepción, podemos incluirla con el siguiente parámetro de configuración.

server.error.include-stacktrace=always

Implementando un ErrorController en Spring Boot

Se puede aumentar el control sobre la gestión de errores implementando un Controller para la url /error a la que Spring Boot redirecciona automáticamente cundo se produce una excepción. Esto permite implementar cualquier lógica que necesitemos realizar.

Veamos un ejemplo sencillo que muestra las jsp error.jsp o 404.jsp que se crearon anteriormente según el error producido. Lo que haremos es escribir un controlador para la url /error que además implemente la interfaz funcional ErrorController para indicar que la finalidad del controlador es renderizar páginas de error.

package com.danielme.springboot.controllers;

import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class CustomErrorController implements ErrorController {

    private static final Logger logger = LoggerFactory.getLogger(CustomErrorController.class);

    @RequestMapping("/error")
    public String handleError(HttpServletRequest request) {
        logger.info("executing custom error controller");
        if (HttpStatus.NOT_FOUND
                .value() == (int) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)) {
            return "/error/404";
        }
        return "error";
    }

    @Override
    public String getErrorPath() {
        return "/error";
    }

}

Para que error.jsp siga mostrando los mismos datos vamos a añadir al Model de Spring MVC los mismos atributos que Spring Boot ya escribía automáticamente y que la jsp lee del request. Estos atributos están disponibles a través del bean ErrorAttributes el cual podemos inyectar en un atributo. Asimismo, para poder acceder al map con los atributos con ErrorAttributes también necesitamos el objeto de tipo WebRequest asociado a la petición que estamos procesando. Podemos obtenerlo añadiéndolo como parámetro al método.

package com.danielme.springboot.controllers;

import java.util.Map;

import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.WebRequest;

@Controller
public class CustomErrorController implements ErrorController {

    private static final Logger logger = LoggerFactory.getLogger(CustomErrorController.class);

    @Autowired
    private ErrorAttributes errorAttributes;

    @RequestMapping("/error")
    public String handleError(HttpServletRequest request, WebRequest webRequest, Model model) {
        logger.info("executing custom error controller");

        if (HttpStatus.NOT_FOUND
                .value() == (int) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)) {
            return "/error/404";
        }

        Map<String, Object> mapErrors = errorAttributes.getErrorAttributes(webRequest,
                ErrorAttributeOptions.of(ErrorAttributeOptions.Include.STACK_TRACE));
        model.addAllAttributes(mapErrors);

        return "error";
    }

    @Override
    public String getErrorPath() {
        return "/error";
    }

}

Al obtenerse el map con los detalles del error se ha solicitado la traza completa del error (ErrorAttributeOptions.Include.STACK_TRACE) lo que permite imprimirla en pantalla.

Siguiendo esta estrategia y dejando de utilizar la gestión automática de errores de Spring Boot encontramos un problema en nuestra aplicación de ejemplo: los errores producidos en la Api REST ahora no devuelven un JSON sino la página correspondiente a error.jsp. Podemos comprobarlo realizando de nuevo la llamada con Postman que vimos anteriormente.

Una posible solución consiste en leer el Accept del cliente y si es de tipo application/json delegar la respuesta en otra url que devolverá un JSON. Esta url la vamos a definir en el mismo controller y su método devolverá un objeto de la clase CustomErrorJson. Puesto que el controlador no está anotado con @RestController, es necesario marcar el método con @ResponseBody para que Spring sepa que el método devuelve un objeto que debe serializar en un JSON.

package com.danielme.springboot.controllers;

import java.util.Map;

import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.WebRequest;

import com.danielme.springboot.model.CustomErrorJson;

@Controller
public class CustomErrorController implements ErrorController {

    private static final Logger logger = LoggerFactory.getLogger(CustomErrorController.class);

    @Autowired
    private ErrorAttributes errorAttributes;

    @RequestMapping("/error")
    public String handleError(@RequestHeader("Accept") String accept, HttpServletRequest request, 
            WebRequest webRequest, Model model) {
        logger.info("executing custom error controller");

        if (accept.contains(MediaType.APPLICATION_JSON.toString())) {
            return "forward:/errorJSON";
        }
        
        if (HttpStatus.NOT_FOUND
                .value() == (int) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)) {
            return "/error/404";
        }

        //Map<String, Object> mapErrors = errorAttributes.getErrorAttributes(webRequest, true);
        Map<String, Object> mapErrors = errorAttributes.getErrorAttributes(webRequest,
                ErrorAttributeOptions.of(ErrorAttributeOptions.Include.STACK_TRACE));
        model.addAllAttributes(mapErrors);

        return "error";
    }
    
    @RequestMapping("/errorJSON")
    @ResponseBody
    public CustomErrorJson handleErrorJson(HttpServletRequest request, WebRequest webRequest) {
        //Map<String, Object> mapErrors = errorAttributes.getErrorAttributes(webRequest, true);
        Map<String, Object> mapErrors = errorAttributes.getErrorAttributes(webRequest,
                ErrorAttributeOptions.of(ErrorAttributeOptions.Include.STACK_TRACE));

        return new CustomErrorJson((int) request.getAttribute(
                RequestDispatcher.ERROR_STATUS_CODE),
                (String) mapErrors.get("error"),
                (String) mapErrors.get("message"),
                (String) mapErrors.get("path"),
                (String) mapErrors.get("trace"));
    }

    @Override
    public String getErrorPath() {
        return "/error";
    }

}
{
    "status": 500,
    "error": "Internal Server Error",
    "message": "\"I am the message\"",
    "path": "/spring-boot/api/throwException",
    "trace": "java.lang.IllegalArgumentException: \"I am the message\"\n\tat com.danielme.springboot.controllers.ForceErrorController.throwException(ForceErrorController.java:11)\n\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.lang.reflect.Method.invoke(Method.java:498)\n\tat org.springframework.web.method.support.Invo

En cualquier caso, en lugar de implementar un error controller genérico, recomiendo utilizar la gestión de errores estándar de Spring MVC que veremos a continuación.

Configuración por controladores y excepciones (Spring Boot y Spring MVC)

Además de los mecanismos de gestión de errores que hemos visto en las secciones anteriores, en Spring Boot podemos seguir utilizando las funcionalidades proporcionadas por Spring MVC con las que es posible conseguir una gestión de errores más exhaustiva y atomizada. Podemos definir “manejadores” (handler) específicos para cualquier tipo de excepción y aplicarlos sólo a los controladores que queramos utilizando las siguientes anotaciones.

@ExceptionHandler Con esta notación marcamos un método de una clase controladora para que gestione uno o varios tipos de excepciones que sean lanzadas desde los métodos de ese controlador. Estas excepciones las indicamos con el atributo values de la anotación, y podemos recibirla como el primer parámetro del método.

 @ExceptionHandler(value = {ServiceException.class, ParseDataException.class})
    public String handleException(Exception ex) {
        return "error";
    }

Para una única excepción, basta con pasarla como parámetro.

@ExceptionHandler
public String handleException(ServiceException ex) {
    return "error";
}

Estos métodos se comportan exactamente igual que los métodos que atienden a una url, esto es, un @RequestMapping o equivalente, sólo que en lugar de escuchar en una dirección del servidor atienden a una excepción. Por lo tanto, podemos recibir como parámetros el Model, el request, el locale, etc, y devolver el nombre de una vista (jsp, thymeleaf, etc) o un ResponseBody.

@ExceptionHandler
public String handleException(Exception ex, HttpServletRequest request, Model model) {        
    return "error";
}

@ControllerAdvice Las clases anotadas con esta anotación aplican sus métodos de tipo @ExceptionHandler a un conjunto de controladores que pueden ser definidos mediante su clase, las rutas de paquetes en las que se encuentren o que tengan cierta anotación. Si no se define ningún controlador, se aplicará a todos. Esta característica nos viene de perlas en nuestra aplicación de ejemplo en la que tenemos un controlador que devuelve vistas .jsp y otro que retornan JSON.

Para trasladar la gestión de errores desarrollada anteriormente en CustomErrorController a este nuevo sistema debemos tener en cuenta las siguientes limitaciones:

  • Con ErrorAttributes no podemos acceder a todos los datos que obteníamos antes, tendremos que recibir la excepción como parámetro.
  • Para el error 404, Spring por defecto no lanza ninguna excepción. Estudiaremos este caso más adelante.

La siguiente clase es un gestor de excepciones genérico para todos los controladores que devuelven vistas html, considerando que dichos controladores estarán dentro del paquete com.danielme.springboot.controllers.web o bien que están anotados con @Controller.

package com.danielme.springboot.controllers.web;
 
import java.io.PrintWriter;
import java.io.StringWriter;
 
import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
 
//@ControllerAdvice(basePackages = "com.danielme.springboot.controllers.web")
@ControllerAdvice(annotations=Controller.class)
public class ErrorHtmlControllerAdvice {
 
    private static final Logger logger = LoggerFactory.getLogger(ErrorHtmlController.class);
 
    @ExceptionHandler
    public String handleException(Exception ex, HttpServletRequest request, Model model) {
        logger.info("executing exception handler (web)");
 
        model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
        model.addAttribute("error", ex.getClass().getName());
        model.addAttribute("message", ex.getMessage());
        model.addAttribute("path", request.getRequestURI());
        model.addAttribute("jdk", System.getProperty("java.version"));
 
        StringWriter sw = new StringWriter();
        ex.printStackTrace(new PrintWriter(sw));
 
        model.addAttribute("trace", sw.toString());
        return "error";
    }
}

Y esta es la clase equivalente para los controladores de la API REST (com.danielme.springboot.controllers.rest). Especificamos el status code 500 con la anotación @ResponseStatus, también se podría devolver ResponseEntity<CustomErrorJson> e indicar en el mismo el código Http.

package com.danielme.springboot.controllers.rest;
 
import java.io.PrintWriter;
import java.io.StringWriter;
 
import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import com.danielme.springboot.model.CustomErrorJson;

//@ControllerAdvice(basePackages = "com.danielme.springboot.controllers.rest")
@ControllerAdvice(annotations=RestController.class)
public class ErrorRestControllerAdvice {
 
    private static final Logger logger = LoggerFactory.getLogger(ErrorRestController.class);
   
    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public CustomErrorJson handleException(Exception ex, HttpServletRequest request) {
        logger.info("executing exception handler (REST)");
 
        StringWriter sw = new StringWriter();
        ex.printStackTrace(new PrintWriter(sw));
 
        return new CustomErrorJson(
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                ex.getClass().getName(),
                ex.getMessage(),
                request.getRequestURI(),
                sw.toString());
    }

}

En caso de recurso no encontrado (404) Spring no lanza una excepción que pueda ser capturada por nuestro ExceptionHandler. Sin embargo, si probamos con Postman veremos lo siguiente.

El sistema de gestión de errores de Spring Boot, en el ejemplo la clase CustomErrorController, está procesando el error 404, y hará lo mismo para cualquier excepción que no trate ningún ExceptionHandler haciendo de “última barrera” entre las excepciones y los clientes de la aplicación. En el caso particular del error 404, se puede indicar a Spring que lance una excepción (NoHandlerFoundException) que pueda ser capturada por un ExceptionHandler realizando la siguiente configuración.

spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false

Sin embargo, la segunda opción deshabilita la configuración automática que Spring Boot realiza del ResourceHandler y que permite gestionar los recursos definidos en /spring-boot-errors/src/main/resources/static/, en nuestro caso el fichero styles.css. Por tanto no recomiendo esta estrategia y en su lugar dejar que actúe el sistema de control de errores automático de Spring Boot que tal y como hemos visto a lo largo del tutorial podemos configurar sin muchas dificultades.

Errores de validación

Vamos a añadir un endpoint que reciba mediante POST un JSON para un objeto de tipo Country y que verifique que se cumplen las restricciones definidas mediante Hibernate Validator.

public class Country {

    private Integer id;
    
    @NotNull
    private String name;
    
    @NotNull
    @Min(1)
    @Max(2000000000)
    private Integer population;

package com.danielme.springboot.controllers.rest;

import com.danielme.springboot.model.Country;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
@RequestMapping(CountryRestController.COUNTRY_RESOURCE)
public class CountryRestController {

    public static final String COUNTRY_RESOURCE = "/api/country";

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void add(@RequestBody @Valid Country country) {
       //noting here 😛
    }

}

Al producirse un error de validación se lanza la excepción MethodArgumentNotValidException. Si no se ha implementado ningún mecanismo de gestión de errores (@ExceptionHandler, ErrorController), en Spring Boot se devolverá un JSON como el siguiente que incluye información (demasiado) detallada sobre los errores de validación.

{
    "timestamp": "2019-03-22T18:25:07.447+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "Min.country.population",
                "Min.population",
                "Min.java.lang.Integer",
                "Min"
            ],
            "arguments": [
                {
                    "codes": [
                        "country.population",
                        "population"
                    ],
                    "arguments": null,
                    "defaultMessage": "population",
                    "code": "population"
                },
                1
            ],
            "defaultMessage": "must be greater than or equal to 1",
            "objectName": "country",
            "field": "population",
            "rejectedValue": -234,
            "bindingFailure": false,
            "code": "Min"
        },
        {
            "codes": [
                "NotNull.country.name",
                "NotNull.name",
                "NotNull.java.lang.String",
                "NotNull"
            ],
            "arguments": [
                {
                    "codes": [
                        "country.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                }
            ],
            "defaultMessage": "must not be null",
            "objectName": "country",
            "field": "name",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotNull"
        }
    ],
    "message": "Validation failed for object='country'. Error count: 2",
    "trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> com.danielme.springboot.controllers.CountryRestController.add(com.danielme.springboot.entities.Country) with 2 errors: [Field error in object 'country' on field 'population': rejected value [-234]; codes [Min.country.population,Min.population,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [country.population,population]; arguments []; default message [population],1]; default message [must be greater than or equal to 1]] [Field error in object 'country' on field 'name': rejected value [null]; codes [NotNull.country.name,NotNull.name,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [country.name,name]; arguments []; default message [name]]; default message [must not be null]] \n\tat org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:138)\n\tat org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:126)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:166)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:660)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:741)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:200)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:834)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.lang.Thread.run(Thread.java:748)\n",
    "path": "/api/country"
}

Personalizar la respuesta es tan sencillo como implementar el @ExceptionHandler correspondiente para esa excepción la cual incluye todos los errores de validación que han provocado su lanzamiento.

package com.danielme.springboot.controllers.rest;

import com.danielme.springboot.model.ValidationError;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;


@ControllerAdvice(annotations = RestController.class)
public class ConstraintViolationErrorRestControllerAdvice {

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public List<ValidationError> handleException(MethodArgumentNotValidException ex) {
        return ex.getBindingResult().getAllErrors()
                .stream()
                .map(this::mapError)
                .collect(Collectors.toList());
    }

    private ValidationError mapError(ObjectError objectError) {
        if (objectError instanceof FieldError) {
            return new ValidationError(((FieldError) objectError).getField(),
                    objectError.getDefaultMessage());
        }
        return new ValidationError(objectError.getObjectName(), objectError.getDefaultMessage());
    }

}
[
    {
        "field": "name",
        "message": "must not be null"
    },
    {
        "field": "population",
        "message": "must be greater than or equal to 1"
    }
]

Código de ejemplo

El proyecto completo se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.

Otros tutoriales relacionados con Spring

Testing en Spring Boot con JUnit 4\5. Mockito, MockMvc, REST Assured, bases de datos embebidas

Introducción a Spring Boot: Aplicación Web con servicios REST y Spring Data JPA

Spring JDBC Template: simplificando el uso de SQL

Persistencia en BD con Spring Data JPA

Spring REST: Securización BASIC y JDBC con Spring Security

Persistencia en BD con Spring: Integrando JPA, c3p0, Hibernate y EHCache

Testing Spring con JUnit 4

Ficheros .properties en Spring IoC

Master Pyhton, Java, Scala or Ruby

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios .