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

Unc*_*air 25 spring servlets exception-handling spring-boot

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

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

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

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

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

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

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

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

War*_*cos 8

您可以将请求正文对象引用到请求范围的 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;
    }
}
Run Code Online (Sandbox Code Playgroud)


WeM*_*are 6

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

控制器映射的示例代码:

public ResponseEntity savePerson(@RequestBody Person person, WebRequest webRequest) {
    webRequest.setAttribute("person", person, RequestAttributes.SCOPE_REQUEST);
Run Code Online (Sandbox Code Playgroud)

稍后在 ExceptionHandler 类/方法中,您可以使用:

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

    Person person = (Person) request.getAttribute("person", RequestAttributes.SCOPE_REQUEST);
Run Code Online (Sandbox Code Playgroud)

  • 这段代码不起作用,因为执行不会进入控制器方法内部,而是立即传递给异常处理方法。因此,请求正文的属性为空。 (3认同)

qui*_*cls 5

您应该能够使用RequestBodyAdvice接口获取请求正文的内容。如果您在使用@ControllerAdvice注释的类上实现此功能,则应该会自动选取它。

为了获取其他请求信息(例如 HTTP 方法和查询参数),我使用了拦截器。我正在捕获所有这些请求信息,以在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;
    }
}
Run Code Online (Sandbox Code Playgroud)