使用 HttpServletRequestWrapper 实现 HTTP 请求流多次读取

一、问题背景

HTTP 请求体通常包含客户端发送给服务器的数据,例如 JSON、XML 或表单数据。在大多数情况下,开发者需要在拦截器或过滤器中读取请求体的内容,进行一些额外的操作,比如日志记录、权限验证或校验。然而,由于 HttpServletRequest 提供的 getInputStream() 方法只能读取流一次,一旦读取了请求流,流就会被消耗,后续的业务逻辑无法再次访问请求体的数据。

如果没有适当的解决方案,就会出现如下问题:

  1. 在拦截器或过滤器中读取请求体后,无法在控制器或服务层获取请求体数据。
  2. 由于流的消耗,可能导致请求体的丢失,影响后续的业务逻辑处理。

二、解决方案:使用 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 方法(如 GETPUT),则直接传递给下游处理。

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");
    }
}

在控制器中,我们可以通过 HttpServletRequestgetInputStream() 获取请求体内容。由于请求体已经被缓存,getInputStream() 方法将始终返回缓存的数据。


三、最佳实践与性能考虑

  1. 缓存大小与内存管理
    在处理大文件上传或大量请求数据时,缓存请求体内容可能会消耗大量内存。建议仅在请求体数据较小或请求数据较少时使用此方案。如果需要处理大文件上传,可以考虑使用流式处理或将请求体内容暂时存储在磁盘中。

  2. 避免多次读取请求体
    为了避免不必要的性能开销,尽量减少对请求体的多次读取。在设计时,可以考虑缓存请求体并在需要的地方一次性读取,而不是频繁调用 getInputStream()

  3. 请求体内容的类型
    如果请求体类型是 JSON 或 XML 等结构化数据,建议将读取到的字节流解析为对象,并将对象传递给下游服务,以减少每次读取请求体的开销。


四、总结

通过使用 HttpServletRequestWrapper,我们能够在拦截器或过滤器中缓存 HTTP 请求体的内容,并允许后续的业务逻辑多次读取该请求体。这种方法有效解决了请求流只能读取一次的问题,尤其在日志记录、请求校验等场景中非常有用。

需要注意的是,这种方法在请求体较大时可能会带来性能问题,因此要根据实际场景选择合适的方案。


参考资料

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值