新增:《解决无法重复读取请求体和响应体的问题》
This commit is contained in:
@@ -5,6 +5,7 @@ date: 2022/08/31 22:39
|
||||
categories:
|
||||
- Bug万象集
|
||||
tags:
|
||||
- Java
|
||||
- "Spring Boot"
|
||||
- "Spring Cloud"
|
||||
- "Open Feign"
|
||||
|
182
docs/categories/issues/2022/09/23/解决无法重复读取请求体和响应体的问题.md
Normal file
182
docs/categories/issues/2022/09/23/解决无法重复读取请求体和响应体的问题.md
Normal file
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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 <code>getWriter</code> 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;
|
||||
|
||||
/**
|
||||
* 缓存请求体和响应体过滤器
|
||||
*
|
||||
* <p>
|
||||
* 由于 requestBody 和 responseBody 分别对应的是 InputStream 和 OutputStream,由于流的特性,读取完之后就无法再被使用了。
|
||||
* 所以,需要额外缓存一次流信息。
|
||||
* </p>
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
```
|
||||
|
Reference in New Issue
Block a user