Tratamento de erros para REST com Spring

Tratamento de erros para REST com Spring

1. Visão geral

Este artigo ilustraráhow to implement Exception Handling with Spring for a REST API. Também teremos uma visão geral histórica e veremos quais novas opções as diferentes versões introduziram.

Before Spring 3.2, the two main approaches to handling exceptions in a Spring MVC application were: HandlerExceptionResolver or the @ExceptionHandler annotation. Ambos têm algumas desvantagens claras.

Since 3.2 we’ve had the @ControllerAdvice annotation para abordar as limitações das duas soluções anteriores e para promover um tratamento de exceção unificado em todo o aplicativo.

Agora,Spring 5 introduces the ResponseStatusException class: uma maneira rápida de tratamento básico de erros em nossas APIs REST.

Todos eles têm uma coisa em comum - lidam muito bem com osseparation of concerns. O aplicativo pode lançar uma exceção normalmente para indicar algum tipo de falha - exceções que serão tratadas separadamente.

Finalmente, veremos o que Spring Boot traz para a mesa e como podemos configurá-lo para atender às nossas necessidades.

Leitura adicional:

Tratamento de mensagens de erro personalizadas para a API REST

Implemente um manipulador de exceção global para uma API REST com Spring.

Read more

Guia para validadores REST de dados Spring

Guia rápido e prático para os validadores REST da Spring Data

Read more

Validação personalizada do Spring MVC

Aprenda a criar uma anotação de validação personalizada e usá-la no Spring MVC.

Read more

2. Solução 1 - O nível do controlador@ExceptionHandler

A primeira solução funciona no nível@Controller - definiremos um método para lidar com exceções e anotaremos isso com@ExceptionHandler:

public class FooController{

    //...
    @ExceptionHandler({ CustomException1.class, CustomException2.class })
    public void handleException() {
        //
    }
}

Essa abordagem tem uma grande desvantagem -the @ExceptionHandler annotated method is only active for that particular Controller, não globalmente para o aplicativo inteiro. Obviamente, adicionar isso a todos os controladores não o torna adequado para um mecanismo geral de manipulação de exceções.

Podemos contornar essa limitação tendoall Controllers extend a Base Controller class - no entanto, isso pode ser um problema para aplicativos onde, por qualquer motivo, isso não é possível. Por exemplo, os Controladores já podem se estender de outra classe base que pode estar em outro jar ou não diretamente modificável, ou pode não ser diretamente modificável.

A seguir, veremos outra maneira de resolver o problema de tratamento de exceções - uma que seja global e não inclua nenhuma alteração nos artefatos existentes, como controladores.

3. Solução 2 -The HandlerExceptionResolver

A segunda solução é definir umHandlerExceptionResolver - isso resolverá qualquer exceção lançada pelo aplicativo. Também nos permitirá implementar umuniform exception handling mechanism em nossa API REST.

Antes de ir para um resolvedor personalizado, vamos rever as implementações existentes.

3.1. ExceptionHandlerExceptionResolver

Este resolvedor foi introduzido no Spring 3.1 e é habilitado por padrão noDispatcherServlet. Este é realmente o componente principal de como funciona o mecanismo @ExceptionHandler apresentado anteriormente.

3.2. DefaultHandlerExceptionResolver

Este resolvedor foi introduzido no Spring 3.0 e é habilitado por padrão noDispatcherServlet. É usado para resolver exceções Spring padrão para seus códigos de status HTTP correspondentes, ou seja, erro do cliente -4xxe erro do servidor - códigos de status5xx. Here’s the full list das exceções Spring que ele trata e como elas são mapeadas para códigos de status.

Embora defina o código de status da resposta corretamente, umlimitation is that it doesn’t set anything to the body of the Response. E para uma API REST - o Código de Status não é realmente uma informação suficiente para apresentar ao Cliente - a resposta também precisa ter um corpo, para permitir que o aplicativo forneça informações adicionais sobre a falha.

Isso pode ser resolvido configurando a resolução da visualização e renderizando o conteúdo de erro por meio deModelAndView, mas a solução claramente não é a ideal. É por isso que o Spring 3.2 apresentou uma opção melhor que discutiremos em uma seção posterior.

3.3. ResponseStatusExceptionResolver

Este resolvedor também foi introduzido no Spring 3.0 e é habilitado por padrão noDispatcherServlet. Sua principal responsabilidade é usar a anotação@ResponseStatus disponível em exceções personalizadas e mapear essas exceções para códigos de status HTTP.

Essa exceção personalizada pode se parecer com:

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class MyResourceNotFoundException extends RuntimeException {
    public MyResourceNotFoundException() {
        super();
    }
    public MyResourceNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
    public MyResourceNotFoundException(String message) {
        super(message);
    }
    public MyResourceNotFoundException(Throwable cause) {
        super(cause);
    }
}

Da mesma forma queDefaultHandlerExceptionResolver, este resolvedor é limitado na maneira como lida com o corpo da resposta - ele mapeia o código de status na resposta, mas o corpo ainda énull.

3.4. SimpleMappingExceptionResolver e AnnotationMethodHandlerExceptionResolver

OSimpleMappingExceptionResolver já existe há algum tempo - ele vem do modelo Spring MVC mais antigo e énot very relevant for a REST Service. Basicamente, usamos para mapear nomes de classes de exceção para visualizar nomes.

OAnnotationMethodHandlerExceptionResolver foi introduzido no Spring 3.0 para lidar com exceções por meio da anotação@ExceptionHandler, mas foi descontinuado porExceptionHandlerExceptionResolver a partir do Spring 3.2.

3.5. HandlerExceptionResolver personalizado

A combinação deDefaultHandlerExceptionResolvereResponseStatusExceptionResolver ajuda muito a fornecer um bom mecanismo de tratamento de erros para um Spring RESTful Service. A desvantagem é - como mencionado antes -no control over the body of the response.

Idealmente, gostaríamos de poder produzir JSON ou XML, dependendo do formato que o cliente solicitou (por meio do cabeçalhoAccept).

Isso por si só justifica a criação dea new, custom exception resolver:

@Component
public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver {

    @Override
    protected ModelAndView doResolveException(
      HttpServletRequest request,
      HttpServletResponse response,
      Object handler,
      Exception ex) {
        try {
            if (ex instanceof IllegalArgumentException) {
                return handleIllegalArgument((IllegalArgumentException) ex, response, handler);
            }
            ...
        } catch (Exception handlerException) {
            logger.warn("Handling of [" + ex.getClass().getName() + "]
              resulted in Exception", handlerException);
        }
        return null;
    }

    private ModelAndView
      handleIllegalArgument(IllegalArgumentException ex, HttpServletResponse response)
      throws IOException {
        response.sendError(HttpServletResponse.SC_CONFLICT);
        String accept = request.getHeader(HttpHeaders.ACCEPT);
        ...
        return new ModelAndView();
    }
}

Um detalhe a notar aqui é que temos acesso ao própriorequest, portanto podemos considerar o valor do cabeçalhoAccept enviado pelo cliente.

Por exemplo, se o cliente pedeapplication/json, então, no caso de uma condição de erro, gostaríamos de ter certeza de retornar um corpo de resposta codificado comapplication/json.

O outro detalhe de implementação importante é quewe return a ModelAndView – this is the body of the responsee nos permitirá definir o que for necessário nele.

Essa abordagem é um mecanismo consistente e facilmente configurável para o tratamento de erros de um Serviço Spring REST. No entanto, ele tem limitações: está interagindo com oHtttpServletResponse de baixo nível e se encaixa no antigo modelo MVC que usaModelAndView - então ainda há espaço para melhorias

4. Solução 3 -@ControllerAdvice

O Spring 3.2 traz suporte paraa global @ExceptionHandler with the @ControllerAdvice annotation. Isso permite um mecanismo que rompe com o modelo MVC mais antigo e faz uso deResponseEntity junto com a segurança de tipo e flexibilidade de@ExceptionHandler:

@ControllerAdvice
public class RestResponseEntityExceptionHandler
  extends ResponseEntityExceptionHandler {

    @ExceptionHandler(value
      = { IllegalArgumentException.class, IllegalStateException.class })
    protected ResponseEntity handleConflict(
      RuntimeException ex, WebRequest request) {
        String bodyOfResponse = "This should be application specific";
        return handleExceptionInternal(ex, bodyOfResponse,
          new HttpHeaders(), HttpStatus.CONFLICT, request);
    }
}


A anotação@ControllerAdvice nos permiteconsolidate our multiple, scattered @ExceptionHandlers from before into a single, global error handling component.

O mecanismo real é extremamente simples, mas também muito flexível. Isso nos dá:

  • Controle total sobre o corpo da resposta, bem como o código de status

  • Mapeamento de várias exceções ao mesmo método, para serem tratadas em conjunto e

  • Faz bom uso da resposta RESTfulResposeEntity mais recente __

Uma coisa a ter em mente aqui ématch the exceptions declared with @ExceptionHandler with the exception used as the argument of the method. Se eles não corresponderem, o compilador não reclamará - não há razão para isso, e o Spring também não reclamará.

No entanto, quando a exceção é realmente lançada no tempo de execução,the exception resolving mechanism will fail with:

java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]
HandlerMethod details: ...

5. Solução 4 -ResponseStatusException (Primavera 5 e acima)

O Spring 5 introduziu a classeResponseStatusException. Podemos criar uma instância dele fornecendoHttpStatuse, opcionalmente,reasonecause:

@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) {
    try {
        Foo resourceById = RestPreconditions.checkFound(service.findOne(id));

        eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response));
        return resourceById;
     }
    catch (MyResourceNotFoundException exc) {
         throw new ResponseStatusException(
           HttpStatus.NOT_FOUND, "Foo Not Found", exc);
    }
}

Quais são os benefícios de usarResponseStatusException?

  • Excelente para prototipagem: podemos implementar uma solução básica rapidamente

  • Um tipo, vários códigos de status: um tipo de exceção pode levar a várias respostas diferentes. This reduces tight coupling compared to the @ExceptionHandler

  • Não teremos que criar tantas classes de exceção personalizadas

  • More control over exception handling uma vez que as exceções podem ser criadas programaticamente

E as trocas?

  • Não há uma maneira unificada de tratamento de exceções: é mais difícil impor algumas convenções em todo o aplicativo, ao contrário de@ControllerAdvice, que fornece uma abordagem global

  • Duplicação de código: podemos nos encontrar replicando código em vários controladores

Devemos também observar que é possível combinar diferentes abordagens dentro de um aplicativo.

For example, we can implement a @ControllerAdvice globally, but also ResponseStatusExceptions locally. No entanto, precisamos ter cuidado: se a mesma exceção puder ser tratada de várias maneiras, podemos notar um comportamento surpreendente. Uma convenção possível é lidar com um tipo específico de exceção sempre de uma maneira.

Para mais detalhes e exemplos adicionais, veja nossotutorial on ResponseStatusException.

6. Lidar com o acesso negado no Spring Security

O acesso negado ocorre quando um usuário autenticado tenta acessar recursos que não possui autoridades suficientes para acessar.

6.1. MVC - Página de erro personalizada

Primeiro, vamos dar uma olhada no estilo MVC da solução e ver como personalizar uma página de erro para Acesso negado:

A configuração XML:


    
    ...
    

E a configuração Java:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
        ...
        .and()
        .exceptionHandling().accessDeniedPage("/my-error-page");
}

Quando os usuários tentam acessar um recurso sem ter autoridades suficientes, eles serão redirecionados para “/my-error-page“.

6.2. AccessDeniedHandler personalizado

A seguir, vamos ver como escrever nossoAccessDeniedHandler personalizado:

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle
      (HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex)
      throws IOException, ServletException {
        response.sendRedirect("/my-error-page");
    }
}

E agora vamos configurá-lo usandoXML Configuration:


    
    ...
    

Ou usando a Configuração Java:

@Autowired
private CustomAccessDeniedHandler accessDeniedHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
        ...
        .and()
        .exceptionHandling().accessDeniedHandler(accessDeniedHandler)
}

Observe como - em nossoCustomAccessDeniedHandler, podemos personalizar a resposta conforme desejarmos redirecionando ou exibindo uma mensagem de erro personalizada.

6.3. REST e segurança de nível de método

Finalmente, vamos ver como lidar com a segurança de nível de método@PreAuthorize,@PostAuthorize e@Secure Acesso negado.

Vamos, é claro, usar o mecanismo de tratamento de exceção global que discutimos anteriormente para lidar comAccessDeniedException também:

@ControllerAdvice
public class RestResponseEntityExceptionHandler
  extends ResponseEntityExceptionHandler {

    @ExceptionHandler({ AccessDeniedException.class })
    public ResponseEntity handleAccessDeniedException(
      Exception ex, WebRequest request) {
        return new ResponseEntity(
          "Access denied message here", new HttpHeaders(), HttpStatus.FORBIDDEN);
    }

    ...
}





7. Suporte de inicialização por mola

Spring Boot fornece uma implementaçãoErrorController para lidar com erros de uma maneira sensata.

Em poucas palavras, ele serve uma página de erro de fallback para navegadores (também conhecida como Whitelabel Error Page) e uma resposta JSON para solicitações RESTful, não HTML:

{
    "timestamp": "2019-01-17T16:12:45.977+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "Error processing the request!",
    "path": "/my-endpoint-with-exceptions"
}

Como de costume, o Spring Boot permite configurar esses recursos com propriedades:

  • server.error.whitelabel.enabled: pode ser usado para desativar a página de erro do Whitelabel e contar com o recipiente do servlet para fornecer uma mensagem de erro HTML

  • server.error.include-stacktrace: com umalways value, inclui o rastreamento de pilha na resposta padrão HTML e JSON

Além dessas propriedades,we can provide our own view-resolver mapping for /error, overriding the Whitelabel Page.

Também podemos personalizar os atributos que queremos mostrar na resposta incluindo umErrorAttributes bean no contexto. Podemos estender a classeDefaultErrorAttributes fornecida pelo Spring Boot para tornar as coisas mais fáceis:

@Component
public class MyCustomErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map getErrorAttributes(
      WebRequest webRequest, boolean includeStackTrace) {
        Map errorAttributes =
          super.getErrorAttributes(webRequest, includeStackTrace);
        errorAttributes.put("locale", webRequest.getLocale()
            .toString());
        errorAttributes.remove("error");

        //...

        return errorAttributes;
    }
}

Se quisermos ir além e definir (ou substituir) como o aplicativo tratará os erros de um tipo de conteúdo específico, podemos registrar umErrorController bean.

Novamente, podemos usar o padrãoBasicErrorController fornecido pelo Spring Boot para nos ajudar.

Por exemplo, imagine que queremos personalizar como nosso aplicativo lida com os erros acionados nos pontos de extremidade XML. Tudo o que precisamos fazer é definir um método público usando@RequestMappinge declarando que ele produz o tipo de mídiaapplication/xml:

@Component
public class MyErrorController extends BasicErrorController {

    public MyErrorController(ErrorAttributes errorAttributes) {
        super(errorAttributes, new ErrorProperties());
    }

    @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
    public ResponseEntity> xmlError(HttpServletRequest request) {

    // ...

    }
}

8. Conclusão

Este tutorial discutiu várias maneiras de implementar um mecanismo de tratamento de exceções para uma API REST no Spring, iniciando com o mecanismo mais antigo e continuando com o suporte do Spring 3.2 e nas versões 4.xe 5.x.

Como sempre, o código apresentado neste artigo está disponívelover on Github.

Para o código relacionado ao Spring Security, você pode verificar o módulospring-security-rest.