springboot项目记录http请求记录

本文最后更新于:6 个月前

代码来咯,看代码多是一件美事呦,你他奶奶的为什么不看代码,给我看。

工作的时候,尤其是接口联调的时候,打印http请求记录是甩锅和解决问题的最好的方式。尤其是请求体里的内容。就是这个获取请求体的内容其实还有点门道,我就被坑过,你直接在拦截器里直接获取request里面的body,拦截器里能获取到,Controller里面就获取不到了。具体原因不深究,就是Body是用流传的,你读取了之后这个流就被消耗了,然后后面的代码再从request里面就读取不到了。

引入spring-boot-starter-actuator

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

引入之后,配置存储方式httpTraceRepository,这个实例就是添加在内存中存储最近100次请求记录

@Configuration
public class ConfigTrace {
    @Bean
    public HttpTraceRepository httpTraceRepository(){
        return new InMemoryHttpTraceRepository();
    }
}

最后在application.properties中加入

management.endpoints.web.exposure.include=*

运行项目访问http://localhost:8080/actuator/httptrace

得到这样的JSON数据(已经格式化),

{
  "traces": [
    {
      "timestamp": "2022-02-19T05:08:05.370267100Z",
      "principal": null,
      "session": null,
      "request": {
        "method": "GET",
        "uri": "http://localhost:8080/index?a=%E4%BB%8E%E5%89%8D%E4%BB%8E%E5%89%8D&b=%E6%9C%89%E4%B8%AA%E4%BA%BA%E7%88%B1%E4%BD%A0%E5%BE%88%E4%B9%85",
        "headers": {
          "host": [
            "localhost:8080"
          ],
          "content-type": [
            "application/x-www-form-urlencoded"
          ],
          "connection": [
            "Keep-Alive"
          ],
          "accept-encoding": [
            "gzip,deflate"
          ],
          "user-agent": [
            "Apache-HttpClient/4.5.13 (Java/11.0.12)"
          ]
        },
        "remoteAddress": null
      },
      "response": {
        "status": 200,
        "headers": {
          "Keep-Alive": [
            "timeout=60"
          ],
          "Connection": [
            "keep-alive"
          ],
          "Content-Length": [
            "35"
          ],
          "Date": [
            "Sat, 19 Feb 2022 05:08:05 GMT"
          ],
          "Content-Type": [
            "text/plain;charset=UTF-8"
          ]
        }
      },
      "timeTaken": 120
    }
  ]
}

引入这个依赖的好处,就是几乎不需要写任何代码,引入,然后配置一下即可查看,缺点也有就是不能记录post请求的请求体,也无法记录返回值,持久化,目前我没有研究,但是我感觉可以。可以自行查阅资料研究。

使用springmvc自带的一个类

增加一个配置类

@Bean
    CommonsRequestLoggingFilter loggingFilter(){
        CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
        // 记录 客户端 IP信息
        loggingFilter.setIncludeClientInfo(true);
        // 记录请求头
        loggingFilter.setIncludeHeaders(true);
        // 如果记录请求头的话,可以指定哪些记录,哪些不记录
        // loggingFilter.setHeaderPredicate();
        // 记录 请求体  特别是POST请求的body参数
        loggingFilter.setIncludePayload(true);
        // 请求体的大小限制 默认50
        loggingFilter.setMaxPayloadLength(10000);
        //记录请求路径中的query参数
        loggingFilter.setIncludeQueryString(true);
        return loggingFilter;
    }

CommonsRequestLoggingFilter可以点进去看一下,就是一个springboot提供得一个类。可以自己进行改造。接下来再配置一下logback就行。配置如下。看不懂或者不理解看这个logback配置详情

<?xml version="1.0" encoding="UTF-8" ?>

<configuration>
    <!--
        输出日志到控制台
    -->
    <appender name="soutAppender" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%date %level [%thread] %logger{10}.%class{0}#%method[%file:%line] %n%msg%n</pattern>
        </layout>
    </appender>

    <!--
        输出日志到文件
     -->
    <appender name="fileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <append>true</append>
        <prudent>false</prudent>
        <File>logs/log.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/%d{yyyy-MM-dd}/log%i.log</fileNamePattern>
            <maxHistory>30</maxHistory>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!-- 单个日志文件最大体积 -->
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!-- 对日志进行格式化 -->
            <pattern>%date %level [%thread] %logger{10}.%class{0}#%method[%file:%line] %n%msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
    </appender>

        <!-- 这个是重点 -->
    <logger name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="debug">
    </logger>

    <root level="info">
        <appender-ref ref="soutAppender"/>
        <appender-ref ref="fileAppender"/>
    </root>
</configuration>

配置好打印的日志应该是这样

2022-02-19 19:02:28,608 DEBUG [http-nio-8080-exec-1] o.s.w.f.CommonsRequestLoggingFilter.CommonsRequestLoggingFilter#beforeRequest[CommonsRequestLoggingFilter.java:47] 
Before request [GET /index?a=%E4%BB%8E%E5%89%8D%E4%BB%8E%E5%89%8D&b=%E6%9C%89%E4%B8%AA%E4%BA%BA%E7%88%B1%E4%BD%A0%E5%BE%88%E4%B9%85, client=127.0.0.1, headers=[host:"localhost:8080", connection:"Keep-Alive", user-agent:"Apache-HttpClient/4.5.13 (Java/11.0.12)", accept-encoding:"gzip,deflate", Content-Type:"application/x-www-form-urlencoded;charset=UTF-8"]]
2022-02-19 19:02:28,668 DEBUG [http-nio-8080-exec-1] o.s.w.f.CommonsRequestLoggingFilter.CommonsRequestLoggingFilter#afterRequest[CommonsRequestLoggingFilter.java:55] 
After request [GET /index?a=%E4%BB%8E%E5%89%8D%E4%BB%8E%E5%89%8D&b=%E6%9C%89%E4%B8%AA%E4%BA%BA%E7%88%B1%E4%BD%A0%E5%BE%88%E4%B9%85, client=127.0.0.1, headers=[host:"localhost:8080", connection:"Keep-Alive", user-agent:"Apache-HttpClient/4.5.13 (Java/11.0.12)", accept-encoding:"gzip,deflate", Content-Type:"application/x-www-form-urlencoded;charset=UTF-8"]]
2022-02-19 19:02:39,737 DEBUG [http-nio-8080-exec-3] o.s.w.f.CommonsRequestLoggingFilter.CommonsRequestLoggingFilter#beforeRequest[CommonsRequestLoggingFilter.java:47] 
Before request [POST /save, client=127.0.0.1, headers=[content-length:"14", host:"localhost:8080", connection:"Keep-Alive", user-agent:"Apache-HttpClient/4.5.13 (Java/11.0.12)", accept-encoding:"gzip,deflate", Content-Type:"application/x-www-form-urlencoded;charset=UTF-8"]]
2022-02-19 19:02:39,743 DEBUG [http-nio-8080-exec-3] o.s.w.f.CommonsRequestLoggingFilter.CommonsRequestLoggingFilter#afterRequest[CommonsRequestLoggingFilter.java:55] 
After request [POST /save, client=127.0.0.1, headers=[content-length:"14", host:"localhost:8080", connection:"Keep-Alive", user-agent:"Apache-HttpClient/4.5.13 (Java/11.0.12)", accept-encoding:"gzip,deflate", Content-Type:"application/x-www-form-urlencoded;charset=UTF-8"], payload=a=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C]

payload中得东西为请求体。一次请求会打印两条日志,Before request为过滤器前,After request为过滤器后的记录,这种方式其实已经挺好用了。但是还有个问题就是不能记录返回值,也有方法。

记录返回值

继承ResponseBodyAdvice和使用RestControllerAdvice注解来实现。


@Slf4j
@RestControllerAdvice
public class RestBodyAdvice implements ResponseBodyAdvice<Object> {
    private static final int DEFAULT_MAX_PAYLOAD_LENGTH = 10000;
    public static final String REQUEST_MESSAGE_PREFIX = "Request [";
    public static final String REQUEST_MESSAGE_SUFFIX = "]";
    private final ObjectMapper objectMapper = new ObjectMapper();


    /**
     * 顶级异常
     * 异常的信息也能捕获到
     */
    @ExceptionHandler(Exception.class)
    ResponseEntity<Object> handlerException(Exception e,
                                              HttpServletRequest rq, HttpServletResponse rp) {
        log.error("顶级异常: {}",e.getMessage());
        e.printStackTrace();
        return ResponseEntity.ok("系统错误");
    }

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

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request,
                                  ServerHttpResponse response) {

        ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest) request;

        log.debug(createRequestMessage(servletServerHttpRequest.getServletRequest(), REQUEST_MESSAGE_PREFIX, REQUEST_MESSAGE_SUFFIX));
        // 这里注意一下,如果body已经是JOSN数据了在进行转JSON处理会报错。
        log.debug("Response Body ["+ objectMapper.writeValueAsString(body) +"]");
        return body;
    }


    private boolean checkPrimitive(Object body) {
        Class<?> clazz = body.getClass();
        return clazz.isPrimitive()
                || clazz.isArray()
                || Collection.class.isAssignableFrom(clazz)
                || body instanceof Number
                || body instanceof Boolean
                || body instanceof Character
                || body instanceof String;
    }


    protected String createRequestMessage(HttpServletRequest request, String prefix, String suffix) {
        StringBuilder msg = new StringBuilder();
        msg.append(prefix);
        msg.append(request.getMethod()).append(" ");
        msg.append(request.getRequestURI());


        String queryString = request.getQueryString();
        if (queryString != null) {
            msg.append('?').append(queryString);
        }


        String client = request.getRemoteAddr();
        if (StringUtils.hasLength(client)) {
            msg.append(", client=").append(client);
        }
        HttpSession session = request.getSession(false);
        if (session != null) {
            msg.append(", session=").append(session.getId());
        }
        String user = request.getRemoteUser();
        if (user != null) {
            msg.append(", user=").append(user);
        }

        HttpHeaders headers = new ServletServerHttpRequest(request).getHeaders();
        msg.append(", headers=").append(headers);

        String payload = getMessagePayload(request);
        if (payload != null) {
            msg.append(", payload=").append(payload);
        }

        msg.append(suffix);
        return msg.toString();
    }

    protected String getMessagePayload(HttpServletRequest request) {
        ContentCachingRequestWrapper wrapper =
                WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        if (wrapper != null) {
            byte[] buf = wrapper.getContentAsByteArray();
            if (buf.length > 0) {
                int length = Math.min(buf.length, DEFAULT_MAX_PAYLOAD_LENGTH);
                try {
                    return new String(buf, 0, length, wrapper.getCharacterEncoding());
                } catch (UnsupportedEncodingException ex) {
                    return "[unknown]";
                }
            }
        }
        return null;
    }
}

然后再配置logback

<logger name="com.example.springboothttptrace.RestBodyAdvice" level="debug">

</logger>

配置一下,输出的日志为

2022-02-21 10:19:29,880 DEBUG [http-nio-8080-exec-3] c.e.s.RestBodyAdvice.RestBodyAdvice#beforeBodyWrite[RestBodyAdvice.java:64] 
Request [POST /save, client=127.0.0.1, headers=[content-length:"14", host:"localhost:8080", connection:"Keep-Alive", user-agent:"Apache-HttpClient/4.5.13 (Java/11.0.12)", accept-encoding:"gzip,deflate", Content-Type:"application/x-www-form-urlencoded;charset=UTF-8"], payload=a=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C]
2022-02-21 10:19:29,880 DEBUG [http-nio-8080-exec-3] c.e.s.RestBodyAdvice.RestBodyAdvice#beforeBodyWrite[RestBodyAdvice.java:66] 
Response Body ["你好世界"]

至此,http的请求记录就全部记录到了,payload中为请求体,请求体和返回内容。自己可以按需使用,开发过程中建议打印全部日志,方便调试,上线后,可以不打印。去掉也很方便,直接在logback配置中配置一下就可以了。

封面

坐在树上的美女宋仁何 绿色裙子 美腿 护眼森系


springboot项目记录http请求记录
https://wangijun.com/2022/02/18/java-09/
作者
无良芳
发布于
2022年2月18日
许可协议