【问题标题】:How to log properly http requests with Spring MVC如何使用 Spring MVC 正确记录 http 请求
【发布时间】:2011-10-01 15:43:39
【问题描述】:

您好,我一直在尝试找出在我的应用程序中记录 http 请求的通用方法,到目前为止没有运气,这是我现在处理日志记录的方式,即:

@RequestMapping(value="register", method = RequestMethod.POST)
    @ResponseBody
    public String register(@RequestParam(value="param1",required=false) String param1, @RequestParam("param2") String param2, @RequestParam("param3") String param3, HttpServletRequest request){
        long start = System.currentTimeMillis();
        logger.info("!--REQUEST START--!");

        logger.info("Request URL: " + request.getRequestURL().toString());

        List<String> requestParameterNames = Collections.list((Enumeration<String>)request.getParameterNames());
        logger.info("Parameter number: " + requestParameterNames.size()); 

 for (String parameterName : requestParameterNames){
           logger.info("Parameter name: " + parameterName + " - Parameter value: " + request.getParameter(parameterName));
        }
                  //Some processing logic, call to the various services/methods with different parameters, response is always String(Json)
        String response = service.callSomeServiceMethods(param1,param2,param3);

logger.info("Response is: " + response);

        long end = System.currentTimeMillis();
        logger.info("Requested completed in: " + (end-start) + "ms");
        logger.info("!--REQUEST END--!");   

        return response;
    }

所以我现在对不同的控制器/方法所做的就是从方法内部的开头复制所有内容,直到处理逻辑因方法而异,然后从下面复制所有内容,如上面的模板所示。

这有点乱,而且有很多代码重复(我不喜欢)。但我需要记录所有内容。

有没有人对这种日志记录有更多经验,有人可以对此有所了解吗?

【问题讨论】:

标签: java spring logging spring-mvc


【解决方案1】:

编辑:另外,请参阅@membersound 对此答案的评论,这会改进此答案。

Spring 支持这一点。见CommonsRequestLoggingFilter。如果使用 Spring Boot,只需注册一个该类型的 bean,Boot 就会将其应用于过滤器链。喜欢:

@Bean
public Filter logFilter() {
    CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
    filter.setIncludeQueryString(true);
    filter.setIncludePayload(true);
    filter.setMaxPayloadLength(5120);
    return filter;
}

此外,此日志过滤器要求将日志级别设置为 DEBUG。例如。在 logback.xml 中执行此操作:

<logger name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="DEBUG"/>

【讨论】:

  • 你也可以覆盖:'​​CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter() { protected boolean shouldLog(javax.servlet.http.HttpServletRequest request) {return true;}; };
  • @membersound 覆盖 shouldLog() 方法以始终返回 true 是不够的,因为 CommonsRequestLoggingFilter 使用 logger.debug 来显示消息。如果日志级别未设置为调试消息将不会出现。
  • @PeterSzanto 当然是的。这只是将 maxPayload 设置为显式值的建议/替代方法。
  • 如果我使用 java 日志而不是 logback 怎么办?如何将此输出定向到我的记录器?
【解决方案2】:

使用interceptor

  • 扩展HandlerInterceptorAdapter并覆盖preHandle
  • dispatcher-servlet.xml 中用&lt;mvc:interceptors&gt; 定义它

它将针对每个请求运行。

【解决方案3】:

读取请求的主要问题是,一旦输入流被消耗,它就消失了……并且无法再次读取。所以输入流必须被缓存。 Spring 提供了几个有用的类,即 ContentCachingRequestWrapperContentCachingResponseWrapper,而不是编写自己的缓存类(可以在 web 上的多个地方找到)。这些类可以非常有效地使用,例如,在用于日志记录的过滤器中。

在 web.xml 中定义一个过滤器:

<filter>
    <filter-name>loggingFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>loggingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

由于过滤器被声明为DelegatingFilterProxy,因此可以使用@Component 或@Bean 注解将其声明为bean。在 loggingFilter 的 doFilter 方法中,使用 spring 提供的类包装请求和响应,然后将其传递给过滤器链:

HttpServletRequest requestToCache = new ContentCachingRequestWrapper(request);
HttpServletResponse responseToCache = new ContentCachingResponseWrapper(response);
chain.doFilter(requestToCache, responseToCache);
String requestData = getRequestData(requestToCache);
String responseData = getResponseData(responseToCache);

在chain.doFilter() 之后输入流被消费后,输入流将被缓存在包装的请求中。然后可以通过如下方式访问:

public static String getRequestData(final HttpServletRequest request) throws UnsupportedEncodingException {
    String payload = null;
    ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
    if (wrapper != null) {
        byte[] buf = wrapper.getContentAsByteArray();
        if (buf.length > 0) {
            payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
        }
    }
    return payload;
}

但是,响应的情况有些不同。由于响应在将其传递给过滤器链之前也被包装了,因此在返回的途中它也将被缓存到输出流中。但是由于输出流也将被消耗,因此您必须使用 wrapper.copyBodyToResponse() 将响应复制回输出流。见下文:

public static String getResponseData(final HttpServletResponse response) throws IOException {
    String payload = null;
    ContentCachingResponseWrapper wrapper =
        WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
    if (wrapper != null) {
        byte[] buf = wrapper.getContentAsByteArray();
        if (buf.length > 0) {
            payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
            wrapper.copyBodyToResponse();
        }
    }
    return payload;
}

希望对你有帮助!

【讨论】:

  • 真的很有帮助——但是这样一来,请求的打印就会在dofilter完成之后进行——是不是先打印请求数据,再dofilter,再打印响应数据?
  • 是的,可以在 doFilter 之前读取请求数据,但随后的任何过滤器或控制器中都无法再次读取所消耗的输入流(除非从缓存的输入流中读取)
  • 我正在尝试使用 ContentCachingRequestWrapper 但是当尝试从它的 getInputStream() 读取字符串时,我总是得到一个空字符串。看不懂它的内部类ContentCachingInputStream的实现?
  • 看起来输入流在你阅读之前已经被消耗掉了。如果它被消耗,那么它将被缓存在ContentCachingRequestWrappercachedContent 字段中。您可以致电getContentAsByteArray() 进行检查。您也可以在grepcode.com/file/repo1.maven.org/maven2/org.springframework/…查看源代码
  • B.Ali - 谢谢你的回答。您能告诉我如何将自定义过滤器连接到弹簧。我的意思是应该有哪个配置文件?
【解决方案4】:

这是我写的一个小库,你可以使用:spring-mvc-logger

我通过 maven Central 提供了它:

<dependency>
    <groupId>com.github.isrsal</groupId>
    <artifactId>spring-mvc-logger</artifactId>
    <version>0.2</version>
</dependency>

【讨论】:

  • 我正在使用它,不错的小工具,感谢 Israel!
  • 异步响应(servlet 3)怎么样?好像不行。
【解决方案5】:

补充@B.Ali 的回答。如果您在 spring 异步请求(serlvet 3.0 或更高版本)处理场景中使用它,那么以下代码对我有用。

public class OncePerRequestLoggingFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    boolean isFirstRequest = !isAsyncDispatch(request);
    HttpServletRequest requestToUse = request;
    HttpServletResponse responseToUse = response;

    // The below check is critical and if not there, then the request/response gets corrupted.
    // Probably because in async case the filter is invoked multiple times.
    if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
        requestToUse = new ContentCachingRequestWrapper(request);
    }

    if (isFirstRequest && !(response instanceof ContentCachingResponseWrapper)) {
        responseToUse = new ContentCachingResponseWrapper(response);
    }

    filterChain.doFilter(requestToUse, responseToUse);

    if (!isAsyncStarted(request)) {
        ContentCachingResponseWrapper responseWrapper =
                WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        responseWrapper.copyBodyToResponse(); // IMPORTANT to copy it back to response
    }
}

@Override
protected boolean shouldNotFilterAsyncDispatch() {
    return false; // IMPORTANT this is true by default and wont work in async scenario.
}

}

【讨论】:

    【解决方案6】:

    作为任何技术答案......这取决于...... 您正在使用的技术堆栈以及您的要求是什么。

    例如,您希望日志记录越通用,您就越希望提前进行。在您的情况下,您只记录启用了日志记录并在 spring 上下文中处理的请求。所以你可能会“错过”其他请求。

    我会查看您用于运行应用程序的容器或 Web 服务器。这将消除对 Spring 的这种依赖。 Plus 容器为您提供了插入日志提供程序的灵活性,然后配置日志外部代码的格式。 例如,如果您使用 Apache Web 服务器,请使用 Apache Web 服务器日志记录在访问日志记录层中记录所有 HTTP 请求。但要小心,一些日志记录选项有性能损失。仅记录您真正需要的访问模式监控透视图。

    如果您使用的是 tomcat,那么 tomcat 也将允许您记录内容。在您正在使用的 tomcat 的 tomcat 文档中搜索 Access Valve。这将打开一个充满可能性的世界。

    更广泛的日志记录应该是异常策略的领域,即当系统出现问题时您希望查看的详细信息。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-06-09
      • 1970-01-01
      • 2012-03-13
      • 1970-01-01
      • 2013-10-09
      • 1970-01-01
      • 2018-01-12
      相关资源
      最近更新 更多