一、问题背景
HTTP 请求体通常包含客户端发送给服务器的数据,例如 JSON、XML 或表单数据。在大多数情况下,开发者需要在拦截器或过滤器中读取请求体的内容,进行一些额外的操作,比如日志记录、权限验证或校验。然而,由于 HttpServletRequest 提供的 getInputStream() 方法只能读取流一次,一旦读取了请求流,流就会被消耗,后续的业务逻辑无法再次访问请求体的数据。
如果没有适当的解决方案,就会出现如下问题:
- 在拦截器或过滤器中读取请求体后,无法在控制器或服务层获取请求体数据。
- 由于流的消耗,可能导致请求体的丢失,影响后续的业务逻辑处理。
二、解决方案:使用 HttpServletRequestWrapper
要解决这个问题,我们可以通过自定义 HttpServletRequestWrapper 来缓存请求体内容。通过这种方式,我们可以在拦截器或过滤器中读取请求体并将其缓存起来,之后可以通过自定义的 InputStream 重新提供请求体内容,确保请求体可以多次读取。
2.1 创建自定义 HttpServletRequestWrapper
首先,我们需要创建一个自定义的 HttpServletRequestWrapper 类,用于缓存请求体内容,并提供重新读取流的能力。HttpServletRequestWrapper 允许我们覆盖 getInputStream() 方法,以便返回缓存的请求体数据。
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.ServletInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public class CachedHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] requestBody;
public CachedHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 读取请求体并缓存
InputStream inputStream = request.getInputStream();
this.requestBody = inputStream.readAllBytes();
}
@Override
public ServletInputStream getInputStream() throws IOException {
// 返回缓存的请求体数据
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
}
}
在 CachedHttpServletRequestWrapper 中,我们将请求体读取并缓存到一个字节数组中。接着,getInputStream() 方法会返回一个新的 ServletInputStream,它基于缓存的字节数组提供请求体内容。这就确保了请求体在后续的处理过程中仍然可用。
2.2 创建 Filter 类来包装请求
接下来,我们需要创建一个 Filter,该 Filter 会拦截所有请求,并在需要时将原始的 HttpServletRequest 包装为 CachedHttpServletRequestWrapper。这样,后续的代码就可以通过包装后的请求对象访问到缓存的请求体数据。
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebFilter("/*")
public class RequestWrapperFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 仅处理 POST 请求(你可以根据需求扩展到其他 HTTP 方法)
if ("POST".equalsIgnoreCase(httpRequest.getMethod())) {
CachedHttpServletRequestWrapper wrappedRequest = new CachedHttpServletRequestWrapper(httpRequest);
chain.doFilter(wrappedRequest, response); // 将包装后的请求传递给下游
} else {
chain.doFilter(request, response); // 对其他请求不做处理
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void destroy() {}
}
在这个 Filter 中,我们对每个进入的请求进行检查。如果请求是 POST 请求,就会将其包装为 CachedHttpServletRequestWrapper,这样请求体就可以缓存并在后续读取。如果请求是其他 HTTP 方法(如 GET 或 PUT),则直接传递给下游处理。
2.3 业务层读取请求体
在控制器或服务层,你可以通过普通的 HttpServletRequest 来访问请求体数据。由于请求体已被缓存,你可以像通常一样调用 getInputStream() 方法来读取请求体内容。
@RestController
public class MyController {
@PostMapping("/process")
public ResponseEntity<String> processRequest(HttpServletRequest request) throws IOException {
// 获取缓存的请求体内容
String requestBody = new String(request.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
System.out.println("Request Body: " + requestBody);
// 进一步处理请求...
return ResponseEntity.ok("Request processed successfully");
}
}
在控制器中,我们可以通过 HttpServletRequest 的 getInputStream() 获取请求体内容。由于请求体已经被缓存,getInputStream() 方法将始终返回缓存的数据。
三、最佳实践与性能考虑
-
缓存大小与内存管理:
在处理大文件上传或大量请求数据时,缓存请求体内容可能会消耗大量内存。建议仅在请求体数据较小或请求数据较少时使用此方案。如果需要处理大文件上传,可以考虑使用流式处理或将请求体内容暂时存储在磁盘中。 -
避免多次读取请求体:
为了避免不必要的性能开销,尽量减少对请求体的多次读取。在设计时,可以考虑缓存请求体并在需要的地方一次性读取,而不是频繁调用getInputStream()。 -
请求体内容的类型:
如果请求体类型是 JSON 或 XML 等结构化数据,建议将读取到的字节流解析为对象,并将对象传递给下游服务,以减少每次读取请求体的开销。
四、总结
通过使用 HttpServletRequestWrapper,我们能够在拦截器或过滤器中缓存 HTTP 请求体的内容,并允许后续的业务逻辑多次读取该请求体。这种方法有效解决了请求流只能读取一次的问题,尤其在日志记录、请求校验等场景中非常有用。
需要注意的是,这种方法在请求体较大时可能会带来性能问题,因此要根据实际场景选择合适的方案。
1684

被折叠的 条评论
为什么被折叠?



