解决 HttpServletRequest 流数据不可重复读

背景介绍

甲方客户的生产系统,有安全风险预警和安全事件快速溯源要求,需要做一套日志管理规范。

要求我们接入的系统,要对用户登录、注册、密码修改等重要场景,严格按照提供的格式,输出相应的日志。

后续通过filebeat对接,收集我们系统上的日志信息。

简单来说,就是应用系统,处理接口请求时,统一打印相应日志。

问题描述

成熟且常见的日志统一打印方案,就是使用AOP技术,自定义注解,在切面上使用环绕通知@Around,拦截请求,获取Controller类上方法的入参、出参即可。

奈何业务场景使用到的接口,以前的人在实现的时候,使用了如下方式:

1
2
3
4
java复制代码@RequestMapping(value = "/auth", method = { RequestMethod.POST, RequestMethod.GET, RequestMethod.OPTIONS })
public void auth(HttpServletRequest req, HttpServletResponse resp) {
authService.auth(req, resp);
}

把传参直接丢在 HttpServletRequest 中。

返回参数,又是采用 HttpServletResponse 输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public void printResult(HttpServletRequest req, HttpServletResponse resp,
String action, int code, String msg, Object result) {
PrintWriter p = null;
Ret ret = new Ret();
Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss")
.serializeNulls()
.create();
try {
p = resp.getWriter();
ret.setRspCode(code);
ret.setRspDesc(msg);
ret.setData(result);

p.write(gson.toJson(ret));
return;
} catch (Exception e) {
logger.error(e.getMessage());
} finally {
p.flush();
p.close();
}
}

不像平时熟练的做法,把具体入参和出参,用对象封装,直接放在方法上即可。

因为上面的做法,导致我们在拦截器中,想提前拦截请求获取传参,使用 request.getParameter() 等方法时,能拿到参数。
但是在具体接口业务流程中,再使用request.getParameter() 等方法,传入参数就获取不到了。

因为流只能被读一次

因此就抛出一个问题:Request 和 Response 怎么重复读取?

解决方案

使用request.getParameter() 等方法,最终会调用getInputStream方法。

需要重写HttpServletRequestWrapper包装类,在调用getInputStream方法时,将流数据同时写到缓存。

后面想获取参数,直接读取缓存数据即可。

这样就可以实现Request的内容多次读取。

实现代码

封装request

自定义类 ContentCachingRequestWrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
java复制代码import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

/**
*
* 重写 HttpServletRequestWrapper
*
* @Author: linzengrui
* @Date: 2021/11/22 15:33
*/
public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {

private final byte[] body;

public ContentCachingRequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8))){
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
}
body = sb.toString().getBytes(StandardCharsets.UTF_8);
}

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

@Override
public ServletInputStream getInputStream() throws IOException {

final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);

return new ServletInputStream() {

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

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

@Override
public void setReadListener(ReadListener readListener) {

}

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

public byte[] getBody() {
return body;
}
}

封装response

自定义类 ContentCachingResponseWrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
java复制代码
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;

/**
*
* 重写 HttpServletResponseWrapper
*
* @Author: linzengrui
* @Date: 2021/11/22 19:45
*/
public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {

private ByteArrayOutputStream byteArrayOutputStream;
private ServletOutputStream servletOutputStream;
private PrintWriter printWriter;

public ContentCachingResponseWrapper(HttpServletResponse response) {
super(response);
byteArrayOutputStream = new ByteArrayOutputStream();
servletOutputStream = new ServletOutputStream() {
@Override
public boolean isReady() {
return false;
}

@Override
public void setWriteListener(WriteListener writeListener) {

}

@Override
public void write(int b) throws IOException {
byteArrayOutputStream.write(b);
}
};
printWriter = new PrintWriter(byteArrayOutputStream);
}

@Override
public PrintWriter getWriter() {
return printWriter;
}

@Override
public ServletOutputStream getOutputStream() throws IOException {
return servletOutputStream;
}

public byte[] toByteArray() {
return byteArrayOutputStream.toByteArray();
}

}

过滤器 Filter 拦截请求

拦截器 LogFilter 使用上面封装的包装类,即可获取传参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
java复制代码@Slf4j
@WebFilter(urlPatterns = "/*")
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 使用 重写 HttpServletRequestWrapper 的自定义包装类
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest)request);
// 使用 重写 HttpServletResponseWrapper 的自定义包装类
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);

// 只能执行一次获取流方法
try(ServletOutputStream outputStream = response.getOutputStream()){
// 获取传参
String requestParamJson = new String(requestWrapper.getBody());
log.info("requestParamJson --> {}", requestParamJson);

// 具体方法执行流程
chain.doFilter(requestWrapper, responseWrapper);

// 触发获取流操作后,可以从缓存多次拿数据
String respDataJson = new String(responseWrapper.toByteArray());
log.info("respDataJson <-- {}", respDataJson);

// TODO 写日志


// 需要重新写入内容,否则流无输出内容
outputStream.write(respDataJson.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}catch (Exception e){
e.printStackTrace();
}

}


@Override
public void destroy() {

}
}

springboot 启动类添加注解 @ServletComponentScan

注意:启动类要加上注解@ServletComponentScan识别上面注入的Filter。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码import org.springframework.boot.SpringApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@ServletComponentScan
@SpringBootApplication
public class SpringBootApplication {

public static void main(String[] args) {
SpringApplication.run(SpringBootApplication.class, args);
}

}

具体业务接口,原来的逻辑保持不变,仍然可以获取到入参。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%