【问题标题】:How to read request body in HandlerInterceptor?如何在 HandlerInterceptor 中读取请求正文?
【发布时间】:2020-03-06 01:01:52
【问题描述】:

我有 Spring Boot,我需要在 DB 中记录用户操作,所以我写了 HandlerInterceptor:

@Component
public class LogInterceptor implements HandlerInterceptor {
@Autovired
private LogUserActionService logUserActionService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
throws IOException {
    String userName = SecurityContextHolder.getContext().getAuthentication().getName();
    String url = request.getRequestURI();
    String queryString = request.getQueryString() != null ? request.getQueryString() : "";
    String body = "POST".equalsIgnoreCase(request.getMethod()) ? new BufferedReader(new InputStreamReader(request.getInputStream())).lines().collect(Collectors.joining(System.lineSeparator())) : queryString;
    logUserActionService.logUserAction(userName, url, body);
    return true;
}
}

但是根据这个答案Get RequestBody and ResponseBody at HandlerInterceptor“RequestBody 只能读取一次”,所以据我所知,我读取了输入流,然后 Spring 尝试做同样的事情,但是流已经被读取并且我收到了一个错误: “缺少所需的请求正文...”

所以我尝试了不同的方法来制作缓冲输入流,即:

HttpServletRequest httpServletRequest = new ContentCachingRequestWrapper(request);
new BufferedReader(new InputStreamReader(httpServletRequest.getInputStream())).lines().collect(Collectors.joining(System.lineSeparator()))

或者

InputStream bufferedInputStream = new BufferedInputStream(request.getInputStream());

但没有任何帮助 我也尝试使用

@ControllerAdvice
public class UserActionRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter {

但它只有正文,没有 URL 或请求参数等请求信息 也尝试使用过滤器,但结果相同。

所以我需要一种从请求中获取信息的好方法,例如用户、URL、参数、正文(如果存在)并将其写入数据库。

【问题讨论】:

  • 请使用过滤器检查这个,它对我有用。 gist.github.com/int128/e47217bebdb4c402b2ffa7cc199307ba
  • Harshit,您的示例中没有正文阅读,只有 URL 等。
  • 是的,检查logRequestBody方法。
  • Harshit,你确定这段代码有效吗?因为我根据您的示例发送了 JSON 正文并编写了下一个代码: new ContentCachingRequestWrapper(request).getContentAsByteArray();它返回空字节数组,但我的控制器按预期得到了我的 DTO 实体。所以我有正文,但方法 getContentAsByteArray 什么也没返回。

标签: java spring spring-boot spring-mvc


【解决方案1】:

您可以使用Filter 记录请求正文。

public class LoggingFilter implements Filter {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
        try {
            chain.doFilter(wrappedRequest, res);
        } finally {
            logRequestBody(wrappedRequest);
        }
    }

    private static void logRequestBody(ContentCachingRequestWrapper request) {

        byte[] buf = request.getContentAsByteArray();
        if (buf.length > 0) {
            try {
                String requestBody = new String(buf, 0, buf.length, request.getCharacterEncoding());
                System.out.println(requestBody);
            } catch (Exception e) {
                System.out.println("error in reading request body");
            }
        }
    }
}

这里需要注意的主要是你必须在过滤器链中传递ContentCachingRequestWrapper的对象,否则你将无法在其中获取请求内容。

在上面的示例中,如果您使用chain.doFilter(req, res)chain.doFilter(request, res),那么您将不会在wrappedRequest 对象中获取请求正文。

【讨论】:

  • 工作得很好,我没想到,这就是我正在寻找的解决方案,谢谢!
  • 这确实有效,但仅当您在chain.doFilter(wrappedRequest, res) 通话后登录时。原因是ContentCachingRequestWrapper 只有在读取 InputStream 时才会填充缓存。如果您想在处理请求之前记录请求负载,那么您需要使用自己的包装器扩展 ContentCachingRequestWrapper
【解决方案2】:

要记录 HTTP 请求和响应,您可以使用 RequestBodyAdviceAdapterResponseBodyAdvice。在这里,它以我的方式使用。

CustomRequestBodyAdviceAdapter.java

@ControllerAdvice
public class CustomRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter {

    @Autowired
    HttpServletRequest httpServletRequest;

    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

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

        // here you can full log httpServletRequest and body.

        return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
    }
}

CustomResponseBodyAdviceAdapter.java

@ControllerAdvice
public class CustomResponseBodyAdviceAdapter implements ResponseBodyAdvice<Object> {

    @Autowired
    private LoggingService loggingService;

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType,
            Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (serverHttpRequest instanceof ServletServerHttpRequest && serverHttpResponse instanceof ServletServerHttpResponse) {

            // here you can full log httpServletRequest and body.
        }
        return o;
    }
}

AdviceAdapter 以上无法处理GET 请求。所以,你可以使用HandlerInterceptor

CustomWebConfigurerAdapter.java

@Component
public class CustomWebConfigurerAdapter implements WebMvcConfigurer {

   @Autowired
   private CustomLogInterceptor httpServiceInterceptor;

   @Override
   public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(httpServiceInterceptor);
   }
}

CustomLogInterceptor.java

@Component
public class CustomLogInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (DispatcherType.REQUEST.name().equals(request.getDispatcherType().name()) && request.getMethod().equals(HttpMethod.GET.name())) {

            // here you can full log httpServletRequest and body for GET Request.

        }
        return true;
    }
}

在这里你可以参考我的 git 中的完整源代码。

springboot-http-request-response-loging-with-json-logger

+Feature => 已经与ELK(Elasticsearch、Logstash、Kibana)集成

【讨论】:

  • 你读过我的问题吗?我说我试过了,“但它只有正文,没有 URL 或请求参数之类的请求信息”。有什么方法可以使用 RequestBodyAdviceAdapter 获取 URL 和查询参数?可能是我儿子什么都没看到,但 RequestBodyAdviceAdapter 方法中没有请求参数。如果您告诉我如何在 RequestBodyAdviceAdapter 中获取 URL 和请求参数,那就太好了!
  • 关于 Interseptor,我说如果我在拦截器中读取正文,我会得到一个错误,你建议我使用 CustomLogInterceptor,为什么?你读过我的问题吗?那里的身体如何准备好?
【解决方案3】:

您可以使用 RequestBodyAdviceAdapter 获取 POST/PUT 请求的请求正文数据。您可以使用 HandlerInterceptorAdapter 进行 GET 调用。这是一个工作示例 - https://frandorado.github.io/spring/2018/11/15/log-request-response-with-body-spring.html

@ControllerAdvice
public class CustomRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter
{

@Autowired
HttpServletRequest httpServletRequest;

private static final Log LOGGER = LogFactory.getLog(CustomRequestBodyAdviceAdapter.class);

private static final Charset DEFAULT_CHARSET = ISO_8859_1;


@Override
public boolean supports(MethodParameter methodParameter, Type type, 
                        Class<? extends HttpMessageConverter<?>> aClass)
{
    return true;
}

@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage,
                            MethodParameter parameter, Type targetType,
        Class<? extends HttpMessageConverter<?>> converterType) 
{
    Instant startTime = Instant.now();
    StringBuilder stringBuilder = new StringBuilder();

    
    stringBuilder.append("REQUEST call Starts :: Start Time : %s ").append(startTime);

    try
    {
        logRequest(httpServletRequest, body);

    } 
    catch (IOException e) 
    {
        LOGGER.info("Exception getting the Request Body into the Log: {}" + e.getMessage());
    }
    

public void logRequest(HttpServletRequest httpServletRequest, Object body) throws IOException
{
    StringBuilder stringBuilder = new StringBuilder();
    Map<String, String> parameters = buildParametersMap(httpServletRequest);
    
    
    stringBuilder.append("REQUEST ");
    stringBuilder.append("method=[").append(httpServletRequest.getMethod()).append("] ");
    stringBuilder.append("path=[").append(httpServletRequest.getRequestURI()).append("] ");
    stringBuilder.append("headers=[").append(buildHeadersMap(httpServletRequest)).append("] ");
    
    if (!parameters.isEmpty())
    {
        stringBuilder.append("parameters=[").append(parameters).append("] ");
    }
    
    if (body != null)
    {
        stringBuilder.append("body=[" + body + "]");
    }

    ObjectMapper objectMapper = new ObjectMapper();
    
    String jsonInString = null;
    try 
    {
        jsonInString = objectMapper.writer().writeValueAsString(body);
    } 
    catch (JsonProcessingException e) 
    {
        throw new RestApiException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
    }
    
    stringBuilder.append("REQUEST Body = [").append(jsonInString).append("] ");
    
    LOGGER.info("BODY DATA >>>> " + jsonInString);
    LOGGER.info("Body - : {}" + stringBuilder);
}



private Map<String, String> buildParametersMap(HttpServletRequest httpServletRequest)
{
    Map<String, String> resultMap = new HashMap<>();
    Enumeration<String> parameterNames = httpServletRequest.getParameterNames();
    
    while (parameterNames.hasMoreElements())
    {
        String key = parameterNames.nextElement();
        String value = httpServletRequest.getParameter(key);
        resultMap.put(key, value);
    }
    
    return resultMap;
}

private Map<String, String> buildHeadersMap(HttpServletRequest request)
{
    Map<String, String> map = new HashMap<>();
    
    Enumeration<String> headerNames = request.getHeaderNames();
    while (headerNames.hasMoreElements()) 
    {
        String key = headerNames.nextElement();
        String value = request.getHeader(key);
        map.put(key, value);
    }
    
    return map;
}
}

我在这里使用了 ObjectMapper,因为我需要将正文响应作为原始 JSON 对象,但是在将正文转换为 Java 对象之后调用 afterBodyRead()

【讨论】:

  • 请发布最小示例。对提问者会更有帮助。
  • @Daniel.Wang 感谢您的建议。添加了相同的工作代码。
【解决方案4】:

我发现这解决了我为应用程序/json 内容类型复制请求缓冲区的问题。它还展示了如何将包装器扩展为 cmets 到 Harshit 解决方案所提到的。

https://levelup.gitconnected.com/how-to-log-the-request-body-in-a-spring-boot-application-10083b70c66

重要的是您需要一个过滤器来将新请求传递给服务器。

@Component
public class LoggingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
        if (Arrays.asList("POST", "PUT").contains(httpRequest.getMethod())) {
            CustomHttpRequestWrapper requestWrapper = new CustomHttpRequestWrapper(httpRequest);
            requestWrapper.setAttribute("input", requestWrapper.getBodyInStringFormat());
            filterChain.doFilter(requestWrapper, servletResponse);
            return;
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

记录器需要一个自定义包装器,而 spring boot 提供的一个似乎不足以处理 application/json 类型的消息。

public class CustomHttpRequestWrapper extends HttpServletRequestWrapper {

    public String getBodyInStringFormat() {
        return bodyInStringFormat;
    }

    private final String bodyInStringFormat;

    public CustomHttpRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        bodyInStringFormat = readInputStreamInStringFormat(request.getInputStream(), Charset.forName(request.getCharacterEncoding()));
    }

    private String readInputStreamInStringFormat(InputStream stream, Charset charset) throws IOException {
        return getString(stream, charset);
    }

    static String getString(InputStream stream, Charset charset) throws IOException {
        final int MAX_BODY_SIZE = 1024;
        final StringBuilder bodyStringBuilder = new StringBuilder();
        if (!stream.markSupported()) {
            stream = new BufferedInputStream(stream);
        }

        stream.mark(MAX_BODY_SIZE + 1);
        final byte[] entity = new byte[MAX_BODY_SIZE + 1];
        final int bytesRead = stream.read(entity);

        if (bytesRead != -1) {
            bodyStringBuilder.append(new String(entity, 0, Math.min(bytesRead, MAX_BODY_SIZE), charset));
            if (bytesRead > MAX_BODY_SIZE) {
                bodyStringBuilder.append("...");
            }
        }
        stream.reset();

        return bodyStringBuilder.toString();
    }

    @Override
    public BufferedReader getReader()  {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream ()  {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bodyInStringFormat.getBytes());

        return new ServletInputStream() {
            private boolean finished = false;

            @Override
            public boolean isFinished() {
                return finished;
            }

            @Override
            public int available()  {
                return byteArrayInputStream.available();
            }

            @Override
            public void close() throws IOException {
                super.close();
                byteArrayInputStream.close();
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
                throw new UnsupportedOperationException();
            }

            public int read ()  {
                int data = byteArrayInputStream.read();
                if (data == -1) {
                    finished = true;
                }
                return data;
            }
        };
    }
}

【讨论】:

    猜你喜欢
    • 2023-04-07
    • 2019-01-17
    • 1970-01-01
    • 1970-01-01
    • 2018-12-25
    • 1970-01-01
    • 2016-04-20
    • 2019-11-30
    • 2014-11-29
    相关资源
    最近更新 更多