【问题标题】:ContentCachingResponseWrapper Produces Empty ResponseContentCachingResponseWrapper 产生空响应
【发布时间】:2017-02-17 12:17:57
【问题描述】:

我正在尝试在Spring MVC 应用程序中实现用于记录请求和响应的过滤器。 我使用以下代码:

@Component
public class LoggingFilter extends OncePerRequestFilter {

    private static final Logger LOGGER = LoggerFactory.getLogger(LoggingFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);

        LOGGER.debug(REQUEST_MESSAGE_FORMAT, requestWrapper.getRequestURI(), requestWrapper.getMethod(), requestWrapper.getContentType(),
                new ServletServerHttpRequest(requestWrapper).getHeaders(), IOUtils.toString(requestWrapper.getInputStream(), UTF_8));

        filterChain.doFilter(requestWrapper, responseWrapper);

        LOGGER.debug(RESPONSE_MESSAGE_FORMAT, responseWrapper.getStatus(), responseWrapper.getContentType(),
                new ServletServerHttpResponse(responseWrapper).getHeaders(), IOUtils.toString(responseWrapper.getContentInputStream(), UTF_8));
    }
}

所以,我的请求和响应按预期记录。以下是日志:

2016-10-08 19:10:11.212 DEBUG 11072 --- [qtp108982313-19] by.kolodyuk.logging.LoggingFilter
----------------------------
ID: 1
URI: /resources/1
Http-Method: GET
Content-Type: null
Headers: {User-Agent=[curl/7.41.0], Accept=[*/*], Host=[localhost:9015]}
Body: 
--------------------------------------
2016-10-08 19:10:11.277 DEBUG 11072 --- [qtp108982313-19] by.kolodyuk.logging.LoggingFilter
----------------------------
ID: 1
Response-Code: 200
Content-Type: application/json;charset=UTF-8
Headers: {}
Body: {"id":"1"}
--------------------------------------

但是,返回的是空响应。这是curl 的输出:

$ curl http://localhost:9015/resources/1 --verbose
*   Trying ::1...
* Connected to localhost (::1) port 9015 (#0)
> GET /resources/1 HTTP/1.1
> User-Agent: curl/7.41.0
> Host: localhost:9015
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sat, 08 Oct 2016 17:10:11 GMT
< Content-Type: application/json;charset=UTF-8
< Content-Length: 0
<
* Connection #0 to host localhost left intact

有什么想法吗?

谢谢

【问题讨论】:

    标签: spring-mvc servlets spring-boot servlet-filters


    【解决方案1】:

    经过几个小时的努力,我终于找到了解决方案。

    简而言之,ContentCachingResponseWrapper.copyBodyToResponse()应该在filter方法的最后调用。

    ContentCachingResponseWrapper 通过从响应输出流中读取响应正文来缓存它。因此,流变为空。要将响应写回输出流,应使用ContentCachingResponseWrapper.copyBodyToResponse()

    【讨论】:

    • 他们应该将此添加到 ContentCachingResponseWrapper 的 javadoc 中
    • 过滤器为什么要做响应处理?
    • end 是否表示方法的最后一行?如果不是最后一行怎么办?我也尝试过并且工作过。恐怕最后一行会影响用户延迟?
    【解决方案2】:

    终于解决了问题。这是完美的解决方案:

    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.apache.commons.io.IOUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.*;
    import org.springframework.stereotype.Component;
    import org.springframework.web.filter.OncePerRequestFilter;
    import org.springframework.web.util.ContentCachingRequestWrapper;
    import org.springframework.web.util.ContentCachingResponseWrapper;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.net.URI;
    import java.util.Enumeration;
    import java.util.Map;
    
    import static java.nio.charset.StandardCharsets.UTF_8;
    import static net.logstash.logback.marker.Markers.appendFields;
    
    @Component
    public class LoggingFilter extends OncePerRequestFilter {
    
      private static final Logger LOGGER = LoggerFactory.getLogger(LoggingFilter.class);
    
      @Autowired
      private ObjectMapper objectMapper;
    
      @Override
      protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
    
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(httpServletRequest);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(httpServletResponse);
    
        filterChain.doFilter(requestWrapper, responseWrapper);
    
        String requestUrl = requestWrapper.getRequestURL().toString();
        HttpHeaders requestHeaders = new HttpHeaders();
        Enumeration headerNames = requestWrapper.getHeaderNames();
        while (headerNames.hasMoreElements()) {
          String headerName = (String) headerNames.nextElement();
          requestHeaders.add(headerName, requestWrapper.getHeader(headerName));
        }
        HttpMethod httpMethod = HttpMethod.valueOf(requestWrapper.getMethod());
        Map<String, String[]> requestParams = requestWrapper.getParameterMap();
    
        String requestBody = IOUtils.toString(requestWrapper.getInputStream(),UTF_8);
        JsonNode requestJson = objectMapper.readTree(requestBody);
    
        RequestEntity<JsonNode> requestEntity = new RequestEntity<>(requestJson,requestHeaders, httpMethod, URI.create(requestUrl));
        LOGGER.info(appendFields(requestEntity),"Logging Http Request");
    
    
        HttpStatus responseStatus = HttpStatus.valueOf(responseWrapper.getStatusCode());
        HttpHeaders responseHeaders = new HttpHeaders();
        for (String headerName : responseWrapper.getHeaderNames()) {
          responseHeaders.add(headerName, responseWrapper.getHeader(headerName));
        }
        String responseBody = IOUtils.toString(responseWrapper.getContentInputStream(), UTF_8);
        JsonNode responseJson = objectMapper.readTree(responseBody);
        ResponseEntity<JsonNode> responseEntity = new ResponseEntity<>(responseJson,responseHeaders,responseStatus);
        LOGGER.info(appendFields(responseEntity),"Logging Http Response");
        responseWrapper.copyBodyToResponse();
      }
    }
    

    【讨论】:

    • 你是如何注册这个过滤器的
    • @Component 注解会自动注册这个过滤器。
    • 那里为什么需要 WebUtils.getNativeResponse?
    【解决方案3】:

    我喜欢使用的模式是将其拆分为 2 个过滤器,一个用于提取原始正文,另一个用于记录日志 - 感觉更像 SRP

    @Slf4j        // lombok logging
    @Component    // spring loads filter into it's filter chain
    @Order(1)     // Best if this goes first (or early in filter chain)
    public class CachingBodyFilter implements Filter {
    
        @Override
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
            ContentCachingRequestWrapper reqWrapper = new ContentCachingRequestWrapper((HttpServletRequest) req);
            ContentCachingResponseWrapper resWrapper = new ContentCachingResponseWrapper((HttpServletResponse) res);
            try {
                chain.doFilter(reqWrapper, resWrapper);
                resWrapper.copyBodyToResponse();  // Necessary (see answer by StasKolodyuk above)
            } catch (IOException | ServletException e) {
                log.error("Error extracting body", e);
            }
        }
    
    }
    

    然后我们创建另一个过滤器来执行日志记录部分。

    @Slf4j
    @Component
    @Order(2)     // This needs to come after `CachingBodyFilter`
    public class PayloadLogFilter implements Filter {
    
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
            chain.doFilter(req, res);
    
            if (req instanceof ContentCachingRequestWrapper) {
                ContentCachingRequestWrapper reqWrapper = (ContentCachingRequestWrapper) req;
                String payload = new String (reqWrapper.getContentAsByteArray(), "utf-8");
                log.debug("Request [ {} ] has payload [ {} ]", reqWrapper.getRequestURI(), payload);
            }
        }
    
    }
    

    将这些拆分的一个很好的优势是其他类(例如 Spring AOP 拦截器或 Spring 控制器)也可以访问/使用 HTTP 主体。

    【讨论】:

      猜你喜欢
      • 2020-08-04
      • 2010-12-26
      • 2018-06-15
      • 2021-12-13
      • 1970-01-01
      • 2013-03-16
      • 2015-10-20
      • 2018-02-27
      相关资源
      最近更新 更多