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

Última actualización: 13/08/2022
logo spring

En este tutorial repasaremos las mejores opciones que tenemos para la gestión de errores en aplicaciones web basadas en Spring MVC, poniendo el foco en las capacidades de Spring Boot que nos facilitarán la vida. Trataremos tanto las aplicaciones que generan páginas web como las que ofrecen APIs REST. Nuestras aplicaciones fallarán, sí, pero con elegancia y dignidad.

Asumo que el lector ya posee unos conocimientos (muy) básicos acerca de Spring Boot y Spring MVC. Si no es el caso, puede aprenderlos en mi tutorial Introducción a Spring Boot: Aplicación con servicios REST y Spring Data JPA. Es más, el presente puede verse como una continuación del mismo.

Proyecto de ejemplo

El proyecto con el que vamos a empezar a trabajar en no es más que un «cascarón vacío» para una aplicación web en el que añadiremos urls que lanzan excepciones. Partimos de un pom sencillo con Spring Boot 2.7, el starter web y el soporte para JSP, basado en lo que explico en Tips Spring : [BOOT] integración con JSP. No obstante, el uso de JSP no será especialmente relevante y podremos aplicar lo que veamos a otras tecnologías como Thymeleaf.

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.2</version>
		<relativePath/>
	</parent>

	<groupId>com.danielme.blog</groupId>
	<artifactId>spring-boot-jsp</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-boot-jsp</name>

	<properties>
		<java.version>11</java.version>
	</properties>

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

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

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

	</dependencies>

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

</project>

La url raíz de la aplicación es http://localhost:8080/spring-boot-errors/.

spring.mvc.view.prefix= /WEB-INF/jsp/
spring.mvc.view.suffix= .jsp

server.servlet.context-path=/spring-boot-errors

La clase con el Main. Basta con ejecutarla para que la aplicación se despliegue en el servidor Tomcat que Spring Boot incluye en el proyecto.

@SpringBootApplication
public class ErrorsApp extends SpringBootServletInitializer {

    public static void main(String[] args) {
        SpringApplication.run(ErrorsApp.class, args);
    }

}

Comportamiento automático de Spring Boot

Whitelabel Error Page

Spring Boot genera una página HTML (whitelabel error page) 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.

@Controller
public class ForceErrorJspController {

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

}

Aquí la tenemos.

Lo primero que vemos es que Spring informa que muestra esta página porque no hemos implementado la gestión de la url /error, algo que haremos más adelante. A continuación aparece la traza de la excepción. Si el lector es curioso, puede comprobar que esta página es una cadena construida en la clase ErrorMvcAutoConfiguration.

Esta es la información disponible. Algunos datos están desactivados por seguridad; se quiere evitar dar pistas relevantes a potenciales atacantes.

  • status: entero con el código de status HTTP.
  • timestamp: objeto Date con el momento en el que se recopilan estos parámetros.
  • exception: la clase de la excepción. Hay que activar este atributo con server.error.include-exception en el application.properties.
  • error: el nombre del error HTTP.
  • message: el mensaje de error, que se corresponde con el mensaje de la excepción lanzada. Activar con server.error.include-message=always.
  • path: la url (relativa) cuya llamada provocó el error.
  • requestId: identificador asociado a la petición HTTP.
  • trace: volcado de la traza completa de la excepción. Activar con server.error.include-stacktrace=always.

Whitelabel Page puede desactivarse por completo. Las páginas de error que se mostrarán serán las del servidor en el que se despliegue la aplicación.

server.error.whitelabel.enabled=false
Páginas personalizadas

La forma más fácil de usar nuestras propias páginas de error es ponerlas en la carpeta /src/main/resources/public/error y nombrarlas con el código de estado de error HTTP que atiende cada una.

Se admite el formato 4xx.html y 5xx.html para abarcar todo el rango de códigos de la serie 400 y 500.

Se trata, pues, de páginas estáticas. Spring Boot también nos permite definir una página de error dinámica escrita con la tecnología que estemos empleando para generar el HTML (JSP, Thymeleaf, Velocity…) y que en el proyecto de ejemplo es JSP. El fichero debe llamarse error y ubicarse en la raíz del directorio en el que Spring Boot espera encontrar las páginas web dinámicas. En nuestro caso, sería /src/main/webapp/WEB-INF/jsp/error.jsp. (ver spring.mvc.view.prefix). En el caso de Thymeleaf, la carpeta predeterminada es /src/main/resources/templates.

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

</body>

</html>

Se puede ir más allá y crear una página de error personalizada en función del código de estado HTTP del error, tal y como vimos para HTML estático. Basta con nombrar a la página con el código HTTP, por ejemplo 404.jsp, y ubicarla dentro de un subdirectorio llamado error ( /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.

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

De momento, el contenido de las JSP de error es fijo. Vamos a «enriquecerlas» y mostrar los atributos que aparecen en la whitelabel page porque Spring Boot los escribe en el request.

Comprobémoslo imprimiendo estos valores en pantalla.

<ul>
        <li>status: <c:out value="${requestScope.status}" /></li>
        <li>timestamp: <c:out value="${requestScope.timestamp}" /></li>
        <li>error: <c:out value="${requestScope.error}" /></li>
        <li>exception: <c:out value="${requestScope.exception}" /></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>

Podemos añadir parámetros adicionales especializando la clase DefaultErrorAttributes, responsable de crear un Map con esos parámetros. Atención a la siguiente clase, el método a sobrescribir depende de la versión de Spring Boot.

@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {

    //para Spring Boot < 2.3
    /*@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;
    }*/


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

Obtenemos el Map original y añadimos nuevos valores: una variable jdk con la versión de Java.

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

Aseguramos que se use CustomErrorAttributes en lugar de DefaultErrorAttributes como implementación del bean errorAttributes que usa internamente el sistema de gestión de errores. Creamos una instancia en un método «factoría» (@Bean), llamado errorAttributes, en una clase de tipo @Configuration (nos sirve @SpringBootApplication).

@Bean
public CustomErrorAttributes errorAttributes() {
    return new CustomErrorAttributes();
}
API REST

Nuestra aplicación de ejemplo, además de páginas HTML, también dispondrá de una API REST basada en JSON. De nuevo, lanzamos una excepción.

@RestController
public class ForceErrorRestController {

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

Spring Boot devuelve la whitelabel error page en formato JSON, tal y como cabría esperar en un servicio REST.

Probamos su respuesta utilizando Postman.

Vemos que incluye el parámetro jdk porque se sigue ejecutando CustomErrorAtributes.

Implementando la url de error en Spring Boot

Se puede aumentar el control sobre la gestión de errores implementando un controlador para la url /error a la que Spring Boot redirecciona la petición (dentro del servidor) si se produce una excepción. Recordemos que whitelabel error page se muestra si esa url no está implementada. Es posible cambiarla con el parámetro server.error.path.

server.error.path=/error

Vamos con un ejemplo simple que muestra las páginas error.jsp y 404.jsp que ya tenemos en el proyecto. Escribamos un controlador para la url /error (o la de server.error.path) que implemente la interfaz ErrorController para indicar que la finalidad del controlador es renderizar páginas de error.

@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");
        
        return is404(request) ? "/error/404" : "error";
    } 

    private boolean is404(HttpServletRequest request)  {
        return HttpStatus.NOT_FOUND
                .value() == (int) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
    }

}

Para que error.jsp siga mostrando los mismos datos vamos a añadir al Model de Spring MVC los atributos que Spring Boot ya mostraba en la Whitelabel Page. Los obtenemos en nuestro controlador inyectando la abstracción ErrorAttributes y pidiéndolos para el WebRequest asociado a la petición que estamos procesando. Tanto el WebRequest como el Model los conseguimos añadiéndolos como argumentos al método.

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

        if (is404(request)) {
            return "/error/404";
        }

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

        return "error";
    }

Al obtenerse el Map con los detalles del error hay que solicitar de forma explícita los atributos que requieren su activación en el application.properties. Asimismo, CustomErrorAttributes sigue ejecutándose, pero después de handleError.

Siguiendo esta estrategia, y dejando de utilizar la gestión automática de errores de Spring Boot, encontramos un problema: los errores producidos en la API REST ahora no devuelven un JSON, sino el HTML generado por error.jsp.

Una solución consiste en leer de la cabecera de la petición HTTP el parámetro Accept que el solicitante debería haber establecido con el valor application/json Así, podríamos delegar la respuesta a otra url que devuelva un JSON usando una redirección interna (forward).

private boolean isREST(String accept) {
    return MediaType.APPLICATION_JSON.toString().equalsIgnoreCase(accept);
}
 
@RequestMapping("/error")
public String handleError(@RequestHeader("Accept") String accept, HttpServletRequest request, WebRequest webRequest, Model model) {
    logger.info("executing custom error controller");

    if (isREST(accept)) {
        return "forward:/errorJSON";
 }

Ese JSON lo modelamos con la nueva clase CustomErrorJson.

public class CustomErrorJson {

    private final int status;
    private final String error;
    private final String message;
    private final String path;
    private final String trace;

    public CustomErrorJson(int status, String error, String message, String path, String trace) {
        this.status = status;
        this.error = error;
        this.message = message;
        this.path = path;
        this.trace = trace;
    }

En CustomErrorController (u otra clase controladora si lo consideramos oportuno) escribimos el método que atiende a errorJSON.

 @RequestMapping("/errorJSON")
 @ResponseBody
 public CustomErrorJson handleErrorJson(HttpServletRequest request, WebRequest webRequest) {
        Map<String, Object> mapErrors = buildMapErrors(webRequest);
        int status = (int) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        return new CustomErrorJson(status,
                (String) mapErrors.get("error"),
                (String) mapErrors.get("message"),
                (String) mapErrors.get("path"),
                (String) mapErrors.get("trace"));
 }

Puesto que el controlador no está anotado con @RestController, es necesario marcar el método con @ResponseBody para que Spring sepa que retorna un objeto con la respuesta que tiene que enviar directamente al solicitante de la petición, en lugar del nombre de una vista JSP (o del tipo que estemos usando).

{
    "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

Lo cierto es que implementar un controlador genérico de errores en Spring Boot no nos aporta gran cosa. Recomiendo utilizar la gestión de errores estándar de Spring MVC que examina la siguiente sección.

Los manejadores de excepciones de Spring MVC

Además de los mecanismos de gestión de errores presentados en las secciones anteriores, en Spring Boot podemos seguir empleando las funcionalidades ofrecidas por Spring MVC con las que es posible conseguir una gestión de errores exhaustiva. La idea es procesar en métodos «especiales» ciertos tipos de excepciones lanzadas por determinados controladores.

En un controlador

Dentro de un controlador, podemos definir «manejadores» (handler) específicos para cualquier tipo de excepción que lancen los métodos del mismo. Lo que haremos será anotar con @ExceptionHandler un método para que procese uno o varios tipos de excepciones. Estas excepciones las indicamos con el atributo values de la anotación, y las recibimos, si queremos, como un argumento del método. Por omisión, el método se ejecutará para cualquier Throwable. Asimismo, los manejadores tendrán preferencia sobre la gestión de errores propia de Spring Boot (whitelable, /errors).

@RestController
public class ForceErrorRestController {

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

    @ExceptionHandler(value = IllegalArgumentException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String, Object> handleException(Exception ex) {
        Map<String, Object> map = new HashMap<>();
        map.put("message", ex.getMessage());
        map.put("timestamp", new Date());
        return map;
    }

}

Ahora, la excepción lanzada al llamar a /api/throwException causa la invocación de handleException quien se encargará de enviar la respuesta al cliente. El uso de Map permite crear «al vuelo» un JSON.

Por omisión, la respuesta de un manejador es de tipo 200 (OK). Especificamos el status code 500 con la anotación @ResponseStatus; también se podría devolver ResponseEntity e indicar en el mismo el código HTTP.

{
    "message": "\"I am the error message from Rest Controller\"",
    "timestamp": "2022-07-28T19:01:49.861+00:00"
}

Un aspecto a tener en cuenta es que los métodos manejadores de excepciones son parecidos a los que atienden a una url y podemos recibir como argumentos el Model, el request, etcétera.

¿Qué sucede con las jerarquías de excepciones? Lo siguiente es válido.

@ExceptionHandler(value = IllegalArgumentException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, Object> handleExceptionIllegal(Exception ex) {
    ...
}

@ExceptionHandler
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, Object> handleException(Exception ex) {
  ...
}

Spring es «inteligente» y solo ejecuta el manejador más específico para una excepción. Esto significa que la excepción IllegalArgumentException lanzada por /api/throwException se procesa en handleExceptionIllegal, de tal modo que handleException no se ejecuta.

Transversal a múltiples controladores

Las clases marcadas con @ControllerAdvice aplican sus métodos de tipo @ExceptionHandler a un conjunto de controladores que cumplan con lo definido en una de las siguientes propiedadades de la anotación.

  • basePackages. Controladores situados en algunos de los paquetes indicados, definidos por una cadena.
  • basePackagesClasses. Controladores situados en algunos de los paquetes indicados, definidos por Class.
  • assignableTypes. Controladores asignables a cualquiera de las clases indicadas por su Class.
  • annotations. Controladores con alguna de las anotaciones indicadas.

Si no se especifica este criterio de selección, el ControllerAdvice se aplicará a todos los controladores.

A la hora de trasladar la gestión de errores que hicimos en CustomErrorController a este nuevo sistema, debemos tener presente las siguientes limitaciones:

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

La siguiente clase es un gestor de excepciones genérico para todos los controladores de tipo @Controller (cuidado porque esto incluye a los @RestController. O los situados en el paquete y subpaquetes de ForceErrorJspController.

@ControllerAdvice(basePackageClasses = ForceErrorJspController.class)
//@ControllerAdvice(annotations=Controller.class)
public class ErrorJspControllerAdvice {
 
    private static final Logger logger = LoggerFactory.getLogger(ErrorJspControllerAdvice.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"));
        model.addAttribute("trace", ExceptionUtils.buildTrace(ex));

        return "error";
    }
    
}

Y esta es la clase equivalente para los controladores de la API REST (com.danielme.springboot.controllers.rest).

//@ControllerAdvice(basePackageClasses = ForceErrorRestController.class)
@ControllerAdvice(annotations = RestController.class)
public class ErrorRestControllerAdvice {

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

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public CustomErrorJson handleException(Exception ex, HttpServletRequest request) {
        logger.info("executing exception handler (REST)");

        return new CustomErrorJson(
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                ex.getClass().getName(),
                ex.getMessage(),
                request.getRequestURI(),
                ExceptionUtils.buildTrace(ex));
    }

}

@RestController incluye a @Controller. Por este motivo, para que no se ejecute ErrorJspControllerAdvice en vez de ErrorRestControllerAdvice, en ErrorJspControllerAdvice usé basePackageClasses.

Un detalle interesante es la prioridad de los manejadores para un mismo tipo de excepción: los definidos dentro de un controlador tienen preferencia sobre los declarados en los ControllerAdvice. A causa de este comportamiento, si queremos que la excepción lanzada en /api/throwException sea tratada por ErrorRestControllerAdvice, hay que quitar el manejador que pusimos en ForceErrorController

Excepción para 404

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

La clase CustomErrorController está procesando el error 404, y hará lo mismo para cualquier excepción que no trate ningún ExceptionHandler. Por tanto, funciona como la «última barrera» entre las excepciones y los clientes de la aplicación.

Se puede pedir a Spring que lance una excepción NoHandlerFoundException cuando no sepa cómo tratar una petición, situación que Spring interpreta como un error 404. Esta es la configuración.

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

El tratamiento de NotHandlerFoundException requiere de un ControllerAdvice global porque la excepción no la lanza uno de nuestros controladores, sino un componente llamado DispatcherServlet. Este elemento vital de Spring MVC es el responsable de atender las peticiones HTTP y decidir qué hacer con ellas, por ejemplo, invocar a un método de un controlador.

@ControllerAdvice
public class GlobalControllerAdvice {

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

    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public String handleException(HttpServletRequest request) {
        logger.info("processing NoHandlerFoundException");
        return "/error/404";
    }
    
}

Estamos devolviendo el HTML que genera 404.jsp. Pero dado que nuestro ejemplo también cuenta con una API REST, tendremos que repetir la estrategia de CustomErrorController si queremos devolver un JSON cuando la petición así lo solicite.

Otro inconveniente es que la opción spring.web.resources.add-mappings deshabilita la configuración automática que Spring Boot hace de los recursos estáticos definidos en /spring-boot-errors/src/main/resources/static/.

Demasiadas dificultades; no recomiendo esta estrategia. En su lugar, se puede 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, es fácil de configurar.

Ejemplo real

En mi tutorial introductorio a APIs REST con Spring Boot creé este servicio.

@PutMapping(value = "/{id}")
public ResponseEntity<Void> update(@PathVariable Long id,
                                       @RequestBody @Valid CountryRequest countryRequest) {
    boolean wasUpdated = countryService.update(id, countryRequest);
    return wasUpdated ? ResponseEntity.noContent().build() : ResponseEntity.notFound().build();
}

Su cometido es actualizar en la base de datos el contenido de una entidad de JPA llamada Country. Especificamos ese país proporcionando su identificador en la url. Si no existe, se retorna el código 404. Esto podemos saberlo haciendo que el método countryService#update (implementa la lógica de actualización) devuelva un lógico que indique si la entidad pudo actualizarse porque existía. Otra opción sería realizar la comprobación de existencia directamente en el método update de la API.

Sin embargo, gracias a lo que hemos aprendido en este tutorial, podemos optar por una solución más elegante y práctica haciendo uso del mecanismo de excepciones de Java.

@Transactional
public void update(Long id, CountryRequest countryRequest) {
    Country country = countryRepository
            .findById(id)
            .orElseThrow(() -> new EntityNotFoundException(Country.class, id));
    country.setName(countryRequest.getName());
    country.setPopulation(countryRequest.getPopulation());
}

Cuando el país no exista (findById devuelve un Optional), lanzamos una excepción creada por nosotros.

public class EntityNotFoundException extends RuntimeException {

    public EntityNotFoundException(Class clazz, Long id) {
        super("Entity " + clazz.getSimpleName() + " with id " + id + " not found ");
    }

}

No la capturaremos para dejar que «ascienda» hasta ser atrapada por un manejador de excepciones.

@ControllerAdvice
public class ExceptionControllerAdvice {

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

    @ExceptionHandler(EntityNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public void handleException(EntityNotFoundException ex) {
        logger.warn("Not found", ex);
    }

}

En consecuencia, cada vez que nos enfrentemos a esta casuística (devolver un 404 porque un elemento no exista), simplemente lanzaremos EntityNotFoundException y Spring Boot se encargará del resto.

Errores de validación

Vamos a crear un servicio REST que reciba mediante POST un JSON para un objeto de tipo Country. Sus atributos tienen restricciones definidas con las notaciones de Hibernate Validator.

public class Country {

    private Integer id;
    
    @NotNull
    private String name;
    
    @NotNull
    @Min(1)
    @Max(2000000000)
    private Integer population;
@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) {
       //nothing here
    }

}

Conseguir que Spring compruebe automáticamente las validaciones de la clase Country sobre el objeto recibido es trivial, pues solo hay añadir a la declaración del argumento la anotación @Valid. Si hay algún error, se lanza la excepción MethodArgumentNotValidException y la respuesta del servicio será este JSON, siempre y cuando no tratemos los errores en un controlador de tipo @ControllerAdvice o ErrorController.

{
    "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"
        },

Demasiada información, ¡y solo es una parte del mensaje! Mejor personalizar la respuesta y construir un mensaje más simple y claro. La técnica ya la conocemos: crear un @ExceptionHandler para MethodArgumentNotValidException.

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

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

}

La excepción contiene el detalle de cada error de validación. Iteramos y los volcamos en objetos de tipo ValidationError que informen de los atributos erróneos y el motivo.

[
    {
        "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

Deja una respuesta

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. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

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