6

Spring在Filter中记录Web请求Request和返回Response的内容及时长

 1 year ago
source link: https://www.pkslow.com/archives/spring-log-request-response
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Spring MVC中,我们有时需要记录一下请求和返回的内容,方便出现问题时排查。比较Header、Request Body等。这些在Controller也可以记录,但在Filter中会更方便。而我们使用的是OncePerRequestFilter

2 记录请求

2.1 流重复读的问题

可以通过下面的代码来读取请求Body:

byte[] requestBody = StreamUtils.copyToByteArray(request.getInputStream());
log.info("request body = {}", new String(requestBody, StandardCharsets.UTF_8));

但是这里从流读取了一次内容后,后续不可再读了。这就造成了真正处理请求的时候,报错失败,我们需要把Request对象改造成可重复读的类。

2.2 通过Wrapper解决流重复读的问题

为了可以让流重复读,加了以下Wrapper:

public class PkslowRequestWrapper extends HttpServletRequestWrapper {
    private final byte[] body;
    public PkslowRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        body = StreamUtils.copyToByteArray(request.getInputStream());
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }

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

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

            @Override
            public void setReadListener(ReadListener readListener) {

            }
        };
    }

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

这里主要在构造时读了流,然后存在变量body里,每次返回流的时候从body构造回去即可。

在Filter中使用这个Wrapper如下:

PkslowRequestWrapper request = new PkslowRequestWrapper(req);
ServletInputStream servletInputStream = request.getInputStream();
String body = StreamUtils.copyToString(servletInputStream, Charset.defaultCharset());
log.info("Request Body(PkslowRequestWrapper): {}", body);

2.3 内置Filter

其实,针对Request,Spring Boot提供了内置的Filter可以直接记录请求,使用如下:

package com.pkslow.springboot.common.web.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CommonsRequestLoggingFilter;

@Configuration
public class PkslowConfig {
    @Bean
    public CommonsRequestLoggingFilter loggingFilter() {
        CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
        filter.setIncludeHeaders(true);
        filter.setIncludeClientInfo(true);
        filter.setIncludePayload(true);
        filter.setIncludeQueryString(true);

        filter.setAfterMessagePrefix("CommonsRequestLoggingFilter Request: ");

        return filter;
    }
}

但要开debug级别的日志才会打出来。

logging:
  level:
    root: debug

日志如下:

DEBUG 20356 --- [nio-8080-exec-1] o.s.w.f.CommonsRequestLoggingFilter      : Before request [POST /hello/pkslow, client=127.0.0.1, headers=[authorization:"Basic xxxxxx", content-length:"37", host:"localhost:8080", connection:"Keep-Alive", user-agent:"Apache-HttpClient/4.5.13 (Java/17.0.5)", accept-encoding:"gzip,deflate", Content-Type:"application/json;charset=UTF-8"]]

3 记录返回

返回也是一样,有流不可重复读的问题,使用Spring自带的ContentCachingResponseWrapper即可。

ContentCachingResponseWrapper response = new ContentCachingResponseWrapper(res);
log.info("Response Code: {}", response.getStatus());
String responseBody = new String(response.getContentAsByteArray(), response.getCharacterEncoding());
log.info("Response Body: {}", responseBody);
response.copyBodyToResponse();

特别注意一定要调用copyBodyToResponse()这个方法,不然无法返回body给请求端了。

4 记录时间

记录整个请求的处理时间请参考: Java如何测量方法执行时间

测试一下:

POST http://localhost:8080/hello/pkslow
Content-Type: application/json
Authorization: Basic xxxxxx

{
  "id": 999,
  "value": "content"
}

执行日志结果如下:

spring-log-request-response.log.png

也可使用ContentCachingRequestWrapper来解决请求流不可重复读的问题,但这个Wrapper是有限制的,具体可以看它源码。也有人提了Issue

代码请看GitHub: https://github.com/LarryDpk/pkslow-samples


References:

OncePerRequestFilter

How to Record Request and Response Bodies in Sping Boot Applications

https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times


Code for all: GitHub

欢迎关注微信公众号<南瓜慢说>,将持续为你更新...

file

Recommendations:
Cloud Native
Terraform
Container: Docker/Kubernetes
Spring Boot / Spring Cloud
Https
如何制定切实可行的计划并好好执行


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK