ServletRequest一旦读取了流,流就关闭了,流中的数据一旦被消费就不能再次从流中读取了,Servlet 是如何还能够从ServletRequest流中读取数据的

是的,在默认情况下,一旦 ServletRequest 的输入流(InputStreamReader)被读取,流就被标记为已消费,数据也无法再次读取。这是因为 ServletRequest 的输入流基于 HTTP 请求的字节流实现,读取数据后,流会关闭或标记为已消费状态,从而阻止重复读取。

如何解决无法重复读取流的问题

在某些情况下,我们可能需要在多个地方处理请求数据(如身份验证、日志记录或业务逻辑等),那么可以通过以下几种方式来解决流不能重复读取的问题:

1. 使用 HttpServletRequestWrapper 缓存请求数据

可以创建一个自定义的 HttpServletRequestWrapper,在请求第一次被读取时缓存流中的数据,以便后续可以多次读取。

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;

public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
    private byte[] cachedBody;

    public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        InputStream requestInputStream = request.getInputStream();
        this.cachedBody = requestInputStream.readAllBytes();
    }

    @Override
    public ServletInputStream getInputStream() {
        return new CachedBodyServletInputStream(cachedBody);
    }

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

    private static class CachedBodyServletInputStream extends ServletInputStream {
        private final ByteArrayInputStream buffer;

        public CachedBodyServletInputStream(byte[] cachedBody) {
            this.buffer = new ByteArrayInputStream(cachedBody);
        }

        @Override
        public int read() {
            return buffer.read();
        }

        @Override
        public boolean isFinished() {
            return buffer.available() == 0;
        }

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

        @Override
        public void setReadListener(ReadListener listener) {
            throw new UnsupportedOperationException();
        }
    }
}

在这个自定义的 HttpServletRequestWrapper 中,流第一次被读取后缓存到 cachedBody 字节数组,后续可以多次读取。使用时,在 FilterServlet 中用包装后的 HttpServletRequest 替换原始请求对象。

2. 在过滤器中替换 HttpServletRequest

可以在过滤器中将原始请求替换为 CachedBodyHttpServletRequest 实例,以便后续在 Servlet 中可以重复读取流。

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class RequestBodyCachingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (request instanceof HttpServletRequest) {
            HttpServletRequest cachedRequest = new CachedBodyHttpServletRequest((HttpServletRequest) request);
            chain.doFilter(cachedRequest, response);
        } else {
            chain.doFilter(request, response);
        }
    }
}

总结

  • 默认情况下,ServletRequest 的输入流读取后便不可再读。
  • 可以使用 HttpServletRequestWrapper 来缓存请求体内容。
  • 使用缓存后的请求对象即可实现流的重复读取。

在 Java Web 应用中,Filter 中读取 HttpServletRequest 的输入流通常会导致流被“消费”,从而在后续的 Servlet 中无法再次读取。为了使 HttpServletRequestFilterServlet 中都能够读取,通常使用缓存技术,即通过将请求数据存储在内存中来实现流的重复读取。这是因为 HttpServletRequest 默认的流是一次性读取的,原始数据被消费后便不能再次获取。

如果在 Filter 中读取 HttpServletRequest 后在 Servlet 中仍然可以读取流,通常是以下几种情况之一:

1. 使用了包装类(HttpServletRequestWrapper)缓存请求数据

这是最常用的解决方案。通过自定义一个 HttpServletRequestWrapper,在流被读取时,将数据缓存为字节数组。这样一来,每次请求读取时都可以从缓存中返回新的流,避免了流被“消费”后无法再次读取的问题。

实现步骤大致如下:

  • Filter 中创建包装类的实例。
  • 缓存请求体,并将包装类传递到后续的 FilterChain 中。
  • Servlet 中,包装类的 getInputStream()getReader() 方法会返回新的流对象,从而实现流的重复读取。

示例包装类代码见前面的回答。

2. 使用了 Spring 框架

在 Spring MVC 中,RequestBody 也可以通过 FilterServlet 重复读取。Spring 内部使用了 ContentCachingRequestWrapper 来包装 HttpServletRequest,其中的请求体被缓存到字节数组,以实现流的重复读取。例如,Spring 的 OncePerRequestFilter 和其他一些过滤器会自动包装 HttpServletRequest

要启用 ContentCachingRequestWrapper,可以在项目中引入 Spring MVC,然后在 Filter 中手动创建或让 Spring 自动管理。例如:

import org.springframework.web.util.ContentCachingRequestWrapper;

public class MyFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
        filterChain.doFilter(wrappedRequest, response);

        // 读取请求体
        byte[] requestBody = wrappedRequest.getContentAsByteArray();
        // 进一步处理 requestBody
    }
}

3. 使用支持多次读取的 Servlet 容器

某些 Servlet 容器在实现时支持多次读取请求流,但这是容器实现的特性,通常不能依赖容器支持来解决该问题。例如,某些定制化的 Servlet 容器会自动缓存请求体,但这不适用于所有环境,也不适用于所有容器。

总结

  • 默认情况下,读取 HttpServletRequest 的输入流是一次性的。
  • 可以通过 HttpServletRequestWrapper 来缓存请求体,从而在 FilterServlet 中重复读取流。
  • Spring MVC 的 ContentCachingRequestWrapper 是实现流重复读取的常用方案。
posted @ 2024-10-28 00:04  gongchengship  阅读(109)  评论(0编辑  收藏  举报