前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >请求录制 | 巧用filter

请求录制 | 巧用filter

作者头像
一个架构师
发布2022-06-27 15:08:30
4820
发布2022-06-27 15:08:30
举报
文章被收录于专栏:从码农的全世界路过

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接口用来处理返回值. 两者配合使用也可以达到请求录制的功能.

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-02-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 从码农的全世界路过 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档