【问题标题】:Spring Boot - How to log all requests and responses with exceptions in single place?Spring Boot - 如何在一个地方记录所有带有异常的请求和响应?
【发布时间】:2016-02-18 02:41:10
【问题描述】:

我正在使用 spring boot 开发 rest api。我需要使用输入参数(使用方法,例如 GET、POST 等)、请求路径、查询字符串、此请求的相应类方法、此操作的响应(成功和错误)记录所有请求。例如:

请求成功:

http://example.com/api/users/1

日志应如下所示:

{
   HttpStatus: 200,
   path: "api/users/1",
   method: "GET",
   clientIp: "0.0.0.0",
   accessToken: "XHGu6as5dajshdgau6i6asdjhgjhg",
   method: "UsersController.getUser",
   arguments: {
     id: 1 
   },
   response: {
      user: {
        id: 1,
        username: "user123",
        email: "user123@example.com"   
      }
   },
   exceptions: []       
}

或者请求错误:

http://example.com/api/users/9999

日志应该是这样的:

{
   HttpStatus: 404,
   errorCode: 101,                 
   path: "api/users/9999",
   method: "GET",
   clientIp: "0.0.0.0",
   accessToken: "XHGu6as5dajshdgau6i6asdjhgjhg",
   method: "UsersController.getUser",
   arguments: {
     id: 9999 
   },
   returns: {            
   },
   exceptions: [
     {
       exception: "UserNotFoundException",
       message: "User with id 9999 not found",
       exceptionId: "adhaskldjaso98d7324kjh989",
       stacktrace: ...................    
   ]       
}

我希望请求/响应是一个单一的实体,在成功和错误的情况下都包含与该实体相关的自定义信息。

春季实现这一目标的最佳实践是什么,可能是过滤器?如果是的话,你能提供具体的例子吗?

我玩过@ControllerAdvice@ExceptionHandler,但正如我所提到的,我需要在一个地方(和一个日志)处理所有成功和错误请求。

【问题讨论】:

  • 可能通过日志记录 ServletFilter(例如 stackoverflow.com/a/2171633/995891 ),或者 HandlerInterceptor 但这可能无法很好地记录答案中提到的响应:concretepage.com/spring/spring-mvc/… - HandlerInterceptor 有权访问该方法(方法:“UsersController.getUser”)。这在 servlet 过滤器中是未知的。
  • 不过,即使您在应用层添加过滤器或任何解决方案,您也不会记录所有请求,例如HTTP 500 服务器错误不会被记录,因为在应用层会抛出未处理的异常时,默认嵌入的 tomcat 错误页面将在吞下异常后显示,当然不会保留日志。此外,如果您检查 user1817243 答案,如果出现任何异常,他将再次不记录请求,但他会记录异常(!!)。
  • 该日志格式是否必须与您编写的每个字符一致?在您的情况下,似乎 JSON 翻译是最佳选择:LogClass{ getRequestAndSaveIt()} Gson.toJson(LogClass) as pseudocode
  • 未来的读者可能会从我的回答中受益(此评论中的网址)。基本上,我能够将关于这个问题的不同帖子放在一起。请在手动尝试之前考虑执行器的答案(在下面的答案中)。但是我发布的答案允许记录“400、404、500”(任何/全部),但将订单优先级设置为最低优先级(或者如果您查看代码,则在“8”之内)。 stackoverflow.com/questions/10210645/…
  • 我确实按照 spring 文档从这里登录:docs.spring.io/spring-boot/docs/current/reference/html/…

标签: java spring logging spring-rest


【解决方案1】:

注意

 @Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
...
}

方法不适用于 spring 安全过滤器链。 您必须手动添加 CommonsRequestLoggingFilter 之类的

protected void configure(HttpSecurity http) throws Exception {
         HttpSecurity filter = http
        .cors().and().addFilterBefore(new CommonsRequestLoggingFilter(), CorsFilter.class);
}

【讨论】:

    【解决方案2】:

    Logbook 库专门用于记录 HTTP 请求和响应。它使用特殊的启动库支持 Spring Boot。

    要在 Spring Boot 中启用日志记录,您只需将库添加到项目的依赖项中即可。例如假设您使用的是 Maven:

    <dependency>
        <groupId>org.zalando</groupId>
        <artifactId>logbook-spring-boot-starter</artifactId>
        <version>1.5.0</version>
    </dependency>
    

    默认情况下,日志输出如下所示:

    {
      "origin" : "local",
      "correlation" : "52e19498-890c-4f75-a06c-06ddcf20836e",
      "status" : 200,
      "headers" : {
        "X-Application-Context" : [
          "application:8088"
        ],
        "Content-Type" : [
          "application/json;charset=UTF-8"
        ],
        "Transfer-Encoding" : [
          "chunked"
        ],
        "Date" : [
          "Sun, 24 Dec 2017 13:10:45 GMT"
        ]
      },
      "body" : {
        "thekey" : "some_example"
      },
      "duration" : 105,
      "protocol" : "HTTP/1.1",
      "type" : "response"
    }
    

    但它不会输出处理请求的类名。该库确实有一些用于编写自定义记录器的接口。

    注意事项

    与此同时,该库已显着发展,当前版本为 2.4.1,请参阅https://github.com/zalando/logbook/releases。例如。默认输出格式已更改,可以进行配置、过滤等操作。

    不要忘记将日志级别设置为TRACE,否则您将看不到任何内容:

    logging:
      level:
        org.zalando.logbook: TRACE
    

    【讨论】:

    • 添加为一个最小的 Spring Boot 应用程序的依赖项并尝试运行 - 没有任何变化,在我的应用程序中根本没有日志输出。我认为这需要一些额外的依赖项或类?将其注册为过滤器似乎也没有任何作用。
    • @eis 您需要按照此处文档中的说明将其注册为过滤器。 github.com/zalando/logbook
    • Logbook 文档说:“Logbook 为 Spring Boot 用户提供了方便的自动配置。它使用合理的默认值自动设置以下所有部分。”但它不起作用。
    • @LeosLiterak 我相信您需要将logging.level.org.zalando.logbook=TRACE 添加到您的application.properties 中(如Readme 中所述)
    • 日志自动配置似乎不适用于 spring-boot v2.0.5
    【解决方案3】:

    作为suggested previouslyLogbook 几乎是完美的,但在使用 Java 模块时我确实在设置它时遇到了一些麻烦,因为 logbook-apilogbook-core 之间的拆分包。

    对于我的 Gradle + Spring Boot 项目,我需要

    build.gradle

    dependencies {
        compileOnly group: 'org.zalando', name: 'logbook-api', version: '2.4.1'
        runtimeOnly group: 'org.zalando', name: 'logbook-spring-boot-starter', version: '2.4.1'
        //...
    }
    
    

    logback-spring.xml

    <configuration>
        <!-- HTTP Requests and Responses -->
        <logger name="org.zalando.logbook" level="trace" />
    </configuration>
    

    【讨论】:

      【解决方案4】:

      自发布最初的问题以来,Actuator HTTP Trace 是否有任何发展,即有没有办法通过响应正文来丰富它?

      如何使用来自 MDC 或 Spring-Sleuth 或 Zipkin 的自定义元数据(例如 traceId 和 spanId)来丰富它?

      对我来说,Actuator HTTP Trace 在 Spring Boot 2.2.3 中也不起作用,我在这里找到了修复:https://juplo.de/actuator-httptrace-does-not-work-with-spring-boot-2-2/

      pom.xml

      <dependency>
        <groupId>org.springframework.boot
        <artifactId>spring-boot-starter-actuator
      </dependency>
      

      application.properties

      management.endpoints.web.exposure.include=httptrace
      

      修复:

      解决这个问题的简单方法是,添加一个 @Bean 类型 InMemoryHttpTraceRepository 到你的@Configuration-class:

      @Bean
      public HttpTraceRepository htttpTraceRepository()
      {
        return new InMemoryHttpTraceRepository();
      }
      

      解释:

      此问题的原因不是错误,而是在 默认配置。不幸的是,此更改未在 根据文档的部分。相反,它被埋在 Spring Boot 2.2 升级说明

      默认实现将捕获的数据存储在内存中。因此, 它会在用户不知情的情况下消耗大量内存,甚至更糟: 需要它。这在集群环境中尤其不受欢迎, 记忆是宝贵的财富。请记住:Spring Boot 是 旨在简化集群部署!

      也就是说,为什么现在默认关闭此功能并且必须 如果需要,由用户显式打开。

      【讨论】:

        【解决方案5】:

        目前 Spring Boot 具有 Actuator 功能来获取请求和响应的日志。

        但您也可以使用 Aspect(AOP) 获取日志。

        Aspect 为您提供注释,例如:@Before@AfterReturning@AfterThrowing 等。

        @Before 记录请求,@AfterReturning 记录响应,@AfterThrowing 记录错误消息, 您可能不需要所有端点的日志,因此您可以在包上应用一些过滤器。

        以下是一些示例

        请求:

        @Before("within(your.package.where.endpoints.are..*)")
            public void endpointBefore(JoinPoint p) {
                if (log.isTraceEnabled()) {
                    log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " START");
                    Object[] signatureArgs = p.getArgs();
        
        
                    ObjectMapper mapper = new ObjectMapper();
                    mapper.enable(SerializationFeature.INDENT_OUTPUT);
                    try {
        
                        if (signatureArgs[0] != null) {
                            log.trace("\nRequest object: \n" + mapper.writeValueAsString(signatureArgs[0]));
                        }
                    } catch (JsonProcessingException e) {
                    }
                }
            }
        

        这里@Before("within(your.package.where.endpoints.are..*)") 有包路径。此包中的所有端点都会生成日志。

        回复:

        @AfterReturning(value = ("within(your.package.where.endpoints.are..*)"),
                    returning = "returnValue")
            public void endpointAfterReturning(JoinPoint p, Object returnValue) {
                if (log.isTraceEnabled()) {
                    ObjectMapper mapper = new ObjectMapper();
                    mapper.enable(SerializationFeature.INDENT_OUTPUT);
                    try {
                        log.trace("\nResponse object: \n" + mapper.writeValueAsString(returnValue));
                    } catch (JsonProcessingException e) {
                        System.out.println(e.getMessage());
                    }
                    log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " END");
                }
            }
        

        这里@AfterReturning("within(your.package.where.endpoints.are..*)") 有包路径。此包中的所有端点都将生成日志。 Object returnValue 也包含响应。

        例外情况:

        @AfterThrowing(pointcut = ("within(your.package.where.endpoints.are..*)"), throwing = "e")
        public void endpointAfterThrowing(JoinPoint p, Exception e) throws DmoneyException {
            if (log.isTraceEnabled()) {
                System.out.println(e.getMessage());
        
                e.printStackTrace();
        
        
                log.error(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " " + e.getMessage());
            }
        }
        

        这里@AfterThrowing(pointcut = ("within(your.package.where.endpoints.are..*)"), throwing = "e") 有包路径。此包中的所有端点都将生成日志。 Exception e 也包含错误响应。

        这里是完整的代码:

        import com.fasterxml.jackson.core.JsonProcessingException;
        import com.fasterxml.jackson.databind.ObjectMapper;
        import com.fasterxml.jackson.databind.SerializationFeature;
        import org.apache.log4j.Logger;
        import org.aspectj.lang.JoinPoint;
        import org.aspectj.lang.annotation.AfterReturning;
        import org.aspectj.lang.annotation.AfterThrowing;
        import org.aspectj.lang.annotation.Aspect;
        import org.aspectj.lang.annotation.Before;
        import org.springframework.core.annotation.Order;
        import org.springframework.stereotype.Component;
        
        @Aspect
        @Order(1)
        @Component
        @ConditionalOnExpression("${endpoint.aspect.enabled:true}")
        public class EndpointAspect {
            static Logger log = Logger.getLogger(EndpointAspect.class);
        
            @Before("within(your.package.where.is.endpoint..*)")
            public void endpointBefore(JoinPoint p) {
                if (log.isTraceEnabled()) {
                    log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " START");
                    Object[] signatureArgs = p.getArgs();
        
        
                    ObjectMapper mapper = new ObjectMapper();
                    mapper.enable(SerializationFeature.INDENT_OUTPUT);
                    try {
        
                        if (signatureArgs[0] != null) {
                            log.trace("\nRequest object: \n" + mapper.writeValueAsString(signatureArgs[0]));
                        }
                    } catch (JsonProcessingException e) {
                    }
                }
            }
        
            @AfterReturning(value = ("within(your.package.where.is.endpoint..*)"),
                    returning = "returnValue")
            public void endpointAfterReturning(JoinPoint p, Object returnValue) {
                if (log.isTraceEnabled()) {
                    ObjectMapper mapper = new ObjectMapper();
                    mapper.enable(SerializationFeature.INDENT_OUTPUT);
                    try {
                        log.trace("\nResponse object: \n" + mapper.writeValueAsString(returnValue));
                    } catch (JsonProcessingException e) {
                        System.out.println(e.getMessage());
                    }
                    log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " END");
                }
            }
        
        
            @AfterThrowing(pointcut = ("within(your.package.where.is.endpoint..*)"), throwing = "e")
            public void endpointAfterThrowing(JoinPoint p, Exception e) throws Exception {
                if (log.isTraceEnabled()) {
                    System.out.println(e.getMessage());
        
                    e.printStackTrace();
        
        
                    log.error(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " " + e.getMessage());
                }
            }
        }
        

        在这里,您可以使用@ConditionalOnExpression("${endpoint.aspect.enabled:true}") 启用/禁用日志。只需将endpoint.aspect.enabled:true 添加到application.property 并控制日志

        有关 AOP 的更多信息,请访问此处:

        Spring docs about AOP

        Sample article about AOP

        【讨论】:

        • new ObjectMapper() 很贵,最好共享一个映射器
        • 是的,当然。这是演示代码。在生产中,我们必须遵循最佳实践。
        【解决方案6】:

        如果你配置了 Spring boot Config 服务器,那么只需为类启用调试记录器:

        Http11InputBuffer.Http11InputBuffer.java

        调试将记录每个请求的所有请求和响应

        【讨论】:

        • 将“logging.level.org.apache.coyote.http11=DEBUG”添加到 application.properties 记录请求和响应,但使用 restTemplate 对其他后端服务发出的任何请求都不会以相同的方式记录。
        【解决方案7】:

        不要写任何拦截器、过滤器、组件、方面等,这是一个很常见的问题,已经解决了很多次。

        Spring Boot 有一个名为 Actuator 的模块,它提供开箱即用的 HTTP 请求日志记录。有一个端点映射到 /trace (SB1.x) 或 /actuator/httptrace (SB2.0+),它将显示最后 100 个 HTTP 请求。您可以对其进行自定义以记录每个请求,或写入数据库。

        要获得您想要的端点,您需要 spring-boot-starter-actuator 依赖项,还需要将您正在寻找的端点“列入白名单”,并可能为其设置或禁用安全性。

        另外,这个应用程序将在哪里运行?你会使用 PaaS 吗?托管服务提供商(例如 Heroku)提供请求日志记录作为其服务的一部分,您无需进行任何任何编码。

        【讨论】:

        • 这不能用于调试:未经身份验证的请求(例如使用 Spring Security)不会被记录。
        • 实际上 Actuator 没有任何特定的组件来启用 http 日志记录。 /trace - 仅显示最后 N 个请求。
        • @ike_love,如何配置执行器以便将请求(也是 POST 正文)记录到文件中?
        • Trace 不会为您记录请求和响应正文....除这些之外的所有其他内容(标头等)。
        • 如果要body,这个没用,请提一下。
        【解决方案8】:

        为了记录所有带有输入参数和正文的请求,我们可以使用过滤器拦截器。但是在使用过滤器或拦截器时,我们不能多次打印请求正文。 更好的方法是我们可以使用 spring-AOP。通过使用它,我们可以将日志记录机制与应用程序分离。 AOP 可用于记录应用程序中每个方法输入和输出

        我的解决办法是:

         import org.aspectj.lang.ProceedingJoinPoint;
         import org.aspectj.lang.annotation.Around;
         import org.aspectj.lang.annotation.Aspect;
         import org.aspectj.lang.annotation.Pointcut;
         import org.aspectj.lang.reflect.CodeSignature;
         import org.slf4j.Logger;
         import org.slf4j.LoggerFactory;
         import org.springframework.stereotype.Component;
         import com.fasterxml.jackson.databind.ObjectMapper;
         @Aspect
         @Component
        public class LoggingAdvice {
        private static final Logger logger = 
        LoggerFactory.getLogger(LoggingAdvice.class);
        
        //here we can provide any methodName, packageName, className 
        @Pointcut(value = "execution(* com.package.name.*.*.*(..) )")
        public void myPointcut() {
        
        }
        
        @Around("myPointcut()")
        public Object applicationLogger(ProceedingJoinPoint pjt) throws Throwable {
            ObjectMapper mapper = new ObjectMapper();
            String methodName = pjt.getSignature().getName();
            String className = pjt.getTarget().getClass().toString();
            String inputParams = this.getInputArgs(pjt ,mapper);
            logger.info("method invoked from " + className + " : " + methodName + "--Request Payload::::"+inputParams);
            Object object = pjt.proceed();
            try {
                logger.info("Response Object---" + mapper.writeValueAsString(object));
            } catch (Exception e) {
            }
            return object;
        }
        
        private String getInputArgs(ProceedingJoinPoint pjt,ObjectMapper mapper) {
            Object[] array = pjt.getArgs();
            CodeSignature signature = (CodeSignature) pjt.getSignature();
        
            StringBuilder sb = new StringBuilder();
            sb.append("{");
            int i = 0;
            String[] parameterNames = signature.getParameterNames();
            int maxArgs = parameterNames.length;
            for (String name : signature.getParameterNames()) {
                sb.append("[").append(name).append(":");
                try {
                    sb.append(mapper.writeValueAsString(array[i])).append("]");
                    if(i != maxArgs -1 ) {
                        sb.append(",");
                    }
                } catch (Exception e) {
                    sb.append("],");
                }
                i++;
            }
            return sb.append("}").toString();
        }
        

        }

        【讨论】:

          【解决方案9】:

          下面粘贴的代码适用于我的测试,可以从我的 [github 项目][1] 下载,在将基于该解决方案的解决方案应用于生产项目后共享。

          @Configuration
          public class LoggingFilter extends GenericFilterBean {
          
              /**
               * It's important that you actually register your filter this way rather then just annotating it
               * as @Component as you need to be able to set for which "DispatcherType"s to enable the filter
               * (see point *1*)
               * 
               * @return
               */
              @Bean
              public FilterRegistrationBean<LoggingFilter> initFilter() {
                  FilterRegistrationBean<LoggingFilter> registrationBean = new FilterRegistrationBean<>();
                  registrationBean.setFilter(new LoggingFilter());
          
                  // *1* make sure you sett all dispatcher types if you want the filter to log upon
                  registrationBean.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
          
                  // *2* this should put your filter above any other filter
                  registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
          
                  return registrationBean;
              }
          
              @Override
              public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                      throws IOException, ServletException {
          
                  ContentCachingRequestWrapper wreq = 
                      new ContentCachingRequestWrapper(
                          (HttpServletRequest) request);
          
                  ContentCachingResponseWrapper wres = 
                      new ContentCachingResponseWrapper(
                          (HttpServletResponse) response);
          
                  try {
          
                      // let it be ...
                      chain.doFilter(wreq, wres);
          
                      // makes sure that the input is read (e.g. in 404 it may not be)
                      while (wreq.getInputStream().read() >= 0);
          
                      System.out.printf("=== REQUEST%n%s%n=== end request%n",
                              new String(wreq.getContentAsByteArray()));
          
                      // Do whatever logging you wish here, in this case I'm writing request 
                      // and response to system out which is probably not what you wish to do
                      System.out.printf("=== RESPONSE%n%s%n=== end response%n",
                              new String(wres.getContentAsByteArray()));
          
                      // this is specific of the "ContentCachingResponseWrapper" we are relying on, 
                      // make sure you call it after you read the content from the response
                      wres.copyBodyToResponse();
          
                      // One more point, in case of redirect this will be called twice! beware to handle that
                      // somewhat
          
                  } catch (Throwable t) {
                      // Do whatever logging you whish here, too
                      // here you should also be logging the error!!!
                      throw t;
                  }
          
              }
          }
          

          【讨论】:

            【解决方案10】:

            Spring 已经提供了一个过滤器来完成这项工作。将以下 bean 添加到您的配置中

            @Bean
            public CommonsRequestLoggingFilter requestLoggingFilter() {
                CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
                loggingFilter.setIncludeClientInfo(true);
                loggingFilter.setIncludeQueryString(true);
                loggingFilter.setIncludePayload(true);
                loggingFilter.setMaxPayloadLength(64000);
                return loggingFilter;
            }
            

            不要忘记将org.springframework.web.filter.CommonsRequestLoggingFilter 的日志级别更改为DEBUG

            【讨论】:

            • 请注意,它记录响应,只记录请求。
            • 只有请求。如何使用 CommonsRequestLoggingFilter 记录响应正文?
            • 这也不记录异常
            • 如果您有较大的 JSON 正文,请将有效负载长度设置为较大的数字以记录整个请求正文。 loggingFilter.setMaxPayloadLength(100000);
            • 为什么会两次记录请求,即在处理请求之前和之后。唯一的区别是请求后日志包含有效负载。
            【解决方案11】:

            这是我的解决方案(Spring 2.0.x)

            添加maven依赖:

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
            

            编辑 application.properties 并添加以下行:

            management.endpoints.web.exposure.include=* 
            

            一旦你的 spring boot 应用程序启动,你可以通过调用这个 url 来跟踪最近的 100 个 http 请求: http://localhost:8070/actuator/httptrace

            【讨论】:

            【解决方案12】:

            具体答案请参考以下链接 https://gist.github.com/int128/e47217bebdb4c402b2ffa7cc199307ba

            对上述解决方案进行了一些更改,如果记录器级别为信息,请求和响应也将登录到控制台和文件中。我们可以在控制台或文件中打印。

            @Component
            public class LoggingFilter extends OncePerRequestFilter {
            
            private static final List<MediaType> VISIBLE_TYPES = Arrays.asList(
                    MediaType.valueOf("text/*"),
                    MediaType.APPLICATION_FORM_URLENCODED,
                    MediaType.APPLICATION_JSON,
                    MediaType.APPLICATION_XML,
                    MediaType.valueOf("application/*+json"),
                    MediaType.valueOf("application/*+xml"),
                    MediaType.MULTIPART_FORM_DATA
                    );
            Logger log = LoggerFactory.getLogger(ReqAndResLoggingFilter.class);
            private static final Path path = Paths.get("/home/ramesh/loggerReq.txt");
            private static BufferedWriter writer = null;
            @Override
            protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
                try {
                    writer = Files.newBufferedWriter(path, Charset.forName("UTF-8"));
                if (isAsyncDispatch(request)) {
                    filterChain.doFilter(request, response);
                } else {
                    doFilterWrapped(wrapRequest(request), wrapResponse(response), filterChain);
                }
                }finally {
                    writer.close();
                }
            }
            
            protected void doFilterWrapped(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response, FilterChain filterChain) throws ServletException, IOException {
                try {
                    beforeRequest(request, response);
                    filterChain.doFilter(request, response);
                }
                finally {
                    afterRequest(request, response);
                    response.copyBodyToResponse();
                }
            }
            
            protected void beforeRequest(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) throws IOException {
                if (log.isInfoEnabled()) {
                    logRequestHeader(request, request.getRemoteAddr() + "|>");
                }
            }
            
            protected void afterRequest(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) throws IOException {
                if (log.isInfoEnabled()) {
                    logRequestBody(request, request.getRemoteAddr() + "|>");
                    logResponse(response, request.getRemoteAddr() + "|<");
                }
            }
            
            private void logRequestHeader(ContentCachingRequestWrapper request, String prefix) throws IOException {
                String queryString = request.getQueryString();
                if (queryString == null) {
                    printLines(prefix,request.getMethod(),request.getRequestURI());
                    log.info("{} {} {}", prefix, request.getMethod(), request.getRequestURI());
                } else {
                    printLines(prefix,request.getMethod(),request.getRequestURI(),queryString);
                    log.info("{} {} {}?{}", prefix, request.getMethod(), request.getRequestURI(), queryString);
                }
                Collections.list(request.getHeaderNames()).forEach(headerName ->
                Collections.list(request.getHeaders(headerName)).forEach(headerValue ->
                log.info("{} {}: {}", prefix, headerName, headerValue)));
                printLines(prefix);
                printLines(RequestContextHolder.currentRequestAttributes().getSessionId());
                log.info("{}", prefix);
            
                log.info(" Session ID: ", RequestContextHolder.currentRequestAttributes().getSessionId());
            }
            
            private void printLines(String ...args) throws IOException {
            
                try {
                for(String varArgs:args) {
                        writer.write(varArgs);
                        writer.newLine();
                }
                    }catch(IOException ex){
                        ex.printStackTrace();
                }
            
            }
            
            private void logRequestBody(ContentCachingRequestWrapper request, String prefix) {
                byte[] content = request.getContentAsByteArray();
                if (content.length > 0) {
                    logContent(content, request.getContentType(), request.getCharacterEncoding(), prefix);
                }
            }
            
            private void logResponse(ContentCachingResponseWrapper response, String prefix) throws IOException {
                int status = response.getStatus();
                printLines(prefix, String.valueOf(status), HttpStatus.valueOf(status).getReasonPhrase());
                log.info("{} {} {}", prefix, status, HttpStatus.valueOf(status).getReasonPhrase());
                response.getHeaderNames().forEach(headerName ->
                response.getHeaders(headerName).forEach(headerValue ->
                log.info("{} {}: {}", prefix, headerName, headerValue)));
                printLines(prefix);
                log.info("{}", prefix);
                byte[] content = response.getContentAsByteArray();
                if (content.length > 0) {
                    logContent(content, response.getContentType(), response.getCharacterEncoding(), prefix);
                }
            }
            
            private void logContent(byte[] content, String contentType, String contentEncoding, String prefix) {
                MediaType mediaType = MediaType.valueOf(contentType);
                boolean visible = VISIBLE_TYPES.stream().anyMatch(visibleType -> visibleType.includes(mediaType));
                if (visible) {
                    try {
                        String contentString = new String(content, contentEncoding);
                        Stream.of(contentString.split("\r\n|\r|\n")).forEach(line -> {
                            try {
                                printLines(line);
                            } catch (IOException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                            }
                        });
            //              log.info("{} {}", prefix, line));
                    } catch (UnsupportedEncodingException e) {
                        log.info("{} [{} bytes content]", prefix, content.length);
                    }
                } else {
            
                    log.info("{} [{} bytes content]", prefix, content.length);
                }
            }
            
            private static ContentCachingRequestWrapper wrapRequest(HttpServletRequest request) {
                if (request instanceof ContentCachingRequestWrapper) {
                    return (ContentCachingRequestWrapper) request;
                } else {
                    return new ContentCachingRequestWrapper(request);
                }
            }
            
            private static ContentCachingResponseWrapper wrapResponse(HttpServletResponse response) {
                if (response instanceof ContentCachingResponseWrapper) {
                    return (ContentCachingResponseWrapper) response;
                } else {
                    return new ContentCachingResponseWrapper(response);
                }
            }
            } 
            

            在文件中输出:

            127.0.0.1|>
            POST
            /createUser
            127.0.0.1|>
            session Id:C0793464532E7F0C7154913CBA018B2B
            Request:
            {
              "name": "asdasdas",
              "birthDate": "2018-06-21T17:11:15.679+0000"
            }
            127.0.0.1|<
            200
            OK
            127.0.0.1|<
            Response:
            {"name":"asdasdas","birthDate":"2018-06-21T17:11:15.679+0000","id":4}
            

            【讨论】:

            • 很好的答案,唯一的建议是将所有输出收集到缓冲区并登录单个语句。
            • 如果 api 返回巨大的文件流怎么办。如何处理?
            【解决方案13】:

            如果您只看到请求负载的一部分,则需要调用 setMaxPayloadLength 函数,因为它默认在请求正文中仅显示 50 个字符。此外,如果您不想记录身份验证标头,将 setIncludeHeaders 设置为 false 是个好主意!

            @Bean
            public CommonsRequestLoggingFilter requestLoggingFilter() {
                CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
                loggingFilter.setIncludeClientInfo(false);
                loggingFilter.setIncludeQueryString(false);
                loggingFilter.setIncludePayload(true);
                loggingFilter.setIncludeHeaders(false);
                loggingFilter.setMaxPayloadLength(500);
                return loggingFilter;
            }
            

            【讨论】:

            • 我正在尝试在 spring mvc 中使用它,但它对我不起作用,除了注册此 bean 和添加记录器之外,还需要任何其他设置?
            【解决方案14】:

            您还可以配置自定义 Spring 拦截器 HandlerInterceptorAdapter 以简化 pre-only/post-only 拦截器的实现:

            @Component
            public class CustomHttpInterceptor extends HandlerInterceptorAdapter {
            
                @Override
                public boolean preHandle (final HttpServletRequest request, final HttpServletResponse response,
                        final Object handler)
                        throws Exception {
            
                    // Logs here
            
                    return super.preHandle(request, response, handler);
                }
            
                @Override
                public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
                        final Object handler, final Exception ex) {
                    // Logs here
                }
            }
            

            然后,您注册任意数量的拦截器:

            @Configuration
            public class WebMvcConfig implements WebMvcConfigurer {
            
                @Autowired
                CustomHttpInterceptor customHttpInterceptor;
            
                @Override
                public void addInterceptors(InterceptorRegistry registry) {
                    registry.addInterceptor(customHttpInterceptor).addPathPatterns("/endpoints");
                }
            
            }
            

            注意: just like stated by @Robert,您需要注意您的应用程序正在使用的HttpServletRequestHttpServletResponse 的具体实现。

            例如,对于使用 ShallowEtagHeaderFilter 的应用,响应实现将是 ContentCachingResponseWrapper,因此您将拥有:

            @Component
            public class CustomHttpInterceptor extends HandlerInterceptorAdapter {
            
                private static final Logger LOGGER = LoggerFactory.getLogger(CustomHttpInterceptor.class);
            
                private static final int MAX_PAYLOAD_LENGTH = 1000;
            
                @Override
                public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
                        final Object handler, final Exception ex) {
                    final byte[] contentAsByteArray = ((ContentCachingResponseWrapper) response).getContentAsByteArray();
            
                    LOGGER.info("Request body:\n" + getContentAsString(contentAsByteArray, response.getCharacterEncoding()));
                }
            
                private String getContentAsString(byte[] buf, String charsetName) {
                    if (buf == null || buf.length == 0) {
                        return "";
                    }
            
                    try {
                        int length = Math.min(buf.length, MAX_PAYLOAD_LENGTH);
            
                        return new String(buf, 0, length, charsetName);
                    } catch (UnsupportedEncodingException ex) {
                        return "Unsupported Encoding";
                    }
                }
            
            }
            

            【讨论】:

              【解决方案15】:

              为了只记录导致 400 的请求:

              import javax.servlet.FilterChain;
              import javax.servlet.ServletException;
              import javax.servlet.http.HttpServletRequest;
              import javax.servlet.http.HttpServletResponse;
              import javax.servlet.http.HttpSession;
              
              import org.apache.commons.io.FileUtils;
              import org.springframework.http.HttpStatus;
              import org.springframework.http.server.ServletServerHttpRequest;
              import org.springframework.stereotype.Component;
              import org.springframework.util.StringUtils;
              import org.springframework.web.filter.AbstractRequestLoggingFilter;
              import org.springframework.web.filter.OncePerRequestFilter;
              import org.springframework.web.util.ContentCachingRequestWrapper;
              import org.springframework.web.util.WebUtils;
              
              /**
               * Implementation is partially copied from {@link AbstractRequestLoggingFilter} and modified to output request information only if request resulted in 400.
               * Unfortunately {@link AbstractRequestLoggingFilter} is not smart enough to expose {@link HttpServletResponse} value in afterRequest() method.
               */
              @Component
              public class RequestLoggingFilter extends OncePerRequestFilter {
              
                  public static final String DEFAULT_AFTER_MESSAGE_PREFIX = "After request [";
              
                  public static final String DEFAULT_AFTER_MESSAGE_SUFFIX = "]";
              
                  private final boolean includeQueryString = true;
                  private final boolean includeClientInfo = true;
                  private final boolean includeHeaders = true;
                  private final boolean includePayload = true;
              
                  private final int maxPayloadLength = (int) (2 * FileUtils.ONE_MB);
              
                  private final String afterMessagePrefix = DEFAULT_AFTER_MESSAGE_PREFIX;
              
                  private final String afterMessageSuffix = DEFAULT_AFTER_MESSAGE_SUFFIX;
              
                  /**
                   * The default value is "false" so that the filter may log a "before" message
                   * at the start of request processing and an "after" message at the end from
                   * when the last asynchronously dispatched thread is exiting.
                   */
                  @Override
                  protected boolean shouldNotFilterAsyncDispatch() {
                      return false;
                  }
              
                  @Override
                  protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
                          throws ServletException, IOException {
              
                      final boolean isFirstRequest = !isAsyncDispatch(request);
                      HttpServletRequest requestToUse = request;
              
                      if (includePayload && isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
                          requestToUse = new ContentCachingRequestWrapper(request, maxPayloadLength);
                      }
              
                      final boolean shouldLog = shouldLog(requestToUse);
              
                      try {
                          filterChain.doFilter(requestToUse, response);
                      } finally {
                          if (shouldLog && !isAsyncStarted(requestToUse)) {
                              afterRequest(requestToUse, response, getAfterMessage(requestToUse));
                          }
                      }
                  }
              
                  private String getAfterMessage(final HttpServletRequest request) {
                      return createMessage(request, this.afterMessagePrefix, this.afterMessageSuffix);
                  }
              
                  private String createMessage(final HttpServletRequest request, final String prefix, final String suffix) {
                      final StringBuilder msg = new StringBuilder();
                      msg.append(prefix);
                      msg.append("uri=").append(request.getRequestURI());
              
                      if (includeQueryString) {
                          final String queryString = request.getQueryString();
                          if (queryString != null) {
                              msg.append('?').append(queryString);
                          }
                      }
              
                      if (includeClientInfo) {
                          final String client = request.getRemoteAddr();
                          if (StringUtils.hasLength(client)) {
                              msg.append(";client=").append(client);
                          }
                          final HttpSession session = request.getSession(false);
                          if (session != null) {
                              msg.append(";session=").append(session.getId());
                          }
                          final String user = request.getRemoteUser();
                          if (user != null) {
                              msg.append(";user=").append(user);
                          }
                      }
              
                      if (includeHeaders) {
                          msg.append(";headers=").append(new ServletServerHttpRequest(request).getHeaders());
                      }
              
                      if (includeHeaders) {
                          final ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
                          if (wrapper != null) {
                              final byte[] buf = wrapper.getContentAsByteArray();
                              if (buf.length > 0) {
                                  final int length = Math.min(buf.length, maxPayloadLength);
                                  String payload;
                                  try {
                                      payload = new String(buf, 0, length, wrapper.getCharacterEncoding());
                                  } catch (final UnsupportedEncodingException ex) {
                                      payload = "[unknown]";
                                  }
                                  msg.append(";payload=").append(payload);
                              }
                          }
                      }
                      msg.append(suffix);
                      return msg.toString();
                  }
              
                  private boolean shouldLog(final HttpServletRequest request) {
                      return true;
                  }
              
                  private void afterRequest(final HttpServletRequest request, final HttpServletResponse response, final String message) {
                      if (response.getStatus() == HttpStatus.BAD_REQUEST.value()) {
                          logger.warn(message);
                      }
                  }
              
              }
              

              【讨论】:

                【解决方案16】:

                如果有人在这里仍然需要它,可以使用 Spring HttpTrace Actuator 进行简单的实现。但正如他们告诉上层的,它不会记录尸体。

                import lombok.extern.slf4j.Slf4j;
                import org.apache.commons.lang3.builder.ToStringBuilder;
                import org.springframework.boot.actuate.trace.http.HttpTrace;
                import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository;
                import org.springframework.stereotype.Repository;
                
                @Slf4j
                @Repository
                public class LoggingInMemoryHttpTraceRepository extends InMemoryHttpTraceRepository {
                    public void add(HttpTrace trace) {
                        super.add(trace);
                        log.info("Trace:" + ToStringBuilder.reflectionToString(trace));
                        log.info("Request:" + ToStringBuilder.reflectionToString(trace.getRequest()));
                        log.info("Response:" + ToStringBuilder.reflectionToString(trace.getResponse()));
                    }
                }
                

                【讨论】:

                  【解决方案17】:

                  如果您在启动应用程序中使用 Tomcat,那么 org.apache.catalina.filters.RequestDumperFilter 在您的类路径中。 (但它不会为您提供“单个地方的例外情况”)。

                  【讨论】:

                    【解决方案18】:

                    我在application.properties 中定义了日志级别以打印请求/响应,日志文件中的方法 url

                    logging.level.org.springframework.web=DEBUG
                    logging.level.org.hibernate.SQL=INFO
                    logging.file=D:/log/myapp.log
                    

                    我使用过 Spring Boot。

                    【讨论】:

                    • 是的,你是对的 - 这是获取请求记录到具有所有其他结果的同一日志文件的有效答案。但是,@moreo 要求将 GET、POST 等记录到单独的文件中(据我了解)
                    • 如果您希望将标头包含在日志中,则应在 application.properties 文件中添加:“spring.http.log-request-details=true”。
                    【解决方案19】:

                    Actuators 添加到基于spring boot 的应用程序后,您将拥有/trace 端点,其中包含最新的请求信息。此端点基于TraceRepository 工作,默认实现为InMemoryTraceRepository,可保存最后100 次调用。您可以通过自己实现此接口来更改它,并将其作为 Spring bean 提供。例如,要记录所有要记录的请求(并且仍然使用默认实现作为在/trace 端点上提供信息的基本存储)我正在使用这种实现:

                    import org.slf4j.Logger;
                    import org.slf4j.LoggerFactory;
                    import org.springframework.boot.actuate.trace.InMemoryTraceRepository;
                    import org.springframework.boot.actuate.trace.Trace;
                    import org.springframework.boot.actuate.trace.TraceRepository;
                    import org.springframework.stereotype.Component;
                    
                    import java.util.List;
                    import java.util.Map;
                    
                    
                    @Component
                    public class LoggingTraceRepository implements TraceRepository {
                    
                      private static final Logger LOG = LoggerFactory.getLogger(LoggingTraceRepository.class);
                      private final TraceRepository delegate = new InMemoryTraceRepository();
                    
                      @Override
                      public List<Trace> findAll() {
                        return delegate.findAll();
                      }
                    
                      @Override
                      public void add(Map<String, Object> traceInfo) {
                        LOG.info(traceInfo.toString());
                        this.delegate.add(traceInfo);
                      }
                    }
                    

                    traceInfo 映射包含有关这种形式的请求和响应的基本信息: {method=GET, path=/api/hello/John, headers={request={host=localhost:8080, user-agent=curl/7.51.0, accept=*/*}, response={X-Application-Context=application, Content-Type=text/plain;charset=UTF-8, Content-Length=10, Date=Wed, 29 Mar 2017 20:41:21 GMT, status=200}}}。这里没有响应内容。

                    编辑!记录 POST 数据

                    您可以通过覆盖 WebRequestTraceFilter 来访问 POST 数据,但不要认为这是一个好主意(例如,所有上传的文件内容都会进入日志) 这是示例代码,但不要使用它:

                    package info.fingo.nuntius.acuate.trace;
                    
                    import org.apache.commons.io.IOUtils;
                    import org.springframework.boot.actuate.trace.TraceProperties;
                    import org.springframework.boot.actuate.trace.TraceRepository;
                    import org.springframework.boot.actuate.trace.WebRequestTraceFilter;
                    import org.springframework.stereotype.Component;
                    
                    import javax.servlet.ServletException;
                    import javax.servlet.http.HttpServletRequest;
                    import java.io.IOException;
                    import java.nio.charset.Charset;
                    import java.util.LinkedHashMap;
                    import java.util.Map;
                    
                    @Component
                    public class CustomWebTraceFilter extends WebRequestTraceFilter {
                    
                      public CustomWebTraceFilter(TraceRepository repository, TraceProperties properties) {
                        super(repository, properties);
                    }
                    
                      @Override
                      protected Map<String, Object> getTrace(HttpServletRequest request) {
                        Map<String, Object> trace = super.getTrace(request);
                        String multipartHeader = request.getHeader("content-type");
                        if (multipartHeader != null && multipartHeader.startsWith("multipart/form-data")) {
                            Map<String, Object> parts = new LinkedHashMap<>();
                            try {
                                request.getParts().forEach(
                                        part -> {
                                            try {
                                                parts.put(part.getName(), IOUtils.toString(part.getInputStream(), Charset.forName("UTF-8")));
                                            } catch (IOException e) {
                                                e.printStackTrace();
                                            }
                                        }
                                );
                            } catch (IOException | ServletException e) {
                                e.printStackTrace();
                            }
                            if (!parts.isEmpty()) {
                                trace.put("multipart-content-map", parts);
                            }
                        }
                        return trace;
                      }
                    }
                    

                    【讨论】:

                    • POST 正文呢?
                    • @dart 我已经为你添加了示例
                    • 我正在做这样的事情,但问题是TraceRepository 没有响应正文,我们如何访问它?
                    • @AmirPashazadeh 您必须覆盖 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 但我不确定何时执行此过滤器 - 可能处于请求阶段,因此响应正文不会在那里准备好。
                    • @Kekar 从 2.0 开始有 HttpTraceRepository(而不是 TraceRepository)
                    【解决方案20】:

                    这是我在春季数据休息中的做法 通过使用 org.springframework.web.util.ContentCachingRequestWrapperorg.springframework.web.util.ContentCachingResponseWrapper

                    /**
                     * Doogies very cool HTTP request logging
                     *
                     * There is also {@link org.springframework.web.filter.CommonsRequestLoggingFilter}  but it cannot log request method
                     * And it cannot easily be extended.
                     *
                     * https://mdeinum.wordpress.com/2015/07/01/spring-framework-hidden-gems/
                     * http://stackoverflow.com/questions/8933054/how-to-read-and-copy-the-http-servlet-response-output-stream-content-for-logging
                     */
                    public class DoogiesRequestLogger extends OncePerRequestFilter {
                    
                      private boolean includeResponsePayload = true;
                      private int maxPayloadLength = 1000;
                    
                      private String getContentAsString(byte[] buf, int maxLength, String charsetName) {
                        if (buf == null || buf.length == 0) return "";
                        int length = Math.min(buf.length, this.maxPayloadLength);
                        try {
                          return new String(buf, 0, length, charsetName);
                        } catch (UnsupportedEncodingException ex) {
                          return "Unsupported Encoding";
                        }
                      }
                    
                      /**
                       * Log each request and respponse with full Request URI, content payload and duration of the request in ms.
                       * @param request the request
                       * @param response the response
                       * @param filterChain chain of filters
                       * @throws ServletException
                       * @throws IOException
                       */
                      @Override
                      protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
                    
                        long startTime = System.currentTimeMillis();
                        StringBuffer reqInfo = new StringBuffer()
                         .append("[")
                         .append(startTime % 10000)  // request ID
                         .append("] ")
                         .append(request.getMethod())
                         .append(" ")
                         .append(request.getRequestURL());
                    
                        String queryString = request.getQueryString();
                        if (queryString != null) {
                          reqInfo.append("?").append(queryString);
                        }
                    
                        if (request.getAuthType() != null) {
                          reqInfo.append(", authType=")
                            .append(request.getAuthType());
                        }
                        if (request.getUserPrincipal() != null) {
                          reqInfo.append(", principalName=")
                            .append(request.getUserPrincipal().getName());
                        }
                    
                        this.logger.debug("=> " + reqInfo);
                    
                        // ========= Log request and response payload ("body") ========
                        // We CANNOT simply read the request payload here, because then the InputStream would be consumed and cannot be read again by the actual processing/server.
                        //    String reqBody = DoogiesUtil._stream2String(request.getInputStream());   // THIS WOULD NOT WORK!
                        // So we need to apply some stronger magic here :-)
                        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
                        ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
                    
                        filterChain.doFilter(wrappedRequest, wrappedResponse);     // ======== This performs the actual request!
                        long duration = System.currentTimeMillis() - startTime;
                    
                        // I can only log the request's body AFTER the request has been made and ContentCachingRequestWrapper did its work.
                        String requestBody = this.getContentAsString(wrappedRequest.getContentAsByteArray(), this.maxPayloadLength, request.getCharacterEncoding());
                        if (requestBody.length() > 0) {
                          this.logger.debug("   Request body:\n" +requestBody);
                        }
                    
                        this.logger.debug("<= " + reqInfo + ": returned status=" + response.getStatus() + " in "+duration + "ms");
                        if (includeResponsePayload) {
                          byte[] buf = wrappedResponse.getContentAsByteArray();
                          this.logger.debug("   Response body:\n"+getContentAsString(buf, this.maxPayloadLength, response.getCharacterEncoding()));
                        }
                    
                        wrappedResponse.copyBodyToResponse();  // IMPORTANT: copy content of response back into original response
                    
                      }
                    
                    
                    }
                    

                    【讨论】:

                      【解决方案21】:

                      @hahn's answer 需要进行一些修改才能为我工作,但它是迄今为止我能得到的最可定制的东西。

                      它对我不起作用,可能是因为我也有一个 HandlerInterceptorAdapter[??] 但在那个版本中我一直收到来自服务器的错误响应。这是我对它的修改。

                      public class LoggableDispatcherServlet extends DispatcherServlet {
                      
                          private final Log logger = LogFactory.getLog(getClass());
                      
                          @Override
                          protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
                      
                              long startTime = System.currentTimeMillis();
                              try {
                                  super.doDispatch(request, response);
                              } finally {
                                  log(new ContentCachingRequestWrapper(request), new ContentCachingResponseWrapper(response),
                                          System.currentTimeMillis() - startTime);
                              }
                          }
                      
                          private void log(HttpServletRequest requestToCache, HttpServletResponse responseToCache, long timeTaken) {
                              int status = responseToCache.getStatus();
                              JsonObject jsonObject = new JsonObject();
                              jsonObject.addProperty("httpStatus", status);
                              jsonObject.addProperty("path", requestToCache.getRequestURI());
                              jsonObject.addProperty("httpMethod", requestToCache.getMethod());
                              jsonObject.addProperty("timeTakenMs", timeTaken);
                              jsonObject.addProperty("clientIP", requestToCache.getRemoteAddr());
                              if (status > 299) {
                                  String requestBody = null;
                                  try {
                                      requestBody = requestToCache.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
                                  } catch (IOException e) {
                                      e.printStackTrace();
                                  }
                                  jsonObject.addProperty("requestBody", requestBody);
                                  jsonObject.addProperty("requestParams", requestToCache.getQueryString());
                                  jsonObject.addProperty("tokenExpiringHeader",
                                          responseToCache.getHeader(ResponseHeaderModifierInterceptor.HEADER_TOKEN_EXPIRING));
                              }
                              logger.info(jsonObject);
                          }
                      }
                      

                      【讨论】:

                      • 你的应用是打包成war还是jar?我不断收到错误 java.io.FileNotFoundException:无法打开 ServletContext 资源 [/WEB-INF/loggingDispatcherServlet-servlet.xml]
                      【解决方案22】:

                      如果不需要记录已执行的 java 方法,您可以使用 javax.servlet.Filter

                      但根据此要求,您必须访问存储在 handlerMappingDispatcherServlet 中的信息。也就是说,您可以覆盖 DispatcherServlet 来完成请求/响应对的记录。

                      以下是可以根据您的需要进一步增强和采用的想法示例。

                      public class LoggableDispatcherServlet extends DispatcherServlet {
                      
                          private final Log logger = LogFactory.getLog(getClass());
                      
                          @Override
                          protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
                              if (!(request instanceof ContentCachingRequestWrapper)) {
                                  request = new ContentCachingRequestWrapper(request);
                              }
                              if (!(response instanceof ContentCachingResponseWrapper)) {
                                  response = new ContentCachingResponseWrapper(response);
                              }
                              HandlerExecutionChain handler = getHandler(request);
                      
                              try {
                                  super.doDispatch(request, response);
                              } finally {
                                  log(request, response, handler);
                                  updateResponse(response);
                              }
                          }
                      
                          private void log(HttpServletRequest requestToCache, HttpServletResponse responseToCache, HandlerExecutionChain handler) {
                              LogMessage log = new LogMessage();
                              log.setHttpStatus(responseToCache.getStatus());
                              log.setHttpMethod(requestToCache.getMethod());
                              log.setPath(requestToCache.getRequestURI());
                              log.setClientIp(requestToCache.getRemoteAddr());
                              log.setJavaMethod(handler.toString());
                              log.setResponse(getResponsePayload(responseToCache));
                              logger.info(log);
                          }
                      
                          private String getResponsePayload(HttpServletResponse response) {
                              ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
                              if (wrapper != null) {
                      
                                  byte[] buf = wrapper.getContentAsByteArray();
                                  if (buf.length > 0) {
                                      int length = Math.min(buf.length, 5120);
                                      try {
                                          return new String(buf, 0, length, wrapper.getCharacterEncoding());
                                      }
                                      catch (UnsupportedEncodingException ex) {
                                          // NOOP
                                      }
                                  }
                              }
                              return "[unknown]";
                          }
                      
                          private void updateResponse(HttpServletResponse response) throws IOException {
                              ContentCachingResponseWrapper responseWrapper =
                                  WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
                              responseWrapper.copyBodyToResponse();
                          }
                      
                      }
                      

                      HandlerExecutionChain - 包含有关请求处理程序的信息。

                      然后您可以按如下方式注册此调度程序:

                          @Bean
                          public ServletRegistrationBean dispatcherRegistration() {
                              return new ServletRegistrationBean(dispatcherServlet());
                          }
                      
                          @Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
                          public DispatcherServlet dispatcherServlet() {
                              return new LoggableDispatcherServlet();
                          }
                      

                      这是日志示例:

                      http http://localhost:8090/settings/test
                      i.g.m.s.s.LoggableDispatcherServlet      : LogMessage{httpStatus=500, path='/error', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)] and 3 interceptors', arguments=null, response='{"timestamp":1472475814077,"status":500,"error":"Internal Server Error","exception":"java.lang.RuntimeException","message":"org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.RuntimeException","path":"/settings/test"}'}
                      
                      http http://localhost:8090/settings/params
                      i.g.m.s.s.LoggableDispatcherServlet      : LogMessage{httpStatus=200, path='/settings/httpParams', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public x.y.z.DTO x.y.z.Controller.params()] and 3 interceptors', arguments=null, response='{}'}
                      
                      http http://localhost:8090/123
                      i.g.m.s.s.LoggableDispatcherServlet      : LogMessage{httpStatus=404, path='/error', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)] and 3 interceptors', arguments=null, response='{"timestamp":1472475840592,"status":404,"error":"Not Found","message":"Not Found","path":"/123"}'}
                      

                      更新

                      如果出现错误,Spring 会自动进行错误处理。因此,BasicErrorController#error 显示为请求处理程序。如果要保留原始请求处理程序,则可以在调用#processDispatchResult 之前在spring-webmvc-4.2.5.RELEASE-sources.jar!/org/springframework/web/servlet/DispatcherServlet.java:971 处覆盖此行为,以缓存原始处理程序。

                      【讨论】:

                      • 当响应是流并且流不支持查找时会发生什么?以上方法还能用吗?
                      • 我不关心调用的方法,只关心接收和发送的数据。过滤器似乎为我指明了正确的方向,@ike_love 的回复已将我引导至github.com/spring-projects/spring-boot/blob/master/…
                      • @TomHoward AFAIK,春季没有开箱即用的“响应记录”。因此,您可以扩展 WebRequestTraceFilter 或 AbstractRequestLoggingFilter 添加响应日志记录逻辑。
                      • 工作得很好!
                      • @hahn 为什么你为此使用 Dispatcher servlet? doFilter中的过滤器可以不添加相同的登录吗?
                      【解决方案23】:

                      如果您不介意尝试 Spring AOP,这是我一直在探索的用于日志记录的东西,它对我来说效果很好。它不会记录尚未定义的请求和失败的请求尝试。

                      添加这三个依赖项

                      spring-aop, aspectjrt, aspectjweaver
                      

                      将此添加到您的 xml 配置文件 &lt;aop:aspectj-autoproxy/&gt;

                      创建一个可以用作切入点的注解

                      @Retention(RetentionPolicy.RUNTIME)
                      @Target({ElementType.METHOD,ElementType.TYPE})
                      public @interface EnableLogging {
                      ActionType actionType();
                      }
                      

                      现在注释您要记录的所有其余 API 方法

                      @EnableLogging(actionType = ActionType.SOME_EMPLOYEE_ACTION)
                      @Override
                      public Response getEmployees(RequestDto req, final String param) {
                      ...
                      }
                      

                      现在进入 Aspect。组件扫描这个类所在的包。

                      @Aspect
                      @Component
                      public class Aspects {
                      
                      @AfterReturning(pointcut = "execution(@co.xyz.aspect.EnableLogging * *(..)) && @annotation(enableLogging) && args(reqArg, reqArg1,..)", returning = "result")
                      public void auditInfo(JoinPoint joinPoint, Object result, EnableLogging enableLogging, Object reqArg, String reqArg1) {
                      
                          HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
                                  .getRequest();
                      
                          if (result instanceof Response) {
                              Response responseObj = (Response) result;
                      
                          String requestUrl = request.getScheme() + "://" + request.getServerName()
                                      + ":" + request.getServerPort() + request.getContextPath() + request.getRequestURI()
                                      + "?" + request.getQueryString();
                      
                      String clientIp = request.getRemoteAddr();
                      String clientRequest = reqArg.toString();
                      int httpResponseStatus = responseObj.getStatus();
                      responseObj.getEntity();
                      // Can log whatever stuff from here in a single spot.
                      }
                      
                      
                      @AfterThrowing(pointcut = "execution(@co.xyz.aspect.EnableLogging * *(..)) && @annotation(enableLogging) && args(reqArg, reqArg1,..)", throwing="exception")
                      public void auditExceptionInfo(JoinPoint joinPoint, Throwable exception, EnableLogging enableLogging, Object reqArg, String reqArg1) {
                      
                          HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
                                  .getRequest();
                      
                          String requestUrl = request.getScheme() + "://" + request.getServerName()
                          + ":" + request.getServerPort() + request.getContextPath() + request.getRequestURI()
                          + "?" + request.getQueryString();
                      
                          exception.getMessage();
                          exception.getCause();
                          exception.printStackTrace();
                          exception.getLocalizedMessage();
                          // Can log whatever exceptions, requests, etc from here in a single spot.
                          }
                      }
                      

                      @AfterReturning 通知在匹配的方法执行返回时运行 正常。

                      @AfterThrowing 通知在匹配的方法执行退出时运行 抛出异常。

                      如果您想详细阅读,请阅读此内容。 http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html

                      【讨论】:

                      • 这会记录方法调用,而不是在 HTTP 级别实际接收和发送的内容。
                      • 请求正文怎么写?就我而言,它是 POST BODY。在 request.getReader 或 getInputStream 我收到流已关闭的错误。
                      【解决方案24】:

                      此代码在 Spring Boot 应用程序中适用于我 - 只需将其注册为过滤器

                          import java.io.BufferedReader;
                          import java.io.ByteArrayInputStream;
                          import java.io.ByteArrayOutputStream;
                          import java.io.IOException;
                          import java.io.InputStream;
                          import java.io.InputStreamReader;
                          import java.io.OutputStream;
                          import java.io.PrintWriter;
                          import java.util.Collection;
                          import java.util.Enumeration;
                          import java.util.HashMap;
                          import java.util.Locale;
                          import java.util.Map;
                          import javax.servlet.*;
                          import javax.servlet.http.Cookie;
                          import javax.servlet.http.HttpServletRequest;
                          import javax.servlet.http.HttpServletRequestWrapper;
                          import javax.servlet.http.HttpServletResponse;
                          import org.apache.commons.io.output.TeeOutputStream;
                          import org.slf4j.Logger;
                          import org.slf4j.LoggerFactory;
                          import org.springframework.stereotype.Component;
                      
                          @Component
                          public class HttpLoggingFilter implements Filter {
                      
                              private static final Logger log = LoggerFactory.getLogger(HttpLoggingFilter.class);
                      
                              @Override
                              public void init(FilterConfig filterConfig) throws ServletException {
                              }
                      
                              @Override
                              public void doFilter(ServletRequest request, ServletResponse response,
                                                   FilterChain chain) throws IOException, ServletException {
                                  try {
                                      HttpServletRequest httpServletRequest = (HttpServletRequest) request;
                                      HttpServletResponse httpServletResponse = (HttpServletResponse) response;
                      
                                      Map<String, String> requestMap = this
                                              .getTypesafeRequestMap(httpServletRequest);
                                      BufferedRequestWrapper bufferedRequest = new BufferedRequestWrapper(
                                              httpServletRequest);
                                      BufferedResponseWrapper bufferedResponse = new BufferedResponseWrapper(
                                              httpServletResponse);
                      
                                      final StringBuilder logMessage = new StringBuilder(
                                              "REST Request - ").append("[HTTP METHOD:")
                                              .append(httpServletRequest.getMethod())
                                              .append("] [PATH INFO:")
                                              .append(httpServletRequest.getServletPath())
                                              .append("] [REQUEST PARAMETERS:").append(requestMap)
                                              .append("] [REQUEST BODY:")
                                              .append(bufferedRequest.getRequestBody())
                                              .append("] [REMOTE ADDRESS:")
                                              .append(httpServletRequest.getRemoteAddr()).append("]");
                      
                                      chain.doFilter(bufferedRequest, bufferedResponse);
                                      logMessage.append(" [RESPONSE:")
                                              .append(bufferedResponse.getContent()).append("]");
                                      log.debug(logMessage.toString());
                                  } catch (Throwable a) {
                                      log.error(a.getMessage());
                                  }
                              }
                      
                              private Map<String, String> getTypesafeRequestMap(HttpServletRequest request) {
                                  Map<String, String> typesafeRequestMap = new HashMap<String, String>();
                                  Enumeration<?> requestParamNames = request.getParameterNames();
                                  while (requestParamNames.hasMoreElements()) {
                                      String requestParamName = (String) requestParamNames.nextElement();
                                      String requestParamValue;
                                      if (requestParamName.equalsIgnoreCase("password")) {
                                          requestParamValue = "********";
                                      } else {
                                          requestParamValue = request.getParameter(requestParamName);
                                      }
                                      typesafeRequestMap.put(requestParamName, requestParamValue);
                                  }
                                  return typesafeRequestMap;
                              }
                      
                              @Override
                              public void destroy() {
                              }
                      
                              private static final class BufferedRequestWrapper extends
                                      HttpServletRequestWrapper {
                      
                                  private ByteArrayInputStream bais = null;
                                  private ByteArrayOutputStream baos = null;
                                  private BufferedServletInputStream bsis = null;
                                  private byte[] buffer = null;
                      
                                  public BufferedRequestWrapper(HttpServletRequest req)
                                          throws IOException {
                                      super(req);
                                      // Read InputStream and store its content in a buffer.
                                      InputStream is = req.getInputStream();
                                      this.baos = new ByteArrayOutputStream();
                                      byte buf[] = new byte[1024];
                                      int read;
                                      while ((read = is.read(buf)) > 0) {
                                          this.baos.write(buf, 0, read);
                                      }
                                      this.buffer = this.baos.toByteArray();
                                  }
                      
                                  @Override
                                  public ServletInputStream getInputStream() {
                                      this.bais = new ByteArrayInputStream(this.buffer);
                                      this.bsis = new BufferedServletInputStream(this.bais);
                                      return this.bsis;
                                  }
                      
                                  String getRequestBody() throws IOException {
                                      BufferedReader reader = new BufferedReader(new InputStreamReader(
                                              this.getInputStream()));
                                      String line = null;
                                      StringBuilder inputBuffer = new StringBuilder();
                                      do {
                                          line = reader.readLine();
                                          if (null != line) {
                                              inputBuffer.append(line.trim());
                                          }
                                      } while (line != null);
                                      reader.close();
                                      return inputBuffer.toString().trim();
                                  }
                      
                              }
                      
                              private static final class BufferedServletInputStream extends
                                      ServletInputStream {
                      
                                  private ByteArrayInputStream bais;
                      
                                  public BufferedServletInputStream(ByteArrayInputStream bais) {
                                      this.bais = bais;
                                  }
                      
                                  @Override
                                  public int available() {
                                      return this.bais.available();
                                  }
                      
                                  @Override
                                  public int read() {
                                      return this.bais.read();
                                  }
                      
                                  @Override
                                  public int read(byte[] buf, int off, int len) {
                                      return this.bais.read(buf, off, len);
                                  }
                      
                                  @Override
                                  public boolean isFinished() {
                                      return false;
                                  }
                      
                                  @Override
                                  public boolean isReady() {
                                      return true;
                                  }
                      
                                  @Override
                                  public void setReadListener(ReadListener readListener) {
                      
                                  }
                              }
                      
                              public class TeeServletOutputStream extends ServletOutputStream {
                      
                                  private final TeeOutputStream targetStream;
                      
                                  public TeeServletOutputStream(OutputStream one, OutputStream two) {
                                      targetStream = new TeeOutputStream(one, two);
                                  }
                      
                                  @Override
                                  public void write(int arg0) throws IOException {
                                      this.targetStream.write(arg0);
                                  }
                      
                                  public void flush() throws IOException {
                                      super.flush();
                                      this.targetStream.flush();
                                  }
                      
                                  public void close() throws IOException {
                                      super.close();
                                      this.targetStream.close();
                                  }
                      
                                  @Override
                                  public boolean isReady() {
                                      return false;
                                  }
                      
                                  @Override
                                  public void setWriteListener(WriteListener writeListener) {
                      
                                  }
                              }
                      
                              public class BufferedResponseWrapper implements HttpServletResponse {
                      
                                  HttpServletResponse original;
                                  TeeServletOutputStream tee;
                                  ByteArrayOutputStream bos;
                      
                                  public BufferedResponseWrapper(HttpServletResponse response) {
                                      original = response;
                                  }
                      
                                  public String getContent() {
                                      return bos.toString();
                                  }
                      
                                  public PrintWriter getWriter() throws IOException {
                                      return original.getWriter();
                                  }
                      
                                  public ServletOutputStream getOutputStream() throws IOException {
                                      if (tee == null) {
                                          bos = new ByteArrayOutputStream();
                                          tee = new TeeServletOutputStream(original.getOutputStream(),
                                                  bos);
                                      }
                                      return tee;
                      
                                  }
                      
                                  @Override
                                  public String getCharacterEncoding() {
                                      return original.getCharacterEncoding();
                                  }
                      
                                  @Override
                                  public String getContentType() {
                                      return original.getContentType();
                                  }
                      
                                  @Override
                                  public void setCharacterEncoding(String charset) {
                                      original.setCharacterEncoding(charset);
                                  }
                      
                                  @Override
                                  public void setContentLength(int len) {
                                      original.setContentLength(len);
                                  }
                      
                                  @Override
                                  public void setContentLengthLong(long l) {
                                      original.setContentLengthLong(l);
                                  }
                      
                                  @Override
                                  public void setContentType(String type) {
                                      original.setContentType(type);
                                  }
                      
                                  @Override
                                  public void setBufferSize(int size) {
                                      original.setBufferSize(size);
                                  }
                      
                                  @Override
                                  public int getBufferSize() {
                                      return original.getBufferSize();
                                  }
                      
                                  @Override
                                  public void flushBuffer() throws IOException {
                                      tee.flush();
                                  }
                      
                                  @Override
                                  public void resetBuffer() {
                                      original.resetBuffer();
                                  }
                      
                                  @Override
                                  public boolean isCommitted() {
                                      return original.isCommitted();
                                  }
                      
                                  @Override
                                  public void reset() {
                                      original.reset();
                                  }
                      
                                  @Override
                                  public void setLocale(Locale loc) {
                                      original.setLocale(loc);
                                  }
                      
                                  @Override
                                  public Locale getLocale() {
                                      return original.getLocale();
                                  }
                      
                                  @Override
                                  public void addCookie(Cookie cookie) {
                                      original.addCookie(cookie);
                                  }
                      
                                  @Override
                                  public boolean containsHeader(String name) {
                                      return original.containsHeader(name);
                                  }
                      
                                  @Override
                                  public String encodeURL(String url) {
                                      return original.encodeURL(url);
                                  }
                      
                                  @Override
                                  public String encodeRedirectURL(String url) {
                                      return original.encodeRedirectURL(url);
                                  }
                      
                                  @SuppressWarnings("deprecation")
                                  @Override
                                  public String encodeUrl(String url) {
                                      return original.encodeUrl(url);
                                  }
                      
                                  @SuppressWarnings("deprecation")
                                  @Override
                                  public String encodeRedirectUrl(String url) {
                                      return original.encodeRedirectUrl(url);
                                  }
                      
                                  @Override
                                  public void sendError(int sc, String msg) throws IOException {
                                      original.sendError(sc, msg);
                                  }
                      
                                  @Override
                                  public void sendError(int sc) throws IOException {
                                      original.sendError(sc);
                                  }
                      
                                  @Override
                                  public void sendRedirect(String location) throws IOException {
                                      original.sendRedirect(location);
                                  }
                      
                                  @Override
                                  public void setDateHeader(String name, long date) {
                                      original.setDateHeader(name, date);
                                  }
                      
                                  @Override
                                  public void addDateHeader(String name, long date) {
                                      original.addDateHeader(name, date);
                                  }
                      
                                  @Override
                                  public void setHeader(String name, String value) {
                                      original.setHeader(name, value);
                                  }
                      
                                  @Override
                                  public void addHeader(String name, String value) {
                                      original.addHeader(name, value);
                                  }
                      
                                  @Override
                                  public void setIntHeader(String name, int value) {
                                      original.setIntHeader(name, value);
                                  }
                      
                                  @Override
                                  public void addIntHeader(String name, int value) {
                                      original.addIntHeader(name, value);
                                  }
                      
                                  @Override
                                  public void setStatus(int sc) {
                                      original.setStatus(sc);
                                  }
                      
                                  @SuppressWarnings("deprecation")
                                  @Override
                                  public void setStatus(int sc, String sm) {
                                      original.setStatus(sc, sm);
                                  }
                      
                                  @Override
                                  public String getHeader(String arg0) {
                                      return original.getHeader(arg0);
                                  }
                      
                                  @Override
                                  public Collection<String> getHeaderNames() {
                                      return original.getHeaderNames();
                                  }
                      
                                  @Override
                                  public Collection<String> getHeaders(String arg0) {
                                      return original.getHeaders(arg0);
                                  }
                      
                                  @Override
                                  public int getStatus() {
                                      return original.getStatus();
                                  }
                      
                              }
                          }
                      

                      【讨论】:

                      • 这适用于响应日志记录 - 尽管我必须限制它记录的字节数,否则它会破坏 Intellij 日志记录控制台输出。
                      • String getContent() { if (bos == null) { return String.format("调用 %s 太早", BufferedResponseWrapper.class.getCanonicalName()); } 字节[] 字节 = bos.toByteArray(); return new String(Arrays.copyOf(bytes, 5000)) + "...."; }
                      • 还值得在日志记录周围添加一个“log.isTraceEnabled()”开关。
                      • 如果 Java 为 HttpServletResponse 添加了一些默认方法这样我们就不需要编写如此庞大的实现了。
                      • 加一个用于包含导入语句
                      猜你喜欢
                      • 2019-03-26
                      • 2018-11-29
                      • 1970-01-01
                      • 2015-05-21
                      • 1970-01-01
                      • 1970-01-01
                      • 2010-12-16
                      • 1970-01-01
                      相关资源
                      最近更新 更多