如何在@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 不会在任何地方缓存请求正文。您可以创建自己的HandlerMapping
和HttpServletRequestWrapper
。
【参考方案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 捕获 HTTP 405“不允许的方法”异常?
Spring Async 最佳实践:ExceptionHandler