Come gestire le eccezioni lanciate nei filtri in primavera?


106

Voglio usare un modo generico per gestire i codici di errore 5xx, diciamo in particolare il caso in cui il db è inattivo in tutta la mia applicazione primaverile. Voglio un json di errore piuttosto che una traccia dello stack.

Per i controller ho una @ControllerAdviceclasse per le diverse eccezioni e questo è anche il caso in cui il db si sta fermando nel mezzo della richiesta. Ma questo non è tutto. Mi capita anche di avere CorsFilterun'estensione personalizzata OncePerRequestFiltere lì quando chiamo doFilterricevo il CannotGetJdbcConnectionExceptione non sarà gestito dal @ControllerAdvice. Ho letto molte cose online che mi hanno solo reso più confuso.

Quindi ho molte domande:

  • Devo implementare un filtro personalizzato? Ho trovato il ExceptionTranslationFilterma questo gestisce solo AuthenticationExceptiono AccessDeniedException.
  • Ho pensato di implementare il mio HandlerExceptionResolver, ma questo mi ha fatto dubitare, non ho alcuna eccezione personalizzata da gestire, deve esserci un modo più ovvio di questo. Ho anche provato ad aggiungere un try / catch e chiamare un'implementazione di HandlerExceptionResolver(dovrebbe essere abbastanza buono, la mia eccezione non è niente di speciale) ma questo non restituisce nulla nella risposta, ottengo uno stato 200 e un corpo vuoto.

C'è un buon modo per affrontare questo problema? Grazie


Possiamo sovrascrivere BasicErrorController di Spring Boot. Ne ho scritto sul blog qui: naturalprogrammer.com/blog/1685463/…
Sanjay

Risposte:


83

Quindi questo è quello che ho fatto:

Ho letto le nozioni di base sui filtri qui e ho capito che ho bisogno di creare un filtro personalizzato che sarà il primo nella catena di filtri e avrà un tentativo di catturare tutte le eccezioni di runtime che potrebbero verificarsi lì. Quindi devo creare manualmente il json e inserirlo nella risposta.

Quindi ecco il mio filtro personalizzato:

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 poi l'ho aggiunto nel web.xml prima del file CorsFilter. E funziona!

<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>

Potresti pubblicare la tua classe ErrorResponse?
Shiva kumar,

La classe @Shivakumar ErrorResponse è probabilmente un semplice DTO con semplici proprietà di codice / messaggio.
ratijas

20

Volevo fornire una soluzione basata sulla risposta di @kopelitsa . Le principali differenze sono:

  1. Riutilizzo della gestione delle eccezioni del controller utilizzando l'estensione HandlerExceptionResolver.
  2. Utilizzando Java config su XML config

Innanzitutto, devi assicurarti di avere una classe che gestisce le eccezioni che si verificano in un normale RestController / Controller (una classe annotata con @RestControllerAdviceo @ControllerAdvicee metodi annotati con @ExceptionHandler). Questo gestisce le eccezioni che si verificano in un controller. Ecco un esempio utilizzando 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) {
        (...)
    }
}

Per riutilizzare questo comportamento nella catena di filtri Spring Security, è necessario definire un filtro e collegarlo alla configurazione della sicurezza. Il filtro deve reindirizzare l'eccezione alla gestione delle eccezioni definita sopra. Ecco un esempio:

@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);
        }
    }
}

Il filtro creato deve quindi essere aggiunto a SecurityConfiguration. Devi agganciarlo alla catena molto presto, perché tutte le eccezioni del filtro precedente non verranno catturate. Nel mio caso, era ragionevole aggiungerlo prima del LogoutFilter. Vedi la catena di filtri predefinita e il suo ordine nei documenti ufficiali . Ecco un esempio:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private FilterChainExceptionHandler filterChainExceptionHandler;

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

}

17

Mi sono imbattuto in questo problema da solo e ho eseguito i passaggi seguenti per riutilizzare il mio ExceptionControllerannotato con @ControllerAdviseper Exceptionsgettato in un filtro registrato.

Ci sono ovviamente molti modi per gestire l'eccezione ma, nel mio caso, volevo che l'eccezione fosse gestita dal mio ExceptionControllerperché sono testardo e anche perché non voglio copiare / incollare lo stesso codice (cioè ho qualche elaborazione / registrazione codice in ExceptionController). Vorrei restituire la bella JSONrisposta proprio come il resto delle eccezioni lanciate non da un filtro.

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

Ad ogni modo, sono riuscito a utilizzare il mio ExceptionHandlere ho dovuto fare un po 'di extra come mostrato di seguito nei passaggi:

Passi


  1. Hai un filtro personalizzato che può o non può generare un'eccezione
  2. Hai un controller Spring che gestisce le eccezioni utilizzando ad @ControllerAdviseesempio MyExceptionController

Codice di esempio

//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 ora l'esempio di Spring Controller che gestisce Exceptionin casi normali (cioè eccezioni che di solito non vengono lanciate a livello di Filtro, quello che vogliamo usare per le eccezioni lanciate in un 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
}

Condividendo la soluzione con coloro che desiderano utilizzare ExceptionControllerper Exceptionsgettato in un filtro.


10
Bene, sei il benvenuto a condividere la tua soluzione che sembra il modo per farlo :)
Raf

1
Se vuoi evitare di avere un controller collegato al tuo filtro (che è ciò a cui si riferisce @ Bato-BairTsyrenov presumo), puoi facilmente estrarre la logica in cui crei ErrorDTO nella sua @Componentclasse e usarla nel Filtro e in il Titolare.
Rüdiger Schulz

1
Non sono totalmente d'accordo con te, perché non è molto pulito iniettare un controller specifico nel tuo filtro.
PSV

Come accennato in answerquesto è uno dei modi! Non ho affermato che sia il modo migliore. Grazie per aver condiviso la tua preoccupazione @psv Sono sicuro che la community apprezzerebbe la soluzione che hai in mente :)
Raf

12

Quindi, ecco cosa ho fatto sulla base di una fusione delle risposte precedenti ... Avevamo già GlobalExceptionHandlerannotato con @ControllerAdvicee volevo anche trovare un modo per riutilizzare quel codice per gestire le eccezioni che provengono dai filtri.

La soluzione più semplice che sono riuscito a trovare è stata quella di lasciare da solo il gestore delle eccezioni e implementare un controller degli errori come segue:

@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");
    }
  }
}

Quindi, qualsiasi errore causato dalle eccezioni passa prima attraverso ErrorControllere viene reindirizzato al gestore delle eccezioni rilanciandolo dall'interno di un @Controllercontesto, mentre qualsiasi altro errore (non causato direttamente da un'eccezione) passa attraverso il ErrorControllersenza modifiche.

Qualche motivo per cui questa è effettivamente una cattiva idea?


1
Grazie ora testando questa soluzione ma nel mio caso funziona perfettamente.
Maciej

pulita e semplice un'aggiunta per lo stivale primaverile 2.0+ dovresti aggiungere @Override public String getErrorPath() { return null; }
Fma

puoi usare javax.servlet.RequestDispatcher.ERROR_EXCEPTION invece di "javax.servlet.error.exception"
Marx

9

Se vuoi un modo generico, puoi definire una pagina di errore in web.xml:

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

E aggiungi la mappatura in 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.";
    }
}

Ma in un altro api reindirizzare con la posizione non è una buona soluzione.
jmattheis

@jmattheis Quanto sopra non è un reindirizzamento.
holmis83

È vero, ho visto la posizione e ho pensato che avesse qualcosa da fare con la posizione http. Allora questo è ciò di cui ho bisogno (:
jmattheis

Potresti aggiungere la configurazione Java equivalente a web.xml, se esiste?
k-den

1
@ k-den Non esiste un equivalente di configurazione Java nelle specifiche correnti, ma puoi combinare web.xml e Java config.
holmis83

5

Questa è la mia soluzione sovrascrivendo il gestore degli errori / Spring Boot predefinito

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) ;
    }

}

ha effetto sulla configurazione automatica?
Samet Baskıcı

Si noti che HandlerExceptionResolver non gestisce necessariamente l'eccezione. Quindi potrebbe cadere come HTTP 200. Usare response.setStatus (..) prima di chiamarlo sembra più sicuro.
ThomasRS

5

Solo per completare le altre belle risposte fornite, poiché troppo recentemente ho voluto un singolo componente di gestione degli errori / eccezioni in una semplice app SpringBoot contenente filtri che possono generare eccezioni, con altre eccezioni potenzialmente generate dai metodi del controller.

Fortunatamente, sembra che non ci sia nulla che ti impedisca di combinare i consigli del controller con un override del gestore degli errori predefinito di Spring per fornire payload di risposta coerenti, consentirti di condividere la logica, ispezionare le eccezioni dai filtri, intercettare eccezioni generate dal servizio specifico, ecc.

Per esempio


@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";
  }

}

3

Quando si desidera testare uno stato dell'applicazione e in caso di problemi restituire un errore HTTP, suggerirei un filtro. Il filtro seguente gestisce tutte le richieste HTTP. La soluzione più breve in Spring Boot con un filtro javax.

Nell'attuazione possono essere varie condizioni. Nel mio caso ApplicationManager verifica se l'applicazione è pronta.

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() {}
}

2

Dopo aver letto i diversi metodi suggeriti nelle risposte precedenti, ho deciso di gestire le eccezioni di autenticazione utilizzando un filtro personalizzato. Sono stato in grado di gestire lo stato ei codici della risposta utilizzando una classe di risposta agli errori utilizzando il seguente metodo.

Ho creato un filtro personalizzato e modificato la mia configurazione di sicurezza utilizzando il metodo addFilterAfter e aggiunto dopo la 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;
     }
   }

ErrorResponse classe

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;
}}

0

Puoi utilizzare il seguente metodo all'interno del blocco catch:

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

Si noti che è possibile utilizzare qualsiasi codice HttpStatus e un messaggio personalizzato.


-1

È strano perché @ControllerAdvice dovrebbe funzionare, stai rilevando l'eccezione corretta?

@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
    }
}

Prova anche a catturare questa eccezione in CorsFilter e invia l'errore 500, qualcosa del genere

@ExceptionHandler(DataAccessException.class)
@ResponseBody
public String handleDataException(DataAccessException ex, HttpServletResponse response) {
    response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
    //Json return
}

La gestione dell'eccezione in CorsFilter funziona, ma non è molto pulita. In effetti ciò di cui ho davvero bisogno è gestire l'eccezione per tutti i filtri
kopelitsa

35
Il lancio dell'eccezione da Filterpotrebbe non essere recuperato @ControllerAdviceperché in potrebbe non raggiungere DispatcherServlet.
Thanh Nguyen Van

-1

Non è necessario creare un filtro personalizzato per questo. Abbiamo risolto questo problema creando eccezioni personalizzate che estendono ServletException (che viene generata dal metodo doFilter, mostrato nella dichiarazione). Questi vengono quindi rilevati e gestiti dal nostro gestore degli errori globale.

modifica: grammatica


Ti dispiacerebbe condividere uno snippet di codice del tuo gestore di errori globale?
Neeraj Vernekar

non funziona per me. Ho creato un'eccezione personalizzata, che estende ServletException, aggiunto il supporto per questa eccezione in ExceptionHandler, ma non è stata intercettata lì.
Marx
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.