Filter能够在一个请求到达servlet之前预处理用户请求, 也可以在离开servlet时处理http响应.
一. 请求录制
请求录制是记录真实业务场景和用户行为, 并通过回放对已有功能进行回归测试.今天就利用filter进行请求录制. 录制内容包括request信息, response返回值信息,链路等信息, 并利用log日志文件记录下来.
二. Request参数解析
先来看下request的两种处理方式: GET和POST. GET把参数包含在URL中, POST通过request body传递参数.是需要两种不同的参数解析方式.
2.1
GET参数解析
GET方式下的参数解析有原生的API, 只需要调用request.getParameterNames()方法就可以拿到所有请求参数
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就接收不到数据了. 所以,为保证不影响后续流程, 需要对数据流重新封装下.
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返回值的流处理也需要重新封装下.
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信息
String uri = request.getRequestURI();
4.2
method信息
String method = request.getMethod();
4.3
header信息
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依赖:
<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日志打印输出
@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请求
@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
执行结果
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接口用来处理返回值. 两者配合使用也可以达到请求录制的功能.