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

Posted

技术标签:

【中文标题】如何在@ExceptionHandler(Spring REST)中获取@RequestBody【英文标题】:How to get the @RequestBody in an @ExceptionHandler (Spring REST) 【发布时间】:2017-09-16 01:45:01 【问题描述】:

我正在使用 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 相同,因此存在相同的问题。

我已经阅读了一些解决方案,例如 this 和 this,它们涉及在过滤器链中插入一个自定义请求包装器,该包装器将读取请求的内容并缓存它们,以便可以多次读取它们。我不喜欢这个解决方案,因为我不想仅仅为了实现日志记录而中断整个过滤器/请求/响应链(并可能引入性能或稳定性问题),并且如果我有任何大型请求,例如上传的文档(其中我愿意),我不想将其缓存在内存中。此外,如果我只能找到它,Spring 可能已经将@RequestBody 缓存在某个地方。

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

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

【问题讨论】:

没有快速的内置解决方案来做你想做的事。 Spring 不会在任何地方缓存请求正文。您可以创建自己的HandlerMappingHttpServletRequestWrapper 【参考方案1】:

您可以将请求主体对象引用到请求范围的 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;
    

【讨论】:

是的,谢谢,我终于有时间验证这一点。它是“@RequestScope”,而不是“@RequestScoped”,我必须在我的所有 POST 方法中添加一些内容,但这正是我想要的,感谢您的回答。 为了使它更容易和组织起来,你可以添加一个拦截器/建议来捕获请求范围的bean到你想要的所有端点。【参考方案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);

【讨论】:

这段代码不起作用,因为执行没有进入控制器方法内部,它立即传递给异常处理方法。因此,请求正文的属性为空。【参考方案3】:

您应该可以使用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;
    

【讨论】:

我有一个测试用例,请求没有指定Content-Type,这会产生一个HttpMediaTypeNotSupportedException。在我的 handleHttpMediaTypeNotSupported 方法中,还没有调用 afterBodyRead。【参考方案4】:

请参阅此处:https://***.com/a/61813076/1036433 - 了解访问 HttpServerRequest 的干净方式。

【讨论】:

以上是关于如何在@ExceptionHandler(Spring REST)中获取@RequestBody的主要内容,如果未能解决你的问题,请参考以下文章

如何确保 @ExceptionHandler(Exception.class) 在 Spring Boot 中最后被调用?

如何覆盖 $exceptionHandler 实现

如何使用@ExceptionHandler 捕获 HTTP 405“不允许的方法”异常?

Spring Async 最佳实践:ExceptionHandler

@ExceptionHandler注解和@ControllerAdvice注解

统一异常处理@ExceptionHandler