Spring中多次读取HttpServletRequest

2023/05/19

1. 概述

在本教程中,我们学习如何使用Spring多次从HttpServletRequest读取请求体。

HttpServletRequest是一个接口,它公开getInputStream()方法来读取请求正文。 默认情况下,来自此InputStream中的数据只能读取一次。

2. Maven依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.6.1</version>
</dependency>

3. Spring中的ContentCachingRequestWrapper

Spring提供了一个ContentCachingRequestWrapper类,此类包含了一个方法getContentAsByteArray()来多次读取请求正文。

但是,这个类有一个限制:我们不能使用getInputStream()和getReader()方法多次读取请求正文

它通过使用InputStream来缓存请求正文,如果我们在其中一个过滤器中读取InputStream,那么过滤器链中的其他后续过滤器将无法再读取它。 由于这个限制,这个类并不适用于所有情况。

因此,让我们看一个更通用的解决方案。

4. 继承HttpServletRequest

我们创建一个CachedBodyHttpServletRequest类,它继承了HttpServletRequestWrapper。 这样,我们就不需要重写HttpServletRequest接口的所有抽象方法。

HttpServletRequestWrapper类有两个抽象方法getInputStream()和getReader(),我们将重写这两个方法并创建一个新的构造函数。

4.1 构造函数

首先,让我们创建一个构造函数,在它内部,我们从实际的InputStream中读取请求正文并将其存储在byte[]对象中:

public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {

    private final byte[] cachedBody;

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

因此,我们能够通过cachedBody数组多次读取请求正文。

4.2 getInputStream()

接下来,我们重写getInputStream()方法,此方法用于读取原始请求体并将其转换为对象。

在这个方法中,我们创建并返回一个新的CachedBodyServletInputStream类对象(ServletInputStream的一个实现)

public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return new CachedBodyServletInputStream(this.cachedBody);
    }
}

4.3 getReader()

然后,我们重写getReader()方法,此方法返回一个BufferedReader对象:

public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {

    @Override
    public BufferedReader getReader() throws IOException {
        // Create a reader from cachedContent and return it
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
        return new BufferedReader(new InputStreamReader(byteArrayInputStream));
    }
}

5. ServletInputStream实现

我们创建一个类CachedBodyServletInputStream,它将实现ServletInputStream。 在这个类中,我们创建一个新的构造函数并重写isFinished()、isReady()和read()方法。

5.1 构造函数

首先,我们创建一个接收字节数组作为参数的新构造函数。

在它内部,使用该字节数组创建一个新的ByteArrayInputStream实例。 之后,我们将其分配给全局变量cachedBodyInputStream:

public class CachedBodyServletInputStream extends ServletInputStream {

    private final InputStream cachedBodyInputStream;

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

5.2 read()

然后重写read()方法,在这个方法中,我们将调用ByteArrayInputStream#read:

public class CachedBodyServletInputStream extends ServletInputStream {

    @Override
    public int read() throws IOException {
        return cachedBodyInputStream.read();
    }
}

5.3 isFinished()

然后重写isFinished()方法,该方法指示InputStream是否有更多数据可供读取。 当有效数据的字节数为0时,它返回true:

public class CachedBodyServletInputStream extends ServletInputStream {

    @Override
    public boolean isFinished() {
        try {
            return cachedBodyInputStream.available() == 0;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
}

5.4 isReady()

最后重写isReady()方法,该方法指示InputStream是否可以读取。

由于我们已经将InputStream复制到一个字节数组中,因此我们返回true,表示数据始终可用:

public class CachedBodyServletInputStream extends ServletInputStream {

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

6. 过滤器

最后,我们创建一个新的过滤器来使用CachedBodyHttpServletRequest类。 在这里,我们继承Spring的OncePerRequestFilter类,这个类有一个抽象方法doFilterInternal()。

在此方法中,我们从实际请求对象创建CachedBodyHttpServletRequest类的对象

@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
@WebFilter(filterName = "ContentCachingFilter", urlPatterns = "/*")
public class ContentCachingFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        System.out.println("IN  ContentCachingFilter ");
        CachedBodyHttpServletRequest cachedBodyHttpServletRequest = new CachedBodyHttpServletRequest(request);
        filterChain.doFilter(cachedBodyHttpServletRequest, response);
    }
}

然后,我们将这个新的请求包装器对象传递给过滤器链。 因此,对getInputStream()方法的所有后续调用都将调用被重写的方法。

7. 总结

在本教程中,我们介绍了如何通过继承HttpServletRequest类来将原始请求对象封装为一个可多次读取请求体的请求对象。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章