请求录制 | 巧用filter

2022-06-27 15:08:30 浏览数 (1)

Filter能够在一个请求到达servlet之前预处理用户请求, 也可以在离开servlet时处理http响应.

一. 请求录制

请求录制是记录真实业务场景和用户行为, 并通过回放对已有功能进行回归测试.今天就利用filter进行请求录制. 录制内容包括request信息, response返回值信息,链路等信息, 并利用log日志文件记录下来.

二. Request参数解析

先来看下request的两种处理方式: GET和POST. GET把参数包含在URL中, POST通过request body传递参数.是需要两种不同的参数解析方式.

2.1

GET参数解析

GET方式下的参数解析有原生的API, 只需要调用request.getParameterNames()方法就可以拿到所有请求参数

代码语言:javascript复制
private StringBuffer getParam(HttpServletRequest request) {
    Enumeration<String> parameterNames = request.getParameterNames();
    StringBuffer bufParam = new StringBuffer();
    while (parameterNames.hasMoreElements()) {
        String paramKey = parameterNames.nextElement();
        String paramValue = request.getParameter(paramKey);
        bufParam.append(paramKey).append("=").append(paramValue).append(";");
    }
    return bufParam;
}

2.2

POST参数解析

与GET方式不同的是, POST方式的参数是存放在消息主体(entity-body)中的, 服务端会根据请求头(headers)中的Content-Type字段来获知请求中编码方式, 并进行解析. 基于当前最流行的微服务模式, 这里介绍Content-Type为application/json的编码方式解析过程.

对于消息主体(entity-body)中的内容, 是需要使用流的方式进行接收解析, 但是, 也就意味着数据只能用一次, 后面的servlet就接收不到数据了. 所以,为保证不影响后续流程, 需要对数据流重新封装下.

代码语言:javascript复制
private static class RequestWrapper extends HttpServletRequestWrapper {
  private byte[] rawData;
  private HttpServletRequest request;
  private ResettableServletInputStream servletStream;

  public RequestWrapper(HttpServletRequest request) {
      super(request);
      this.request = request;
      this.servletStream = new ResettableServletInputStream();
  }

  public void resetInputStream() {
      servletStream.stream = new ByteArrayInputStream(rawData);
  }

  @Override
  public ServletInputStream getInputStream() throws IOException {
      if (rawData == null) {
          rawData = IOUtils.toByteArray(this.request.getReader());
          servletStream.stream = new ByteArrayInputStream(rawData);
      }
      return servletStream;
  }

  @Override
  public BufferedReader getReader() throws IOException {
      if (rawData == null) {
          rawData = IOUtils.toByteArray(this.request.getReader());
          servletStream.stream = new ByteArrayInputStream(rawData);
      }
      return new BufferedReader(new InputStreamReader(servletStream));
  }

  private class ResettableServletInputStream extends ServletInputStream {
      private InputStream stream;

      @Override
      public int read() throws IOException {
          return stream.read();
      }

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

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

      @Override
      public void setReadListener(ReadListener readListener) {
      }
  }
}

三. Response返回值解析

与POST的参数解析类似, Response返回值的流处理也需要重新封装下.

代码语言:javascript复制
private static class ResponseWrapper extends HttpServletResponseWrapper {

  private ByteArrayOutputStream bos = new ByteArrayOutputStream();
  private ServletResponse response;
  private PrintWriter writer;
  private byte[] data;

  public ResponseWrapper(ServletResponse response) {
      super((HttpServletResponse) response);
      this.response = response;
  }

  @Override
  public ServletOutputStream getOutputStream() {
      return new MyServletOutputStream(bos);
  }

  @Override
  public PrintWriter getWriter() throws UnsupportedEncodingException {
      writer = new PrintWriter(new OutputStreamWriter(bos, "utf-8"));
      return writer;
  }

  public byte[] getData() throws IOException {
      bos.flush();
      data = this.bos.toByteArray();
      return data;
  }

  public void rewrite() {
      try (ServletOutputStream outputStream = response.getOutputStream();) {
          outputStream.write(data);
          outputStream.flush();
      } catch (IOException e) {
          LOGGER.warn("rewrite error:", e);
      }
  }

  class MyServletOutputStream extends ServletOutputStream {

      private ByteArrayOutputStream ostream;

      public MyServletOutputStream(ByteArrayOutputStream ostream) {
          this.ostream = ostream;
      }

      @Override
      public void write(int b) throws IOException {
          ostream.write(b);
      }

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

      @Override
      public void setWriteListener(WriteListener listener) {

      }
  }
}

四. 其他请求信息

除了参数和返回信息之外, 还需要uri, method, header, 链路跟踪trace等信息.

4.1

uri信息

代码语言:javascript复制
String uri = request.getRequestURI();

4.2

method信息

代码语言:javascript复制
String method = request.getMethod();

4.3

header信息

代码语言:javascript复制
private StringBuffer getHeader(HttpServletRequest request) {
    Enumeration<String> headerNames = request.getHeaderNames();
    StringBuffer bufHeader = new StringBuffer();
    while (headerNames.hasMoreElements()) {
        String headerName = headerNames.nextElement();
        String headerValue = request.getHeader(headerName);
        bufHeader.append(headerName).append("=").append(headerValue).append(";");
    }
    return bufHeader;
}

4.4

链路跟踪trace信息

trace信息可以利用zipkin将traceId打印到日志文件中

POM依赖:

代码语言:javascript复制
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>Finchley.SR4</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

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

五. Filter类

整合request和response信息, 并利用log日志打印输出

代码语言:javascript复制
@WebFilter(urlPatterns = "/*", filterName = "RecordFilter")
public class RequestRecordFilter implements Filter {
    private static final Logger LOGGER = LoggerFactory.getLogger(RequestRecordFilter.class);
    org.slf4j.Marker marker = org.slf4j.MarkerFactory.getMarker("REQUEST");

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
            FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String uri = request.getRequestURI();
        String method = request.getMethod();
        StringBuffer bufHeader = getHeader(request);
        StringBuffer bufParam = getParam(request);
        RequestWrapper wrappedRequest = new RequestWrapper(request);
        String body = getBody(wrappedRequest);
        wrappedRequest.resetInputStream();

        ResponseWrapper wrappedResponse = new ResponseWrapper(servletResponse);
        filterChain.doFilter(wrappedRequest, wrappedResponse);
        String responseData = getResponseData(wrappedResponse);
        LOGGER.error(marker,
                "uri=[{}],method=[{}],Header=[{}],Parameter=[{}],Body=[{}],response=[{}]",
                uri, method, bufHeader.toString(), bufParam.toString(), body, responseData);
    }
// ...
}

六. 测试

6.1

POST请求

代码语言:javascript复制
@RestController
public class HelloController {
    private static final Logger logger = LoggerFactory.getLogger(HelloController.class);

    @PostMapping(value = "/post/test",produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public String post(@RequestBody Dto dto){
        return dto.str  "-"  System.currentTimeMillis();
    }
}
class Dto{
     String str;

    public String getStr() {
        return str;
    }

    public void setStr(String str) {
        this.str = str;
    }
}

6.2

执行结果

代码语言:javascript复制
2021-02-17 17:05:18.338 ERROR [RequestRecord,d9cbc8f9f6f47d30,d9cbc8f9f6f47d30,true] 56661 --- [nio-8080-exec-1] com.in.RequestRecordFilter               : uri=[/post/test],method=[POST],Header=[host=localhost:8080;connection=keep-alive;content-length=21;sec-ch-ua="Chromium";v="88", "Google Chrome";v="88", ";Not A Brand";v="99";accept=application/json;charset=UTF-8;sec-ch-ua-mobile=?0;user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36;content-type=application/json;origin=http://localhost:8080;sec-fetch-site=same-origin;sec-fetch-mode=cors;sec-fetch-dest=empty;referer=http://localhost:8080/swagger-ui.html;accept-encoding=gzip, deflate, br;accept-language=zh-CN,zh;q=0.9;cookie=Idea-2af32566=512880ee-48ca-47a8-a36e-8a43047d2321;],Parameter=[],Body=[{ "str": "string"}],response=[string-1613552718328]

小结

本文主要介绍了利用filter记录数据请求和用户行为, 使用filter的优点是泛用性比较强, 只要是web服务都可以使用.

除此之外,在特定的环境下也有其他方式实现请求录制. 在SpringV4.2版本中利用AOP机制新加了RequestBodyAdvice接口用来处理@RequestBody或HttpEntit封装的参数, 但也意味着不能处理GET方式参数. 类似的, 在SpringV4.1版本中增加了ResponseBodyAdvice接口用来处理返回值. 两者配合使用也可以达到请求录制的功能.

0 人点赞