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

js5cn81o  于 2022-12-10  发布在  Spring
关注(0)|答案(6)|浏览(542)

我使用的是 Boot ,其中包含spring-web-4.3.3。我有一个用@ControllerAdvice注解的类和用@ExceptionHandler注解的方法来处理服务代码引发的异常。在处理这些异常时,我希望记录PUT和POST操作请求中的@RequestBody,以便查看导致问题的请求主体,该请求主体位于我病例对诊断至关重要。
根据Spring Docs@ExceptionHandler方法的方法签名可以包括各种东西,包括HttpServletRequest。请求主体通常可以通过getInputStream()getReader()从这里获得,但是如果我的控制器方法像我的所有方法一样解析"@RequestBody Foo fooBody"的请求主体,HttpServletRequest's输入流或读取器在调用我的异常处理程序方法时已经关闭。与here描述的问题类似。使用servlet时,请求主体只能读取一次,这是一个常见的问题。
不幸的是,@RequestBody不是异常处理方法可用的选项之一,如果是的话,我可以使用它。
我可以将InputStream添加到异常处理程序方法中,但这最终与HttpServletRequest的InputStream是一样的,因此存在相同的问题。
我还尝试使用((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()获取当前请求,这是获取当前请求的另一个技巧,但这最终是Spring传递到异常处理程序方法中的同一个HttpServletRequest,因此存在相同的问题。
我读过一些解决方案,如thisthis,它们涉及在过滤器链中插入一个自定义请求 Package 器,该 Package 器将读取请求的内容并缓存它们,以便可以多次读取它们。(并可能引入性能或稳定性问题),并且如果我有任何大型请求(如上传的文档)另外,Spring可能已经在某个地方缓存了@RequestBody,如果我能找到它的话。
顺便说一句,许多解决方案建议使用ContentCachingRequestWrapper Spring类,但根据我的经验,这并不起作用。除了没有被记录,看它的源代码,它看起来只缓存了参数,而不是请求主体。试图从这个类中获取请求主体总是导致一个空字符串。
所以我正在寻找任何其他的选择,我可能已经错过了。谢谢阅读。

nue99wik

nue99wik1#

Accepted answer创建一个新的POJO来传递内容,但是通过重用http请求,无需创建额外的对象也可以实现相同的行为。
控制器Map的示例代码:

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);
3j86kqsm

3j86kqsm2#

您可以将请求主体对象引用到请求作用域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;
    }
}
k5ifujac

k5ifujac3#

您应该能够通过使用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;
    }
}
rpppsulh

rpppsulh4#

下面是我用于某些字段验证控件的Kotlin语法解决方案。
我需要增强@RestControllerAdvice的默认handleMethodArgumentNotValid(...)方法,以便系统地记录嵌入在同一请求主体对象中的字段。

override fun handleMethodArgumentNotValid(e: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatus, request: WebRequest): ResponseEntity<Any> {
        val error = e.bindingResult.fieldErrors.first()
        val requestBody = try {
            val field = error.javaClass.getDeclaredField("violation").apply { trySetAccessible() }
            ((field[error] as ConstraintViolationImpl<Any>).rootBean as MyRequestBodyObject)
        } catch (ex : Exception) {
            //do some failsafe here
        }
    }
2ledvvac

2ledvvac5#

只是对quintencl的答案的增强
我得到了请求主体,可以在异常处理程序类中的任何地方使用它。

@ControllerAdvice
public class CustomErrorHandler extends ResponseEntityExceptionHandler implements RequestBodyAdvice {
    
    ...

    private Object reqBody;

    ...

    @ExceptionHandler(NoSuchElementException.class)
    public ResponseEntity<Object> handleNoSuchElementException(final NoSuchElementException ex,
            final WebRequest request) {
        System.out.println("===================================" + reqBody);
        return handleNotFoundException(ex, request);
    }

    ...

    @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) throws IOException {
        return inputMessage;
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
            Class<? extends HttpMessageConverter<?>> converterType) {

        // capture request body here to use in our controller advice class
        this.reqBody = body;

        return body;
    }

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

}
scyqe7ek

scyqe7ek6#

请参见此处:https://stackoverflow.com/a/61813076/1036433-获取访问HttpServerRequest的干净方法。

相关问题