Como gerenciar exceções lançadas em filtros no Spring?

106

Eu quero usar uma maneira genérica de gerenciar códigos de erro 5xx, digamos especificamente o caso em que o banco de dados está baixo em todo o meu aplicativo Spring. Eu quero um belo erro json em vez de um rastreamento de pilha.

Para os controladores eu tenho uma @ControllerAdviceclasse para as diferentes exceções e isso também está pegando o caso que o db está parando no meio da requisição. Mas isto não é tudo. Acontece que eu também tenho uma CorsFilterextensão personalizada OncePerRequestFiltere quando ligo doFilterrecebo o CannotGetJdbcConnectionExceptione não será gerenciado pelo @ControllerAdvice. Eu li várias coisas online que só me deixaram mais confuso.

Então, eu tenho muitas perguntas:

  • Eu preciso implementar um filtro personalizado? Eu encontrei o ExceptionTranslationFiltermas isso só alça AuthenticationExceptionou AccessDeniedException.
  • Pensei em implementar o meu próprio HandlerExceptionResolver, mas fiquei em dúvida, não tenho nenhuma exceção customizada para gerenciar, deve haver uma forma mais óbvia do que essa. Também tentei adicionar um try / catch e chamar uma implementação de HandlerExceptionResolver(deve ser bom o suficiente, minha exceção não é nada especial), mas isso não está retornando nada na resposta, recebo um status 200 e um corpo vazio.

Existe alguma boa maneira de lidar com isso? obrigado

kopelitsa
fonte
Podemos sobrescrever BasicErrorController do Spring Boot. Eu escrevi sobre isso aqui: naturalprogrammer.com/blog/1685463/…
Sanjay

Respostas:

83

Então foi isso que eu fiz:

Eu li o básico sobre filtros aqui e descobri que preciso criar um filtro personalizado que será o primeiro na cadeia de filtros e terá uma captura de tentativa para capturar todas as exceções de tempo de execução que podem ocorrer lá. Em seguida, preciso criar o json manualmente e colocá-lo na resposta.

Então, aqui está meu filtro personalizado:

public class ExceptionHandlerFilter extends OncePerRequestFilter {

    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } catch (RuntimeException e) {

            // custom error response class used across my project
            ErrorResponse errorResponse = new ErrorResponse(e);

            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.getWriter().write(convertObjectToJson(errorResponse));
    }
}

    public String convertObjectToJson(Object object) throws JsonProcessingException {
        if (object == null) {
            return null;
        }
        ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(object);
    }
}

E então eu adicionei no web.xml antes do CorsFilter. E funciona!

<filter> 
    <filter-name>exceptionHandlerFilter</filter-name> 
    <filter-class>xx.xxxxxx.xxxxx.api.controllers.filters.ExceptionHandlerFilter</filter-class> 
</filter> 


<filter-mapping> 
    <filter-name>exceptionHandlerFilter</filter-name> 
    <url-pattern>/*</url-pattern> 
</filter-mapping> 

<filter> 
    <filter-name>CorsFilter</filter-name> 
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> 
</filter> 

<filter-mapping>
    <filter-name>CorsFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
kopelitsa
fonte
Você poderia postar sua classe ErrorResponse?
Shiva kumar
A classe @Shivakumar ErrorResponse é provavelmente um DTO simples com propriedades de código / mensagem simples.
ratijas 01 de
20

Eu queria fornecer uma solução baseada na resposta de @kopelitsa . As principais diferenças são:

  1. Reutilizando o tratamento de exceção do controlador usando o HandlerExceptionResolver.
  2. Usando configuração Java sobre configuração XML

Primeiro, você precisa ter certeza de que tem uma classe que lida com exceções que ocorrem em um RestController / Controller regular (uma classe anotada com @RestControllerAdviceou @ControllerAdvicee método (s) anotados com @ExceptionHandler). Isso lida com suas exceções que ocorrem em um controlador. Aqui está um exemplo usando o RestControllerAdvice:

@RestControllerAdvice
public class ExceptionTranslator {

    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorDTO processRuntimeException(RuntimeException e) {
        return createErrorDTO(HttpStatus.INTERNAL_SERVER_ERROR, "An internal server error occurred.", e);
    }

    private ErrorDTO createErrorDTO(HttpStatus status, String message, Exception e) {
        (...)
    }
}

Para reutilizar esse comportamento na cadeia de filtros Spring Security, você precisa definir um Filtro e conectá-lo à sua configuração de segurança. O filtro precisa redirecionar a exceção para o tratamento de exceção definido acima. Aqui está um exemplo:

@Component
public class FilterChainExceptionHandler extends OncePerRequestFilter {

    private final Logger log = LoggerFactory.getLogger(getClass());

    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        try {
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            log.error("Spring Security Filter Chain Exception:", e);
            resolver.resolveException(request, response, null, e);
        }
    }
}

O filtro criado precisa ser adicionado a SecurityConfiguration. Você precisa conectá-lo à cadeia muito cedo, porque todas as exceções do filtro anterior não serão capturadas. No meu caso, era razoável adicioná-lo antes de LogoutFilter. Veja a cadeia de filtros padrão e sua ordem nos documentos oficiais . Aqui está um exemplo:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private FilterChainExceptionHandler filterChainExceptionHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(filterChainExceptionHandler, LogoutFilter.class)
            (...)
    }

}
ssc-hrep3
fonte
17

Eu me deparei com esse problema e executei as etapas abaixo para reutilizar o meu ExceptionControllerque está anotado @ControllerAdvisepara Exceptionsinserido em um filtro registrado.

Obviamente, existem muitas maneiras de lidar com a exceção, mas, no meu caso, eu queria que a exceção fosse tratada pelo meu ExceptionControllerporque sou teimoso e também porque não quero copiar / colar o mesmo código (ou seja, tenho algum processamento / registro código emExceptionController ). Eu gostaria de retornar a bela JSONresposta, assim como o resto das exceções lançadas não de um Filtro.

{
  "status": 400,
  "message": "some exception thrown when executing the request"
}

Enfim, consegui fazer uso do meu ExceptionHandler e tive que fazer um pouco mais conforme mostrado abaixo nas etapas:

Passos


  1. Você tem um filtro personalizado que pode ou não lançar uma exceção
  2. Você tem um controlador Spring que lida com exceções usando, por exemplo, @ControllerAdviseMyExceptionController

Código de amostra

//sample Filter, to be added in web.xml
public MyFilterThatThrowException implements Filter {
   //Spring Controller annotated with @ControllerAdvise which has handlers
   //for exceptions
   private MyExceptionController myExceptionController; 

   @Override
   public void destroy() {
        // TODO Auto-generated method stub
   }

   @Override
   public void init(FilterConfig arg0) throws ServletException {
       //Manually get an instance of MyExceptionController
       ApplicationContext ctx = WebApplicationContextUtils
                  .getRequiredWebApplicationContext(arg0.getServletContext());

       //MyExceptionHanlder is now accessible because I loaded it manually
       this.myExceptionController = ctx.getBean(MyExceptionController.class); 
   }

   @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        try {
           //code that throws exception
        } catch(Exception ex) {
          //MyObject is whatever the output of the below method
          MyObject errorDTO = myExceptionController.handleMyException(req, ex); 

          //set the response object
          res.setStatus(errorDTO .getStatus());
          res.setContentType("application/json");

          //pass down the actual obj that exception handler normally send
          ObjectMapper mapper = new ObjectMapper();
          PrintWriter out = res.getWriter(); 
          out.print(mapper.writeValueAsString(errorDTO ));
          out.flush();

          return; 
        }

        //proceed normally otherwise
        chain.doFilter(request, response); 
     }
}

E agora o exemplo de Spring Controller que lida Exceptionem casos normais (ou seja, exceções que normalmente não são lançadas no nível de Filtro, aquele que queremos usar para exceções lançadas em um Filtro)

//sample SpringController 
@ControllerAdvice
public class ExceptionController extends ResponseEntityExceptionHandler {

    //sample handler
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(SQLException.class)
    public @ResponseBody MyObject handleSQLException(HttpServletRequest request,
            Exception ex){
        ErrorDTO response = new ErrorDTO (400, "some exception thrown when "
                + "executing the request."); 
        return response;
    }
    //other handlers
}

Compartilhando a solução com aqueles que desejam usar ExceptionControllerpara Exceptionsjogar em um filtro.

Raf
fonte
10
Bem, você está convidado a compartilhar sua própria solução que parece ser a maneira de fazê-lo :)
Raf
1
Se você quiser evitar ter um controlador conectado em seu filtro (que é o que @ Bato-BairTsyrenov está se referindo, eu suponho), você pode facilmente extrair a lógica onde você cria o ErrorDTO em sua própria @Componentclasse e usa isso no filtro e no o controlador.
Rüdiger Schulz
1
Não concordo totalmente com você, pois isso não é muito limpo para injetar um controlador específico no seu filtro.
psv
Conforme mencionado no, answeresta é uma das maneiras! Não afirmei que seja a melhor maneira. Obrigado por compartilhar sua preocupação @psv. Tenho certeza de que a comunidade apreciaria a solução que você tem em mente :)
Raf
12

Então, aqui está o que eu fiz com base em um amálgama das respostas acima ... Já tínhamos uma GlobalExceptionHandleranotação com @ControllerAdvicee eu também queria encontrar uma maneira de reutilizar esse código para lidar com exceções que vêm de filtros.

A solução mais simples que encontrei foi deixar o manipulador de exceção sozinho e implementar um controlador de erro da seguinte maneira:

@Controller
public class ErrorControllerImpl implements ErrorController {
  @RequestMapping("/error")
  public void handleError(HttpServletRequest request) throws Throwable {
    if (request.getAttribute("javax.servlet.error.exception") != null) {
      throw (Throwable) request.getAttribute("javax.servlet.error.exception");
    }
  }
}

Portanto, quaisquer erros causados ​​por exceções passam primeiro pelo ErrorControllere são redirecionados para o manipulador de exceções, lançando-os novamente de dentro de um @Controllercontexto, enquanto quaisquer outros erros (não causados ​​diretamente por uma exceção) passam pelo ErrorControllersem modificação.

Alguma razão para que isso seja realmente uma má ideia?

AndyB
fonte
1
Obrigado agora testando essa solução mas no meu caso funcionou perfeito.
Maciej
uma adição limpa e simples para o Spring Boot 2.0+ que você deve adicionar @Override public String getErrorPath() { return null; }
Fma
você pode usar javax.servlet.RequestDispatcher.ERROR_EXCEPTION em vez de "javax.servlet.error.exception"
Marx
9

Se você quiser uma forma genérica, pode definir uma página de erro em web.xml:

<error-page>
  <exception-type>java.lang.Throwable</exception-type>
  <location>/500</location>
</error-page>

E adicione mapeamento no Spring MVC:

@Controller
public class ErrorController {

    @RequestMapping(value="/500")
    public @ResponseBody String handleException(HttpServletRequest req) {
        // you can get the exception thrown
        Throwable t = (Throwable)req.getAttribute("javax.servlet.error.exception");

        // customize response to what you want
        return "Internal server error.";
    }
}
holmis83
fonte
Mas em uma API de descanso, redirecionar com a localização não é uma boa solução.
jmattheis
@jmattheis O texto acima não é um redirecionamento.
holmis83 de
É verdade que vi a localização e pensei que tinha algo a ver com a localização http. Então é disso que eu preciso (:
jmattheis
Você poderia adicionar a configuração Java equivalente ao web.xml, se houver?
k-den
1
@ k-den Não há configuração Java equivalente na especificação atual, eu acho, mas você pode misturar web.xml e configuração Java.
holmis83
5

Esta é a minha solução substituindo Spring Boot / manipulador de erros padrão

package com.mypackage;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

/**
 * This controller is vital in order to handle exceptions thrown in Filters.
 */
@RestController
@RequestMapping("/error")
public class ErrorController implements org.springframework.boot.autoconfigure.web.ErrorController {

    private final static Logger LOGGER = LoggerFactory.getLogger(ErrorController.class);

    private final ErrorAttributes errorAttributes;

    @Autowired
    public ErrorController(ErrorAttributes errorAttributes) {
        Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
        this.errorAttributes = errorAttributes;
    }

    @Override
    public String getErrorPath() {
        return "/error";
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest aRequest, HttpServletResponse response) {
        RequestAttributes requestAttributes = new ServletRequestAttributes(aRequest);
        Map<String, Object> result =     this.errorAttributes.getErrorAttributes(requestAttributes, false);

        Throwable error = this.errorAttributes.getError(requestAttributes);

        ResponseStatus annotation =     AnnotationUtils.getAnnotation(error.getClass(), ResponseStatus.class);
        HttpStatus statusCode = annotation != null ? annotation.value() : HttpStatus.INTERNAL_SERVER_ERROR;

        result.put("status", statusCode.value());
        result.put("error", statusCode.getReasonPhrase());

        LOGGER.error(result.toString());
        return new ResponseEntity<>(result, statusCode) ;
    }

}
walv
fonte
afeta alguma configuração automática?
Samet Baskıcı
Observe que HandlerExceptionResolver não lida necessariamente com a exceção. Portanto, ele pode cair como HTTP 200. Usar response.setStatus (..) antes de chamá-lo parece mais seguro.
ThomasRS
5

Apenas para complementar as outras respostas finas fornecidas, já que recentemente eu queria um único componente de tratamento de erro / exceção em um aplicativo SpringBoot simples contendo filtros que podem lançar exceções, com outras exceções potencialmente lançadas de métodos de controlador.

Felizmente, parece que não há nada que o impeça de combinar seu conselho de controlador com uma substituição do manipulador de erro padrão do Spring para fornecer cargas de resposta consistentes, permitir que você compartilhe lógica, inspecione exceções de filtros, intercepte exceções lançadas de serviço específico, etc.

Por exemplo


@ControllerAdvice
@RestController
public class GlobalErrorHandler implements ErrorController {

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(ValidationException.class)
  public Error handleValidationException(
      final ValidationException validationException) {
    return new Error("400", "Incorrect params"); // whatever
  }

  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  @ExceptionHandler(Exception.class)
  public Error handleUnknownException(final Exception exception) {
    return new Error("500", "Unexpected error processing request");
  }

  @RequestMapping("/error")
  public ResponseEntity handleError(final HttpServletRequest request,
      final HttpServletResponse response) {

    Object exception = request.getAttribute("javax.servlet.error.exception");

    // TODO: Logic to inspect exception thrown from Filters...
    return ResponseEntity.badRequest().body(new Error(/* whatever */));
  }

  @Override
  public String getErrorPath() {
    return "/error";
  }

}
Tom Bunting
fonte
3

Quando você quiser testar um estado de aplicação e em caso de algum problema retornar erro HTTP, sugiro um filtro. O filtro abaixo lida com todas as solicitações HTTP. A solução mais curta em Spring Boot com um filtro javax.

Na implementação pode haver várias condições. No meu caso o applicationManager testando se o aplicativo está pronto.

import ...ApplicationManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class SystemIsReadyFilter implements Filter {

    @Autowired
    private ApplicationManager applicationManager;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (!applicationManager.isApplicationReady()) {
            ((HttpServletResponse) response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "The service is booting.");
        } else {
            chain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {}
}
Cyva
fonte
2

Depois de ler os diferentes métodos sugeridos nas respostas acima, decidi lidar com as exceções de autenticação usando um filtro personalizado. Consegui lidar com o status e os códigos de resposta usando uma classe de resposta de erro usando o método a seguir.

Criei um filtro personalizado e modifiquei minha configuração de segurança usando o método addFilterAfter e adicionei após a classe CorsFilter.

@Component
public class AuthFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    //Cast the servlet request and response to HttpServletRequest and HttpServletResponse
    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
    HttpServletRequest httpServletRequest = (HttpServletRequest) request;

    // Grab the exception from the request attribute
    Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
    //Set response content type to application/json
    httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);

    //check if exception is not null and determine the instance of the exception to further manipulate the status codes and messages of your exception
    if(exception!=null && exception instanceof AuthorizationParameterNotFoundException){
        ErrorResponse errorResponse = new ErrorResponse(exception.getMessage(),"Authetication Failed!");
        httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        PrintWriter writer = httpServletResponse.getWriter();
        writer.write(convertObjectToJson(errorResponse));
        writer.flush();
        return;
    }
    // If exception instance cannot be determined, then throw a nice exception and desired response code.
    else if(exception!=null){
            ErrorResponse errorResponse = new ErrorResponse(exception.getMessage(),"Authetication Failed!");
            PrintWriter writer = httpServletResponse.getWriter();
            writer.write(convertObjectToJson(errorResponse));
            writer.flush();
            return;
        }
        else {
        // proceed with the initial request if no exception is thrown.
            chain.doFilter(httpServletRequest,httpServletResponse);
        }
    }

public String convertObjectToJson(Object object) throws JsonProcessingException {
    if (object == null) {
        return null;
    }
    ObjectMapper mapper = new ObjectMapper();
    return mapper.writeValueAsString(object);
}
}

Classe SecurityConfig

    @Configuration
    public class JwtSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    AuthFilter authenticationFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterAfter(authenticationFilter, CorsFilter.class).csrf().disable()
                .cors(); //........
        return http;
     }
   }

Classe ErrorResponse

public class ErrorResponse  {
private final String message;
private final String description;

public ErrorResponse(String description, String message) {
    this.message = message;
    this.description = description;
}

public String getMessage() {
    return message;
}

public String getDescription() {
    return description;
}}
Adewagold
fonte
0

Você pode usar o seguinte método dentro do bloco catch:

response.sendError(HttpStatus.UNAUTHORIZED.value(), "Invalid token")

Observe que você pode usar qualquer código HttpStatus e uma mensagem personalizada.

Santiago Carrillo
fonte
-1

É estranho porque @ControllerAdvice deve funcionar, você está capturando a exceção correta?

@ControllerAdvice
public class GlobalDefaultExceptionHandler {

    @ResponseBody
    @ExceptionHandler(value = DataAccessException.class)
    public String defaultErrorHandler(HttpServletResponse response, DataAccessException e) throws Exception {
       response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
       //Json return
    }
}

Também tente pegar essa exceção no CorsFilter e enviar erro 500, algo como isso

@ExceptionHandler(DataAccessException.class)
@ResponseBody
public String handleDataException(DataAccessException ex, HttpServletResponse response) {
    response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
    //Json return
}
user2669657
fonte
Lidar com a exceção no CorsFilter funciona, mas não é muito limpo. Na verdade, o que eu realmente preciso é lidar com a exceção para todos os filtros
kopelitsa
35
O lançamento de exceção de Filternão pode ser capturado @ControllerAdviceporque não pode alcançar DispatcherServlet.
Thanh Nguyen Van
-1

Você não precisa criar um filtro personalizado para isso. Resolvemos isso criando exceções personalizadas que estendem ServletException (que é lançada do método doFilter, mostrado na declaração). Em seguida, eles são detectados e tratados por nosso gerenciador de erros global.

editar: gramática

beta-brad
fonte
Você se importaria de compartilhar um snippet de código de seu manipulador de erros global?
Neeraj Vernekar
não funciona para mim. Eu fiz uma exceção personalizada, que estende ServletException, adicionei suporte para essa exceção em ExceptionHandler, mas ela não foi interceptada lá.
Marx