如何在@ExceptionHandler(Spring REST)中获取@RequestBody

时间:2017-04-19 17:25:49

标签: spring servlets spring-boot exception-handling

我正在使用Spring Boot 1.4.1,其中包括spring-web-4.3.3。我有一个用@ControllerAdvice注释的类和用@ExceptionHandler注释的方法来处理服务代码抛出的异常。在处理这些异常时,我想记录作为PUT和POST操作请求一部分的@RequestBody,这样我就可以看到导致问题的请求体,在我的情况下,这对诊断至关重要。

Spring Docs @ExceptionHandler方法的方法签名可以包含各种内容,包括HttpServletRequest。通常可以通过getInputStream()getReader()从此处获取请求正文,但如果我的控制器方法像"@RequestBody Foo fooBody"一样解析请求正文,那么HttpServletRequest's输入在我的异常处理程序方法被调用时,流或读取器已经关闭。本质上,Spring已经读取了请求主体,类似于here所描述的问题。使用servlet是一个常见问题,请求体只能读取一次。

不幸的是@RequestBody不是异常处理程序方法可用的选项之一,如果是,那么我可以使用它。

我可以在异常处理程序方法中添加InputStream,但最终与HttpServletRequest的InputStream相同,因此具有相同的问题。

我还尝试使用((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()获取当前请求,这是获取当前请求的另一个技巧,但这最终与Spring传递到异常处理程序方法的HttpServletRequest相同,因此具有相同的问题。 / p>

我已经阅读了一些像thisthis这样的解决方案,这些解决方案涉及在过滤器链中插入一个自定义请求包装器,它将读取请求的内容并对其进行缓存以便可以阅读更多内容不止一次。我不喜欢这个解决方案,因为我不想中断整个过滤器/请求/响应链(并可能引入性能或稳定性问题)只是为了实现日志记录,如果我有任何大的请求,如上传的文件(我这样做),我不想将其缓存在内存中。此外,如果我只能找到它,那么Spring可能已经在某处缓存了@RequestBody

顺便提一下,许多解决方案建议使用ContentCachingRequestWrapper Spring类,但根据我的经验,这不起作用。除了没有记录之外,查看其源代码看起来它只是缓存参数,而不是请求体。尝试从此类获取请求正文总是会产生一个空字符串。

所以我正在寻找我可能错过的任何其他选择。谢谢你的阅读。

3 个答案:

答案 0 :(得分:3)

您可以将请求正文对象引用到请求范围的bean。然后在您的异常处理程序中注入请求范围的bean以检索请求主体(或您希望引用的其他请求上下文bean)。

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

答案 1 :(得分:2)

接受的答案会创建一个新的POJO来传递事物,但是可以实现相同的行为而无需通过重用http请求来创建其他对象。

控制器映射的示例代码:

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

然后在ExceptionHandler类/方法中可以使用:

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

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

答案 2 :(得分:0)

您应该能够使用RequestBodyAdvice界面获取请求正文的内容。如果在带有@ControllerAdvice注释的类上实现此功能,则应自动将其拾取。

要获取其他请求信息(例如HTTP方法和查询参数),我正在使用interceptor。我正在ThreadLocal变量中捕获所有用于错误报告的请求信息,该变量在同一拦截器的afterCompletion钩子上清除了。

下面的类实现了这一点,可以在您的ExceptionHandler中使用它来获取所有请求信息:

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