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

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 trabajar en el presente tutorial es una versión simplificada del desarrollado en el tutorial Introducción a Spring Boot: Aplicación Web con servicios REST y Spring Data JPA sin hacer uso de una base de datos. Consiste en una aplicación web basada en Spring Boot 2.1 que muestra un listado de países con la siguiente página JSP

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

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <link rel="shortcut icon" type="image/png" href="favicon.ico">
    
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" 
    integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" 
    crossorigin="anonymous">
    <link rel="stylesheet" href="styles.css">
    <title>Spring Boot</title>

</head>

<body>

   <div class="container">
      <div class="title">
        <h1>Countries</h1>
      </div>

      <c:choose>
            <c:when test="${not empty countriesList}">
    
                <ul>
                    <c:forEach var="item" items="${countriesList}">
                        <li>${item.name}:<fmt:formatNumber
                                value="${item.population}" /></li>
                    </c:forEach>
                </ul>
    
            </c:when>
            <c:otherwise>
                <b>NO DATA</b>
            </c:otherwise>
        </c:choose>
    </div>

    <footer class="footer">
      <div class="container">
        <p class="text-muted"><a href="https://danielme.com/spring/">danielme.com</a></p>
      </div>
    </footer>

</body>

</html>

la cual es la respuesta del siguiente controlador

package com.danielme.springboot.controllers.web;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import com.danielme.springboot.services.CountryService;

@Controller
public class CountryController {

    private final CountryService countryService;

    public CountryController(CountryService countryService) {
        this.countryService = countryService;
    }

    @RequestMapping("/")
    public String list(Model model) {
        model.addAttribute("countriesList", countryService.findAll());
        return "countriesList";
    }
}

El servicio siempre devuelve los mismos datos de prueba.

package com.danielme.springboot.services;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.springframework.stereotype.Service;

import com.danielme.springboot.model.Country;

@Service
public class CountryService {

    private static final List<Country> countries;
    static {
        countries = new ArrayList<>();
        countries.add(new Country(1, "Spain", 49067981));
        countries.add(new Country(2, "Mexico", 130497248));
    }

    public List<Country> findAll() {
        return countries;
    }

    public Optional<Country> findById(Integer id) {
        return countries.stream()
                .filter(c -> c.getId().equals(id))
                .findFirst();
    }

}

También contamos con un servicio REST que publica una llamada GET.

package com.danielme.springboot.controllers.rest;

import java.util.Optional;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.danielme.springboot.model.Country;
import com.danielme.springboot.services.CountryService;

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

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

    private final CountryService countryService;

    public CountryRestController(CountryService countryService) {
        this.countryService = countryService;
    }

    @GetMapping(value = "/{id}/")
    public ResponseEntity<Country> getById(@PathVariable("id") Integer id) {
        Optional<Country> country = countryService.findById(id);
        if (country.isPresent()) {
            return new ResponseEntity<>(country.get(), HttpStatus.OK);
        }
        return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
    }

}

Esta es la estructura del proyecto

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.

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 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 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 extendiendo a 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>

Nuestra aplicación de ejemplo 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 funcionando correctamente vamos a añadir al Model de Spring MVC para el controlador los mismos atributos que Spring Boot ya escribía automáticamente y que recuperamos en la jsp. 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 un objeto de tipo WebRequest asociado a la petición que estamos procesando. Podemos obtenerlo añadiéndolo como parámetro del 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, true);
        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 (parámetro includeStackTrace del método getErrorAttributes a true) 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 se puede definir en el mismo controller y devuelve una instancia de la clase CustomErrorJson que será serializada automáticamente por Spring en un JSON que contendrá los datos que sean accesible mediante métodos getters. Puesto que el controlador no está anotado con @RestController, es necesario marcar el método con @ResponseBody para que Spring serialice en formato JSON el objeto devuelto.

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.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("application/json")) {
            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);
        model.addAllAttributes(mapErrors);

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

        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

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: marca un método como gestor de uno o varios tipos de excepciones que indicaremos con el atributo values. En dicho método podemos inyectar como parámetros no sólo la excepción sino también el request, el locale, el Model etc, y devolvemos el nombre de una vista (.jsp, html, un response body…) a utilizar o un ResponseBody. Si no indicamos el tipo de la excepción en la anotación, se aplica el de la excepción que reciba el método como parámetro. El ExceptionHandler puede definirse dentro de cualquier Controller ( sólo atiende a excepciones que se lancen desde el mismo) o de un ControllerAdvice…
  • 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 el nombre de la 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 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.

Este es el código del manejador 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. No es necesario definir el tipo de excepción a tratar si esta es recibida como un parámetro. En el ejemplo vamos a capturar todas las de tipo Exception.

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.stereotype.Controller;
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")
@Controller
@ControllerAdvice(annotations=Controller.class)
public class ErrorHtmlController {
 
    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());
 
        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)

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.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.danielme.springboot.model.CustomErrorJson;

//@ControllerAdvice(basePackages = "com.danielme.springboot.controllers.rest")
@ControllerAdvice(annotations=RestController.class)
public class ErrorRestController {
 
    private static final Logger logger = LoggerFactory.getLogger(ErrorRestController.class);
   
    @ExceptionHandler
    @ResponseBody
    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 un país y valide que se verifican las restricciones de la clase Country definidas mediante Hibernate Validator.

public class Country {

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

@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, 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 este JSON es tan sencillo como implementar el @ExceptionHandler correspondiente.

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

    @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.

También se encuentran en GitHub los ejemplos oficiales de Spring Boot.

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.