Tratamento de erros personalizados para APIs rest com Spring Boot.
Tags: java, spring, springboot, rest
Published: 2020-12-20 01:05:48

Hello guys! Aqui quem vos escreve é o Ledson Silva, desenvolvedor de software desde meados de 2014 e o objetivo desta publicação é demonstrar como faço para implementar tratamento de erros personalizados para api rest no spring boot.

Atualmente o spring tem uma resposta genérica para caso de erros e que na maioria das vezes queremos devolver para os clients uma resposta mais elaborada, que tenha mais informações e principalmente quando estamos trabalhando com javax validation elaborar uma resposta elegante.

Ah!!! e além de implementar uma resposta personalizada, também vou mostrar como faço para organizar minhas exceptions e as mensagens de erros que minhas apis devolvem de resposta.

Vamos lá!!

OBS: Neste exemplo estou usando Java 11 e nosso projeto spring tem as seguintes dependencias:

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

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.5</version>
</dependency>
```

Passo 01: No primeiro passo iremos criar dois arquivos, onde será colocado nossas mensagens de erros. Faremos algo com esta estrutura dentro do pacote resources:

![image](https://user-images.githubusercontent.com/20303042/102701784-66312d00-4239-11eb-8202-d0c46abc16ef.png)

No arquivo `business.properties` concentraremos as mensagems de regras de negócio, e no arquivo `validation.properties` serão as mensagens de validações de dados.

Passo 02: Criar o arquivo de configuração para o spring recuperar estes arquivos de mensagens, no caso iremos apenas utilizar o esquema de internacionalização do spring.

`MessageConfiguration.java`
```java
@Configuration
public class MessageConfiguration implements WebMvcConfigurer {
    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasenames("classpath:/messages/business/business", "classpath:/messages/validation/validation");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }

    @Bean
    public LocalValidatorFactoryBean validator(MessageSource messageSource) {
        LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
        bean.setValidationMessageSource(messageSource);
        return bean;
    }

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver localeResolver = new SessionLocaleResolver();
        localeResolver.setDefaultLocale(Locale.US);
        return localeResolver;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }
}
```

Passo 03: Criar a estrutura dos erros personalizados, no caso os dtos que retornaremos em nossa api:

`ErrorDTO.java`
```java
public class ErrorDTO {
    private String key;
    private String message;

    public ErrorDTO() {
    }

    public ErrorDTO(String key, String message) {
        this.key = key;
        this.message = message;
    }

    ... getters and setters
}
```

`ApiErrorDTO.java`
```java
public class ApiErrorDTO {
    private Date timestamp;
    private Integer status;
    private String code;
    private Set<ErrorDTO> errors;

    public ApiErrorDTO() {
    }

    public ApiErrorDTO(Date timestamp, Integer status, String code, Set<ErrorDTO> errors) {
        this.timestamp = timestamp;
        this.status = status;
        this.code = code;
        this.errors = errors;
    }

    ... getters and setters
}
```

Passo 04: No quarto passo criaremos nossa exception base, que utilizaremos para estende-las ao criar nossas exceptions de negócio.

`MessageException.java`
```java
public interface MessageException {
    String getExceptionKey();
    Map<String, Object> getMapDetails();
}
```

`BaseRuntimeException.java`
```java
public abstract class BaseRuntimeException extends RuntimeException implements MessageException {
    private final Map<String, Object> mapDetails;

    public BaseRuntimeException() {
        mapDetails = null;
    }
    public BaseRuntimeException(final Map<String, Object> mapDetails) {
        this.mapDetails = mapDetails;
    }

    public abstract String getExceptionKey();

    public Map<String, Object> getMapDetails() {
        return this.mapDetails;
    }
}
```

Passo 05: Aqui está o pulo do gato!! criaremos a nossa clase que será responsável por interceptar as exceptions e transforma-las em nossa resposta personalizada conforme os dtos que criamos anteriormente.

`ExceptionHandlerAdvice.java`
```java
@ControllerAdvice
public class ExceptionHandlerAdvice {
    private static final String UNKNOWN_ERROR_KEY = "unknown.error";

    private static final Logger logger = LoggerFactory.getLogger(ExceptionHandlerAdvice.class);
    private final MessageSource messageSource;

    public ExceptionHandlerAdvice(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiErrorDTO> handlerMethodArgumentNotValid(
            MethodArgumentNotValidException exception
    ) {
        logger.error("Exception {}, Message: {}", exception.getClass().getName(), exception.getMessage());
        Set<ErrorDTO> errors = exception.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(error -> buildError(error.getCode(), error.getDefaultMessage()))
                .collect(Collectors.toSet());

        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(baseErrorBuilder(HttpStatus.BAD_REQUEST, errors));
    }

    @ExceptionHandler(BaseRuntimeException.class)
    public ResponseEntity<ApiErrorDTO> handlerBaseException(Throwable exception) {
        logger.error("Exception {}", exception.getClass().getName());
        MessageException messageException = (MessageException) exception;
        ErrorDTO error = buildError(messageException.getExceptionKey(),
                bindExceptionKeywords(messageException.getMapDetails(),messageException.getExceptionKey()));

        Set<ErrorDTO> errors = Set.of(error);
        ApiErrorDTO apiErrorDto = baseErrorBuilder(getResponseStatus(exception), errors);

        return ResponseEntity
                .status(getResponseStatus(exception))
                .body(apiErrorDto);
    }

    @ExceptionHandler(Throwable.class)
    public ResponseEntity<ApiErrorDTO> handlerMethodThrowable(Throwable exception) {
        logger.error("Exception {}, Message: {}", exception.getClass().getName(), exception.getMessage());
        Set<ErrorDTO> errors = Set.of(buildError(UNKNOWN_ERROR_KEY, exception.getMessage()));
        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(baseErrorBuilder(HttpStatus.INTERNAL_SERVER_ERROR, errors));
    }

    private ErrorDTO buildError(String code, String message) {
        return new ErrorDTO(code, message);
    }

    private ApiErrorDTO baseErrorBuilder(HttpStatus httpStatus, Set<ErrorDTO> errorList) {
        return new ApiErrorDTO(
                new Date(),
                httpStatus.value(),
                httpStatus.name(),
                errorList);
    }

    private String bindExceptionKeywords(Map<String, Object> keywords, String exceptionKey) {
        String message = messageSource.getMessage(exceptionKey, null, LocaleContextHolder.getLocale());
        return Objects.nonNull(keywords) ? new StrSubstitutor(keywords).replace(message) : message;
    }

    private HttpStatus getResponseStatus(Throwable exception) {
        ResponseStatus responseStatus = exception.getClass().getAnnotation(ResponseStatus.class);
        if (exception.getClass().getAnnotation(ResponseStatus.class) == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return responseStatus.value();
    }
}
```

Isso já é suficiente para transformar nossas exceptions em nossa resposta personalizada. E agora iremos criar uma estrutura com uma Controller e um DTO para requests, onde teremos dois endpoints `POST` mesmo um para exemplificar erros de validação e outro para erros de negócio!

`ExampleDTO.java`
```java
public class ExampleDTO {
    @NotNull(message = "{required.validation}")
    private Long id;

    @NotBlank(message = "{required.validation}")
    @Size(min = 4, max = 30, message = "{size.validation}")
    private String name;

    public ExampleDTO() {
    }

    public ExampleDTO(Long id, String name) {
        this.id = id;
        this.name = name;
    }
     
    ... getters and setters
}
```

ExampleExceptionController.java
```java
@RestController
@RequestMapping(path = "custom-exception-example")
public class ExampleExceptionController {
    @PostMapping(path = "validation")
    public ResponseEntity<ExampleDTO> exampleModelValidationEndpoint(@Validated @RequestBody ExampleDTO dto) {
        return ResponseEntity.ok(dto);
    }

    @PostMapping(path = "business")
    public ResponseEntity<String> exampleBusinessValidationEndpoint(@Validated @RequestBody ExampleDTO dto) {
        if (dto.getName().equalsIgnoreCase("params")) {
            throw new ExampleNameRuleWithParamsException("params");
        }

        if (!dto.getName().equalsIgnoreCase("business")) {
            throw new ExampleNameRuleException();
        }
        return ResponseEntity.ok("Success!");
    }
}
```

E por último e não menos importante são as mensagens nos arquivos que criamos no primeiro passo.

`business.properties`
```
example.name.rule=No campo name somente é permitido o valor: business
example.name.rule.with.params=Não é permitido digitar o valor: ´${value}´ no campo name
```

`validation.properties`
```
required.validation=Existem campos obrigatórios que não foram preenchidos
size.validation=Tamanho inválido! Digite no mínimo {min} e no máximo {max} caracteres
```

Só uma explicação!! Em nosso DTO de exemplo quando temos uma anotação de validation, por exemplo `@NotBlank` é necessário definir a chave na qual irá representar em seu arquivo de mensagem por exemplo:

`@NotNull(message = "{required.validation}")`

E quando for exceptions de negócio, no qual precisaremos criar uma exception que estende de `BaseRuntimeException.java` a mesma nos obriga a implementar o método `String getExceptionKey()`, e será neste método que retornaremos nossa chave correspondente em nosso arquivo `business.properties`.

Exemplo:

`ExampleNameRuleException.java`
```java
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class ExampleNameRuleException extends BaseRuntimeException {
    private static final String KEY = "example.name.rule";

    public ExampleNameRuleException() {
        super();
    }

    @Override
    public String getExceptionKey() {
        return KEY;
    }
}
```

`ExampleNameRuleWithParamsException.java`
```java
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class ExampleNameRuleWithParamsException extends BaseRuntimeException {
    private static final String KEY = "example.name.rule.with.params";

    public ExampleNameRuleWithParamsException(String value) {
        super(Map.of("value", value));
    }

    @Override
    public String getExceptionKey() {
        return KEY;
    }
}
```

Entendido??!!! se não deixa um comentário aqui que trocaremos uma idéia.

Beleza galera!!! temos nossa implementação, e no final meu projeto ficou com esta estrutura:

![image](https://user-images.githubusercontent.com/20303042/102702445-8238cc80-4241-11eb-9b54-ec23fc837d51.png)

E por fim segue os prints dos testes:

Erros de validação:
![image](https://user-images.githubusercontent.com/20303042/102702584-3f77f400-4243-11eb-95b7-d713aee484af.png)

Erros de negócio:
![image](https://user-images.githubusercontent.com/20303042/102702600-69311b00-4243-11eb-8207-271cfa2542a0.png)

![image](https://user-images.githubusercontent.com/20303042/102702615-87971680-4243-11eb-8125-723ec71e4a30.png)

Porque eu gosto desta abordagem?? cara desta forma eu consigo ter um padrão de resposta de erros para minha `api`, tanto para erros de validações, obrigatoriedade etc.. quanto para erros de negócio!! além de também separar as mensagens de erros em arquivos especificos e com chaves que podem ser reutilizadas pelo sistema!!! quando precisar mudar uma mensagem de erro não preciso dar um `ctrl + f` no projeto e sair alterando em vários arquivos e códigos... fica concentrado apenas nos arquivos das mensagens.

Por enquanto é isso galera!! esta é a forma com que gosto de organizar meus projetos em relação aos erros de API e espero que tenham gostado da implementação.

Todos os arquivos citados no post pode ser encontrado em meu [github](https://github.com/ledsonsilva/spring-boot-custom-exceptions).