web-dev-qa-db-fra.com

Comment obtenir le @RequestBody dans un @ExceptionHandler (Spring REST)

J'utilise Spring Boot 1.4.1 qui inclut spring-web-4.3.3. J'ai une classe annotée avec @ControllerAdvice Et des méthodes annotées avec @ExceptionHandler Pour gérer les exceptions levées par le code de service. Lors de la gestion de ces exceptions, je voudrais enregistrer le @RequestBody Qui faisait partie de la demande pour PUT et POST opérations afin que je puisse voir le corps de la demande qui a causé le problème qui dans mon cas est crucial pour le diagnostic.

Par Spring Docs la signature de méthode pour les méthodes @ExceptionHandler Peut inclure diverses choses, y compris le HttpServletRequest. Le corps de la demande peut normalement être obtenu à partir d'ici via getInputStream() ou getReader(), mais si mes méthodes de contrôleur analysent le corps de la demande comme "@RequestBody Foo fooBody" Comme toutes les miennes, le HttpServletRequest's Le flux d'entrée ou le lecteur est déjà fermé au moment de l'appel de ma méthode de gestionnaire d'exceptions. Essentiellement, le corps de la demande a déjà été lu par Spring, semblable au problème décrit ici . C'est un problème courant de travailler avec des servlets que le corps de la requête ne peut être lu qu'une seule fois.

Malheureusement, @RequestBody N'est pas l'une des options disponibles pour la méthode du gestionnaire d'exceptions, si c'était le cas, je pourrais l'utiliser.

Je peux ajouter un InputStream à la méthode du gestionnaire d'exceptions, mais cela finit par être la même chose que InputStream de HttpServletRequest et a donc le même problème.

J'ai également essayé d'obtenir la demande actuelle avec ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(), Ce qui est une autre astuce pour obtenir la demande actuelle, mais cela finit par être le même HttpServletRequest que Spring passe dans la méthode du gestionnaire d'exceptions et a donc le même problème.

J'ai lu quelques solutions comme this et this qui impliquent l'insertion d'un wrapper de requête personnalisé dans la chaîne de filtrage qui lira le contenu de la requête et les mettra en cache afin qu'ils puissent être lu plusieurs fois. Je n'aime pas cette solution parce que je ne veux pas interrompre toute la chaîne de filtrage/demande/réponse (et potentiellement introduire des problèmes de performances ou de stabilité) juste pour implémenter la journalisation, et si j'ai des demandes volumineuses telles que des documents téléchargés (qui Je le fais), je ne veux pas le mettre en mémoire cache. En outre, Spring a probablement déjà mis en cache le @RequestBody Si je ne pouvais que le trouver.

Soit dit en passant, de nombreuses solutions recommandent d'utiliser la classe ContentCachingRequestWrapper Spring, mais d'après mon expérience, cela ne fonctionne pas. En plus de ne pas être documenté, en regardant son code source, il semble qu'il ne cache que les paramètres, mais pas le corps de la demande. Essayer d'obtenir le corps de la demande de cette classe entraîne toujours une chaîne vide.

Je suis donc à la recherche d'autres options que j'ai pu manquer. Merci d'avoir lu.

24
Uncle Long Hair

Vous pouvez référencer l'objet corps de demande à un bean à portée de demande. Et puis injectez ce bean à portée de requête dans votre gestionnaire d'exceptions pour récupérer le corps de la requête (ou d'autres beans de contexte de requête que vous souhaitez référencer).

// @Component
// @Scope("request")
@ManagedBean
@RequestScope
public class RequestContext {
    // fields, getters, and setters for request-scoped beans
}

@RestController
@RequestMapping("/api/v1/persons")
public class PersonController {

    @Inject
    private RequestContext requestContext;

    @Inject
    private PersonService personService;

    @PostMapping
    public Person savePerson(@RequestBody Person person) throws PersonServiceException {
         requestContext.setRequestBody(person);
         return personService.save(person);
    }

}

@ControllerAdvice
public class ExceptionMapper {

    @Inject
    private RequestContext requestContext;

    @ExceptionHandler(PersonServiceException.class)
    protected ResponseEntity<?> onPersonServiceException(PersonServiceException exception) {
         Object requestBody = requestContext.getRequestBody();
         // ...
         return responseEntity;
    }
}
3
Warren M. Nocos

La réponse acceptée crée un nouveau POJO pour faire circuler les choses, mais le même comportement peut être obtenu sans créer d'objets supplémentaires en réutilisant la demande http.

Exemple de code pour le mappage du contrôleur:

public ResponseEntity savePerson(@RequestBody Person person, WebRequest webRequest) {
    webRequest.setAttribute("person", person, RequestAttributes.SCOPE_REQUEST);

Et plus tard dans la classe/méthode ExceptionHandler, vous pouvez utiliser:

@ExceptionHandler(Exception.class)
public ResponseEntity exceptionHandling(WebRequest request,Exception thrown) {

    Person person = (Person) request.getAttribute("person", RequestAttributes.SCOPE_REQUEST);
2
WeMakeSoftware

Vous devriez pouvoir obtenir le contenu du corps de la demande en utilisant l'interface RequestBodyAdvice . Si vous implémentez ceci sur une classe annotée avec @ ControllerAdvice cela devrait être récupéré automatiquement.

Pour obtenir d'autres informations de demande comme la méthode HTTP et les paramètres de requête, j'utilise un intercepteur . Je capture toutes ces informations de demande de rapport d'erreur dans une variable ThreadLocal que j'efface sur le crochet afterCompletion dans ce même intercepteur.

La classe ci-dessous implémente cela et peut être utilisée dans votre ExceptionHandler pour obtenir toutes les informations de demande:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import Java.lang.reflect.Type;
import Java.util.HashMap;
import Java.util.Map;

@ControllerAdvice
public class RequestInfo extends HandlerInterceptorAdapter implements RequestBodyAdvice {
    private static final Logger logger = LoggerFactory.getLogger(RequestInfo.class);
    private static final ThreadLocal<RequestInfo> requestInfoThreadLocal = new ThreadLocal<>();

    private String method;
    private String body;
    private String queryString;
    private String ip;
    private String user;
    private String referrer;
    private String url;

    public static RequestInfo get() {
        RequestInfo requestInfo = requestInfoThreadLocal.get();
        if (requestInfo == null) {
            requestInfo = new RequestInfo();
            requestInfoThreadLocal.set(requestInfo);
        }
        return requestInfo;
    }

    public Map<String,String> asMap() {
        Map<String,String> map = new HashMap<>();
        map.put("method", this.method);
        map.put("url", this.url);
        map.put("queryParams", this.queryString);
        map.put("body", this.body);
        map.put("ip", this.ip);
        map.put("referrer", this.referrer);
        map.put("user", this.user);
        return map;
    }

    private void setInfoFromRequest(HttpServletRequest request) {
        this.method = request.getMethod();
        this.queryString = request.getQueryString();
        this.ip = request.getRemoteAddr();
        this.referrer = request.getRemoteHost();
        this.url = request.getRequestURI();
        if (request.getUserPrincipal() != null) {
            this.user = request.getUserPrincipal().getName();
        }
    }

    public void setBody(String body) {
        this.body = body;
    }

    private static void setInfoFrom(HttpServletRequest request) {
        RequestInfo requestInfo = requestInfoThreadLocal.get();
        if (requestInfo == null) {
            requestInfo = new RequestInfo();
        }
        requestInfo.setInfoFromRequest(request);
        requestInfoThreadLocal.set(requestInfo);
    }

    private static void clear() {
        requestInfoThreadLocal.remove();
    }

    private static void setBodyInThreadLocal(String body) {
        RequestInfo requestInfo = get();
        requestInfo.setBody(body);
        setRequestInfo(requestInfo);
    }

    private static void setRequestInfo(RequestInfo requestInfo) {
        requestInfoThreadLocal.set(requestInfo);
    }

    // Implementation of HandlerInterceptorAdapter to capture the request info (except body) and be able to add it to the report in case of an error

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        RequestInfo.setInfoFrom(request);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) {
        RequestInfo.clear();
    }

    // Implementation of RequestBodyAdvice to capture the request body and be able to add it to the report in case of an error

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return inputMessage;
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        RequestInfo.setBodyInThreadLocal(body.toString());
        return body;
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }
}
0
quintencls