Tratamento Global de Erros em Spring - Básico
Nesse post rápido vou lhe ensinar como criar um tratamento de erros global no spring boot. Para um artigo avançado sobre o tema veja esse post
Basicamente precisamos criar um ControllerAdvice que irá interceptar exceções de tipos especificos, ou todas as exceções se quiser capturar uma RuntimeException.
Crie o arquivo em sua camada de controladores (exemplo package controller)
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* todo translate message codes
* Handles all DomainException
* @param ex the exception to handle
* @return ErroMessage
*/
@ExceptionHandler(DomainException.class)
public ResponseEntity<ErrorResponse> handleAllDomainException(DomainException ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse(ex.getMessage()));
}
}
Nesse caso estamos capturando um tipo de erro chamado DomainException, que é uma classe da aplicação (package domain):
public class DomainException extends RuntimeException {
@Getter
private final Message domainMessage;
public DomainException(Message domainMessage) {
super(domainMessage.toString());
this.domainMessage = domainMessage;
}
}
Nosso ErrorResponse é um objeto de resposta (DTO) que está na camada de controladores:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;
@Value
public class ErrorResponse {
private final String message;
@JsonCreator
public ErrorResponse(@JsonProperty("message") String message) {
this.message = message;
}
}
Note que essa classe aqui é Imutável, pois não faz sentido modificar o valor de message após criada. Para isso usamos o @JsonCreator para dizer qual é o contrutor a ser usado pelo Jackson Mapper para serializar para json. Caso contrário ele falha sem um construtor padrão.
Nota: O tipo de retorno ResponseEntity só funcionará em aplicações REST. Para aplicações diferentes (MVC por exemplo). Pode-se retornar um ModelAndView que irá renderizar um template html por exemplo.
Você pode interceptar outros tipos de erro como por exemplo DataIntegrityViolationException que é lançada quando há uma constraint no banco que não foi respeitada (unique, not null, etc..). Ou mesmo interceptar todos os Errors
Abaixo segue um exemplo mais completo de ControlerAdvice:
@ControllerAdvice
public class ExceptionHandlerConfig extends ResponseEntityExceptionHandler {
private final Logger logger;
private final PropertiesMessageProxy messageProxy;
public ExceptionHandlerConfig(Logger logger, PropertiesMessageProxy messageProxy) {
this.logger = logger;
this.messageProxy = messageProxy;
}
/**
* Validates all Runtime exceptions, transforming to ApiError with generic message
*
* @param e Exception
* @return ApiError for Json consumption
*/
@ResponseStatus(INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
@ResponseBody
public ApiError handleAll(Exception e) {
logger.error(e.getMessage(), e);
return ApiError.fromMessage(INTERNAL_SERVER_ERROR,
messageProxy.messageFromKey(MessageConstants.APPLICATION_EXCEPTION_GENERIC));
}
@ResponseStatus(UNPROCESSABLE_ENTITY)
@ExceptionHandler(DomainBusinessException.class)
@ResponseBody
public ApiError handleBusinessException(DomainBusinessException e) {
if (!e.getMessageConstants().isPresent()) {
return ApiError.fromException(UNPROCESSABLE_ENTITY, e);
} else {
return ApiError.fromMessage(UNPROCESSABLE_ENTITY,
messageProxy.messageFromKey(e.getMessageConstants().get()));
}
}
/**
* Handle @Valid exceptions in methods with @RequestBody
*
* @param ex MethodException
* @param headers Default HttpHeaders
* @param status Status of the request
* @param request The web request
* @return ApiError for Json consumption
*/
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
return ResponseEntity.badRequest().body(ApiError.fromMessage(HttpStatus.BAD_REQUEST,
messageProxy.getMessageFromListErrors(ex.getBindingResult().getAllErrors())));
}
/**
* @return ApiError for Json consumption
*/
@ExceptionHandler({ConstraintViolationException.class})
public ResponseEntity<ApiError> handleConstraintViolation(
ConstraintViolationException ex) {
List<String> errors = new ArrayList<>();
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
errors.add(violation.getPropertyPath() + ": " + violation.getMessage());
}
ApiError apiError =
ApiError.fromMessage(HttpStatus.BAD_REQUEST, errors);
return ResponseEntity.badRequest().body(apiError);
}
@Data // lombok annotation
class ApiError implements Serializable {
private Integer statusCode;
private String error;
private List<String> messages;
private ApiError(Integer statusCode, String error, String message) {
this(statusCode, error,
Objects.nonNull(message) ? Collections.singletonList(message) : Collections.emptyList());
}
private ApiError(Integer statusCode, String error, List<String> messages) {
this.statusCode = statusCode;
this.error = error;
this.messages = messages;
}
public ApiError() {
this.messages = Collections.emptyList();
}
public static ApiError fromException(HttpStatus httpStatus, Exception exception) {
return fromException(httpStatus, exception, httpStatus.getReasonPhrase());
}
public static ApiError fromException(HttpStatus httpStatus, Exception exception,
String errorIdentifier) {
return new ApiError(httpStatus.value(), errorIdentifier, exception.getMessage());
}
public static ApiError fromMessage(HttpStatus httpStatus, String message) {
return new ApiError(httpStatus.value(), httpStatus.getReasonPhrase(), message);
}
public static ApiError fromMessage(HttpStatus httpStatus, List<String> message) {
return new ApiError(httpStatus.value(), httpStatus.getReasonPhrase(), message);
}
Não cobrimos:
Internacionalização e codigos de erro
Hierarquia bem definida de Exceptions
É isso espero que este post tenha sido útil.