From 3fc44375d8fa26069127e74ee0ad416686f1efc3 Mon Sep 17 00:00:00 2001 From: Charles7c Date: Mon, 26 Sep 2022 19:00:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E3=80=8A=E8=A7=A3?= =?UTF-8?q?=E5=86=B3=E6=97=A0=E6=B3=95=E9=87=8D=E5=A4=8D=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E4=BD=93=E5=92=8C=E5=93=8D=E5=BA=94=E4=BD=93?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=E3=80=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- .../SpringBoot项目引入OpenFeign后无法启动.md | 1 + .../解决无法重复读取请求体和响应体的问题.md | 182 ++++++++++++++++++ 3 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 docs/categories/issues/2022/09/23/解决无法重复读取请求体和响应体的问题.md diff --git a/README.md b/README.md index b5d9cbcb5..c2d080265 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ git clone https://github.com/Charles7c/charles7c.github.io.git # 2.安装依赖 yarn install -# 3.dev 运行,访问:http://localhost:3000 +# 3.dev 运行,访问:http://localhost:5173 yarn dev # 4.打包,文件存放位置:docs/.vitepress/dist yarn build diff --git a/docs/categories/issues/2022/08/31/SpringBoot项目引入OpenFeign后无法启动.md b/docs/categories/issues/2022/08/31/SpringBoot项目引入OpenFeign后无法启动.md index fd5028b76..70aba27ae 100644 --- a/docs/categories/issues/2022/08/31/SpringBoot项目引入OpenFeign后无法启动.md +++ b/docs/categories/issues/2022/08/31/SpringBoot项目引入OpenFeign后无法启动.md @@ -5,6 +5,7 @@ date: 2022/08/31 22:39 categories: - Bug万象集 tags: + - Java - "Spring Boot" - "Spring Cloud" - "Open Feign" diff --git a/docs/categories/issues/2022/09/23/解决无法重复读取请求体和响应体的问题.md b/docs/categories/issues/2022/09/23/解决无法重复读取请求体和响应体的问题.md new file mode 100644 index 000000000..316c1d11a --- /dev/null +++ b/docs/categories/issues/2022/09/23/解决无法重复读取请求体和响应体的问题.md @@ -0,0 +1,182 @@ +--- +title: 解决无法重复读取请求体和响应体的问题 +author: 查尔斯 +date: 2022/09/23 20:55 +categories: + - Bug万象集 +tags: + - Java + - "Spring Boot" + - 过滤器 +--- + +# 解决无法重复读取请求体和响应体的问题 + +## 项目场景 + +**C:** 这两天实现了一个操作日志功能,需求是要记录指定操作的请求 URL,请求方式、请求头、请求体、响应码、响应头、响应体、请求耗时、操作人、操作IP、操作地址等信息。 + +考虑了几种方案,结合以前的经验,排掉了 AOP,综合评估后这次采用的是 Spring 拦截器的方式来记录,大体的实现流程是: + +1. 提供一个 `@Log` 注解 +2. 在需要记录操作日志的接口类及方法上添加 `@Log` 注解,指定好资源名称和操作类型(具体为什么要在类和方法上都加,是考虑复用操作的资源名称) +3. 提供一个拦截器,在拦截器中判断当前 Handler 是否存在 `@Log` 注解 +4. 存在该注解,就在 `preHandle()` 中开始计时,在 `afterCompletion()` 中结束计时并获取请求和响应信息 +5. 将请求和响应信息异步存储到数据库中 + + +## 问题描述 + +流程很简单,但是在获取 requestBody(请求体)和 responseBody(响应体)时出了些问题。如果我在 `preHandle()` 中获取了请求体信息,那么对应 Handler 就无法获取了,反之如果我是在 `afterCompletion` 中获取请求体信息,那么就获取不到了。而对于响应体,在我获取完之后,向前端响应就没内容了。 + +## 原因分析 +之所以如此,是由于请求体和响应体分别对应的是 InputStream 和 OutputStream,由于流的特性,使用完之后就无法再被使用了。 + +```java +/** + * Retrieves the body of the request as binary data using a {@link ServletInputStream}. Either this method or + * {@link #getReader} may be called to read the body, not both. + * + * @return a {@link ServletInputStream} object containing the body of the request + * + * @exception IllegalStateException if the {@link #getReader} method has already been called for this request + * + * @exception IOException if an input or output exception occurred + */ +public ServletInputStream getInputStream() throws IOException; +``` + +```java +/** + * Returns a {@link ServletOutputStream} suitable for writing binary data in the response. The servlet container + * does not encode the binary data. + * + *

+ * Calling flush() on the ServletOutputStream commits the response. + * + * Either this method or {@link #getWriter} may be called to write the body, not both, except when {@link #reset} + * has been called. + * + * @return a {@link ServletOutputStream} for writing binary data + * + * @exception IllegalStateException if the getWriter method has been called on this response + * + * @exception IOException if an input or output exception occurred + * + * @see #getWriter + * @see #reset + */ +public ServletOutputStream getOutputStream() throws IOException; +``` + +想要解决的话就要想办法把这信息使用完再“塞回去”,直接“塞回去”是不可能的。 + + +## 解决方案 + +为了解决这个问题,Servlet 提供了两个类 HttpServletRequestWrapper、HttpServletResponseWrapper,我们可以继承它们来实现请求体和响应体内容的缓存,达到重复读取请求体和响应体的目的。 + +不过既然我们在使用 Spring 框架,贴心的 Spring 也提供了两个实现类:ContentCachingRequestWrapper、ContentCachingResponseWrapper,这样我们就无需再自行定义相应 Wrapper 直接使用它们就可以解决这个问题了。 + +下面是在过滤器中对请求对象和响应对象进行包装处理的代码段: + + +```java +import org.springframework.core.Ordered; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; +import org.springframework.web.util.WebUtils; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; + +/** + * 缓存请求体和响应体过滤器 + * + *

+ * 由于 requestBody 和 responseBody 分别对应的是 InputStream 和 OutputStream,由于流的特性,读取完之后就无法再被使用了。 + * 所以,需要额外缓存一次流信息。 + *

+ * + * @author Charles7c + * @since 2022/9/22 16:33 + */ +@Component +public class ContentCachingWrapperFilter extends OncePerRequestFilter implements Ordered { + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 10; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + // 包装流,可重复读取 + if (!(request instanceof ContentCachingRequestWrapper)) { + request = new ContentCachingRequestWrapper(request); + } + if (!(response instanceof ContentCachingResponseWrapper)) { + response = new ContentCachingResponseWrapper(response); + } + + filterChain.doFilter(request, response); + updateResponse(response); + } + + /** + * 更新响应(不操作这一步,会导致接口响应空白) + * + * @param response 响应对象 + * @throws IOException / + */ + private void updateResponse(HttpServletResponse response) throws IOException { + ContentCachingResponseWrapper responseWrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class); + Objects.requireNonNull(responseWrapper).copyBodyToResponse(); + } +} + +``` + +下面是使用缓存对象来获取请求体或响应体的代码段,在你需要的地方使用就可以了: + +```java +import org.apache.commons.io.IOUtils; +// -------------------------------------------- +/** + * 获取请求体 + * + * @param request 请求对象 + * @return 请求体 + */ +private String getRequestBody(HttpServletRequest request) { + String requestBody = ""; + ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); + if (wrapper != null) { + requestBody = IOUtils.toString(wrapper.getContentAsByteArray(), StandardCharsets.UTF_8.toString()); + } + return requestBody; +} + +/** + * 获取响应体 + * + * @param response 响应对象 + * @return 响应体 + */ +private String getResponseBody(HttpServletResponse response) { + String responseBody = ""; + ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class); + if (wrapper != null) { + responseBody = IOUtils.toString(wrapper.getContentAsByteArray(), StandardCharsets.UTF_8.toString()); + } + return responseBody; +} +``` +