Filtro CPL Polarizado ventajas

Error Handling REST API in Spring Boot with @ControllerAdvice @ExceptionHandler

 En esta entrada veremos como implementar un error handling usando las anotaciones @ControllerAdvice y @ExceptionHandler de Spring Boot extendiendo de ExceptionHandlerExceptionResolver.

 


Nuestras respuestas serán personalizadas.

Validaremos los headers, body, tipo de dato en el header, el path, parámetros en el body y tipo, tipo de solicitud HTTP, etc.

Implementaremos los errores para las siguientes excepciones:

  • HttpRequestMethodNotSupportedException
         Se ejecutara cuando el tipo de método HTTP (POST,GET,PUT, etc.) no sea el que el endpoint está esperando.
  • HttpMediaTypeNotSupportedException
           Se ejecutará cuando el contenido de la petición no sea soportado y esperado por el endpoint.
  • MissingRequestHeaderException
            Se ejecutará cuando no existan los headers requeridos por el endpoint.
  • HttpMessageNotReadableException
            Se ejecutará cuando el body venga incompleto y sea requerido por el endpoint.
  • MethodArgumentNotValidException
            Se ejecutará cuando en el body los datos no sean del tipo requerido, se detona cuando @Valid detecta errores.
  • ConstraintViolationException
            Se ejecutará cuando en el body falten parámetros o vengan vacíos.
  • MethodArgumentTypeMismatchException
            Se ejecutará cuando en el valor de un header no es del tipo correcto requerido. Ejemplo, cuando envió un dato int en lugar de un String.
  • NoHandlerFoundException
            Se ejecutará cuando la ruta o path no exista o que un endpoint no coincida con el nombre.  

 En otras palabras, cuando una de estas excepciones ocurra en nuestra API, automáticamente nuestro error handler detonará con una respuesta personalizada que definamos.

Nuestra respuesta tendrá la siguiente estructura:

Tendrá un mensaje y un arreglo de detalles de tipo String.
package com.spring.remote.model;
import java.util.ArrayList;
import java.util.List;
public class ApiError {
private String mensaje;
private List<String> detalles;
public ApiError() {
detalles = new ArrayList<>();
}
public void addDetalle(String detalle) {
detalles.add(detalle);
}
public String getMensaje() {
return mensaje;
}
public void setMensaje(String mensaje) {
this.mensaje = mensaje;
}
public List<String> getDetalles() {
return detalles;
}
public void setDetalles(List<String> detalles) {
this.detalles = detalles;
}
}
view raw ApiError.java hosted with ❤ by GitHub

Nuestro endpoint o Controller es el siguiente
package com.spring.remote.controllers;
import java.util.List;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import com.spring.remote.model.Persona;
import com.spring.remote.services.ServicePersona;
@RestController
@Validated
@CrossOrigin("*")
public class ControllerPersona {
@Autowired
ServicePersona servicePersona;
@GetMapping("/personas")
public ResponseEntity<List<Persona>> getAllPersonas(){
List<Persona> personas = servicePersona.getPersonas();
return new ResponseEntity<>(personas, new HttpHeaders(), HttpStatus.ACCEPTED);
}
@PutMapping("/persona/{id}")
public ResponseEntity<Object> updatePersonById(@RequestBody Persona persona, @PathVariable("id") int id) {
System.out.println(id + " " + persona.getNombre());
servicePersona.updatePersonById(persona, id);
return new ResponseEntity<>(HttpStatus.ACCEPTED);
}
@DeleteMapping("/persona/{id}")
public ResponseEntity<Object> deletePersonById(@PathVariable int id){
servicePersona.deletePersonById(id);
return new ResponseEntity<>(HttpStatus.ACCEPTED);
}
@PostMapping("/persona")
public ResponseEntity<Object> insertPerson(
@RequestHeader(name = "idPais", required = true) int idPais,
@Valid @RequestBody(required = true) List<Persona> personas){
personas.forEach(p -> {
System.out.println(p.getId());
System.out.println(p.getNombre());
System.out.println(p.getApellido());
p.getListTelefonos().forEach(telefono -> {
System.out.println("\t " + telefono);
});
System.out.println(p.getCiudad());
System.out.println(p.getDomicilio());
System.out.println(p.getRfc());
System.out.println(p.getCurp());
});
return ResponseEntity.status(HttpStatus.CREATED).body("Creado correctamente");
}
}

Para este ejemplo iremos haciendo la prueba con el endpoint insertPerson(), tanto el header como el body son requeridos, sólo requerimos un header de tipo int y se llama idPais.
En el objeto Persona he colocado las siguientes anotaciones para validar todos los parámetros del body
package com.spring.remote.model;
import java.util.List;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class Persona {
@NotNull
private int id;
@NotEmpty
private String nombre;
@NotEmpty
private String apellido;
@NotEmpty
private List<String> listTelefonos;
@NotEmpty
private String ciudad;
@NotEmpty
private String domicilio;
@NotEmpty
private String rfc;
@NotEmpty
private String curp;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public String getApellido() {
return apellido;
}
public void setApellido(String apellido) {
this.apellido = apellido;
}
public List<String> getListTelefonos() {
return listTelefonos;
}
public void setListTelefonos(List<String> listTelefonos) {
this.listTelefonos = listTelefonos;
}
public String getCiudad() {
return ciudad;
}
public void setCiudad(String ciudad) {
this.ciudad = ciudad;
}
public String getDomicilio() {
return domicilio;
}
public void setDomicilio(String domicilio) {
this.domicilio = domicilio;
}
public String getRfc() {
return rfc;
}
public void setRfc(String rfc) {
this.rfc = rfc;
}
public String getCurp() {
return curp;
}
public void setCurp(String curp) {
this.curp = curp;
}
}
view raw Persona.java hosted with ❤ by GitHub
@NotEmpty nos sirve para indicarle que no deben venir null o vacíos.
 
Finalmente nuestra clase de excepciones es la siguiente:
package com.spring.remote.exceptions;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ElementKind;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.tags.form.ErrorsTag;
import com.spring.remote.model.ApiError;
@ControllerAdvice
public class RestException extends ExceptionHandlerExceptionResolver{
//cuando el tipo HTTP no es correcto
@ExceptionHandler({HttpRequestMethodNotSupportedException.class})
public ResponseEntity<ApiError> methodNotSupportedException(HttpRequestMethodNotSupportedException ex){
ApiError error = new ApiError();
error.setMensaje("HTTP " + ex.getMethod() + " NO soportado. Debe ser " +ex.getSupportedMethods()[0] + " para el endpoint solicitado");
error.setDetalles(Arrays.asList(""+ex));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
//cuando el tipo de datos enviado no es aceptado
@ExceptionHandler({ HttpMediaTypeNotSupportedException.class })
public ResponseEntity<ApiError> notAcceptableMediaTypeHandler(HttpMediaTypeNotSupportedException ex) {
ApiError error = new ApiError();
error.setMensaje(ex.getMessage());
error.setDetalles(Arrays.asList(""+ex));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
//cuando no existe el header requerido
@ExceptionHandler({ MissingRequestHeaderException.class })
public ResponseEntity<ApiError> MissingRequestHeaderException(MissingRequestHeaderException ex) {
ApiError error = new ApiError();
error.setMensaje("No existe " + ex.getParameter().getParameterName() + " en el header");
error.setDetalles(Arrays.asList(""+ex));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
//cuando el body no es correcto
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiError> handleAllOtherErrors(HttpMessageNotReadableException ex) {
ApiError error = new ApiError();
error.setMensaje(ex.getMessage());
error.setDetalles(Arrays.asList(""+ex));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
//cuando @Valid tiene errores en validacion del body
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> MethodArgumentNotValidException(MethodArgumentNotValidException ex) {
ApiError error = new ApiError();
error.setMensaje(ex.getMessage());
error.setDetalles(Arrays.asList(""+ex));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
//cuando valores del body estan vacios o no existen
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiError> ConstraintViolationException(ConstraintViolationException ex) {
ApiError error = new ApiError();
error.setMensaje("Body no valido. Validar datos");
ex.getConstraintViolations().stream().forEach(v -> {
v.getPropertyPath().forEach(e ->{
if(e.getKind() == ElementKind.PROPERTY)
error.addDetalle(e.getName()+ " debe existir y no estar vacio");
});
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
//cuando el tipo de dato del header no coincide
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ApiError> MethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex) {
ApiError error = new ApiError();
error.setMensaje("El header " + ex.getName() + " debe der de tipo " + ex.getRequiredType());
error.setDetalles(Arrays.asList(""+ex));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
//cuando el path no existe
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<ApiError> noHandlerFoundException(NoHandlerFoundException ex) {
ApiError error = new ApiError();
error.setMensaje("Ruta " + ex.getRequestURL() + " no existe");
error.setDetalles(Arrays.asList(""+ex));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
//cuando hay un error SQL
@ExceptionHandler(SQLException.class)
public ResponseEntity<ApiError> exception(SQLException ex){
ApiError error = new ApiError();
error.setMensaje(ex.getMessage());
error.setDetalles(Arrays.asList(""+ex));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
}
En el caso de la excepción NoHandlerFoundException tenemos que configurar en el application.properties las siguientes lineas para que pueda responder correctamente:
Esto sería todo de mi parte. Cualquier duda o pregunta con gusto estaré respondiendo.
 
Dejo el link hacia el proyecto completo en github
 

 

 
  


Comentarios