Je veux utiliser un moyen générique pour gérer les codes d'erreur 5xx, disons spécifiquement le cas où la base de données est en panne sur l'ensemble de mon application Spring. Je veux une jolie erreur json au lieu d'une trace de pile.
Pour les contrôleurs, j'ai une classe @ControllerAdvice
pour les différentes exceptions, ce qui attrape également le cas où la base de données est en train de s'arrêter au milieu de la demande. Mais ce n'est pas tout. Il se trouve que j’ai aussi une variable CorsFilter
étendue OncePerRequestFilter
et lorsque j’appelle doFilter
j’obtiens la CannotGetJdbcConnectionException
et elle ne sera pas gérée par le @ControllerAdvice
. J'ai lu plusieurs choses en ligne qui ne faisaient que me rendre plus confus.
J'ai donc beaucoup de questions:
ExceptionTranslationFilter
mais cela ne traite que AuthenticationException
ou AccessDeniedException
. HandlerExceptionResolver
, mais cela m'a fait douter, je n'ai aucune exception personnalisée à gérer, il doit y avoir un moyen plus évident que celui-là. J'ai aussi essayé d'ajouter un try/catch and call une implémentation de la HandlerExceptionResolver
(ça devrait être suffisant, mon exception n'a rien de spécial) mais cela ne renvoie rien dans la réponse, je reçois un statut 200 et un corps vide. Y a-t-il un bon moyen de gérer cela? Merci
Alors voici ce que j'ai fait:
J'ai lu les notions de base sur les filtres ici et je me suis rendu compte que je devais créer un filtre personnalisé qui figurerait en premier dans la chaîne de filtres. Ensuite, je dois créer le JSON manuellement et le mettre dans la réponse.
Alors voici mon filtre personnalisé:
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);
}
}
Et puis je l'ai ajouté dans le fichier web.xml avant la CorsFilter
. Et il fonctionne!
<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>
Je rencontre ce problème moi-même et j'ai suivi les étapes ci-dessous pour réutiliser ma ExceptionController
annotée avec @ControllerAdvise
pour Exceptions
envoyée dans un filtre enregistré.
Il y a évidemment beaucoup de façons de gérer l'exception mais, dans mon cas, je voulais que l'exception soit gérée par ma ExceptionController
parce que je suis têtue et aussi parce que je ne veux pas copier/coller le même code code de connexion dans ExceptionController
). Je voudrais renvoyer la belle réponse JSON
de la même manière que le reste des exceptions rejetées pas d'un filtre.
{
"status": 400,
"message": "some exception thrown when executing the request"
}
Quoi qu’il en soit, j’ai réussi à utiliser ma ExceptionHandler
et j’ai dû faire un petit extra comme indiqué ci-dessous par étapes:
Pas
@ControllerAdvise
, par exemple, MyExceptionController.Exemple de code
//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);
}
}
Et maintenant, l'exemple de contrôleur de ressort qui gère Exception
dans des cas normaux (c'est-à-dire les exceptions qui ne sont généralement pas générées dans le niveau de filtre, celle que nous souhaitons utiliser pour les exceptions générées dans un filtre)
//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
}
Partage de la solution avec ceux qui souhaitent utiliser ExceptionController
pour Exceptions
jeté dans un filtre.
Si vous voulez un moyen générique, vous pouvez définir une page d'erreur dans web.xml:
<error-page>
<exception-type>Java.lang.Throwable</exception-type>
<location>/500</location>
</error-page>
Et ajoutez un mappage dans 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.";
}
}
Ceci est ma solution en surchargeant le gestionnaire d'erreurs Spring Boot par défaut
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) ;
}
}
Donc, voici ce que j'ai fait en fusionnant les réponses ci-dessus ... Nous avions déjà une variable GlobalExceptionHandler
annotée avec @ControllerAdvice
et je voulais aussi trouver un moyen de réutiliser ce code pour gérer les exceptions issues des filtres.
La solution la plus simple que j'ai trouvée consistait à laisser le gestionnaire d'exceptions seul et à implémenter un contrôleur d'erreur comme suit:
@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");
}
}
}
Ainsi, toutes les erreurs causées par des exceptions traversent d'abord la variable ErrorController
et sont redirigées vers le gestionnaire d'exceptions en les renvoyant à l'intérieur d'un contexte @Controller
, tandis que toutes les autres erreurs (non directement causées par une exception) traversent la variable ErrorController
sans modification.
Des raisons pour lesquelles c'est réellement une mauvaise idée?
Lorsque vous souhaitez tester un état d'application et en cas de problème, renvoyer une erreur HTTP, je suggère un filtre. Le filtre ci-dessous gère toutes les requêtes HTTP. La solution la plus courte dans Spring Boot avec un filtre javax.
Dans la mise en œuvre peut être diverses conditions. Dans mon cas, l'applicationManager teste si l'application est prête.
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() {}
}
Je voulais fournir une solution basée sur la réponse de @kopelitsa . Les principales différences étant:
HandlerExceptionResolver
.Tout d’abord, vous devez vous assurer que vous avez une classe qui gère les exceptions survenant dans un RestController/Controller normal (une classe annotée avec @RestControllerAdvice
ou @ControllerAdvice
et une ou plusieurs méthodes annotées avec @ExceptionHandler
). Ceci gère vos exceptions survenant dans un contrôleur. Voici un exemple utilisant 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) {
(...)
}
}
Pour réutiliser ce comportement dans la chaîne de filtres Spring Security, vous devez définir un filtre et le connecter à votre configuration de sécurité. Le filtre doit rediriger l'exception vers la gestion des exceptions définie ci-dessus. Voici un exemple:
@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 (RuntimeException e) {
log.error("Spring Security Filter Chain RuntimeException:", e);
resolver.resolveException(request, response, null, e);
}
}
}
Le filtre créé doit ensuite être ajouté à SecurityConfiguration. Vous devez le connecter très tôt à la chaîne, car toutes les exceptions du filtre précédent ne seront pas interceptées. Dans mon cas, il était raisonnable de l'ajouter avant le LogoutFilter
. Voir la chaîne de filtres par défaut et son ordre dans la documentation officielle . Voici un exemple:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private FilterChainExceptionHandler filterChainExceptionHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(filterChainExceptionHandler, LogoutFilter.class)
(...)
}
}
C'est étrange parce que @ControllerAdvice devrait fonctionner, attrapez-vous la bonne exception?
@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
}
}
Essayez également d’attraper cette exception dans CorsFilter et d’envoyer une erreur 500, quelque chose comme ceci.
@ExceptionHandler(DataAccessException.class)
@ResponseBody
public String handleDataException(DataAccessException ex, HttpServletResponse response) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
//Json return
}
Après avoir lu différentes méthodes suggérées dans les réponses ci-dessus, j'ai décidé de gérer les exceptions d'authentification à l'aide d'un filtre personnalisé. J'ai été en mesure de gérer le statut de la réponse et les codes à l'aide d'une classe de réponse d'erreur à l'aide de la méthode suivante.
J'ai créé un filtre personnalisé et modifié ma configuration de sécurité à l'aide de la méthode addFilterAfter, puis ajouté après 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;
}
}
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;
}}
Juste pour compléter les autres réponses fournies, car je souhaitais trop récemment un composant de traitement single error/exception dans une simple application SpringBoot contenant des filtres pouvant générer des exceptions, avec d’autres exceptions potentiellement générées par les méthodes du contrôleur.
Heureusement, rien ne vous empêche apparemment de combiner les conseils du contrôleur avec une substitution du gestionnaire d’erreurs par défaut de Spring afin de fournir des charges de réponse cohérentes, de partager la logique, de contrôler les exceptions à partir de filtres, de dérouter des exceptions spécifiques générées par le service, etc.
Par exemple.
@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";
}
}