ContentCachingRequestWrapper解决入参读取

如果在处理入参的时候发生了序列化等问题,在执行我们逻辑代码之前就会抛异常给总异常控制器。如果在总异常控制器通过inputStream读数据,是无法读到的,因为这个inputStream只支持读一次,无论是否有异常,都不会读到入参的值。这个时候有一个办法就是ContentCachingRequestWrapper

本文就带你一探究竟,学习ContentCachingRequestWrapper源码,并且理解其设计思路。

还等啥,go!

一、使用

使用这个类需要两个步骤,第一个是配置过滤器

@Component
public class RequestCachingFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        filterChain.doFilter(new ContentCachingRequestWrapper(httpServletRequest), httpServletResponse);
    }
}

这个过滤器很简单,就是把httpServeletRequest包装成ContentCachingRequestWrapper类型继续往下传。

第二步就是在使用的使用用方法getContentAsByteArray获取入参值。

if(req != null && req instanceof ContentCachingRequestWrapper){
                ContentCachingRequestWrapper wrapper =(ContentCachingRequestWrapper) req;
                log.error("request_body:{}", StringUtils.toEncodedString(wrapper.getContentAsByteArray(), Charset.forName(wrapper.getCharacterEncoding()))
                );
            }

这样你就获取到来自客户端请求的入参。注意inputStream只能读一次流就会被关闭,不会再被读取,所以如果在序列化等过程中读过一次inputStream,之后就不要尝试自己再读,会抛异常,无法读取数据。

好了,你看ContentCachingRequestWrapper的使用是不是要多简单有多简单!接下来我们继续分析它做了什么让这件事儿这么简单。

二、源码走读

我们来看整个过程

1、包装ContentCachingRequestWrapper

public ContentCachingRequestWrapper(HttpServletRequest request) {
        super(request);
        int contentLength = request.getContentLength();
        this.cachedContent = new ByteArrayOutputStream(contentLength >= 0 ? contentLength : 1024);
        this.contentCacheLimit = null;
    }

包装ContentCachingRequestWrapper,主要就做了一件事儿,感知request里面内容的长度,为自己开一个同样大小的输出流cachedContent。

2、覆盖方法getInputStream

@Override
	public ServletInputStream getInputStream() throws IOException {
		if (this.inputStream == null) {
			this.inputStream = new ContentCachingInputStream(getRequest().getInputStream());
		}
		return this.inputStream;
	}

这个方法在spring框架在解析输入流的时候读取。读取该方法,我们再做一次“移花接木”,将inputStream包装成ContentCachingInputStream类型。

3、覆盖java.io.InputStream#read()方法

@Override
		public int read() throws IOException {
			int ch = this.is.read();
			if (ch != -1 && !this.overflow) {
				if (contentCacheLimit != null && cachedContent.size() == contentCacheLimit) {
					this.overflow = true;
					handleContentOverflow(contentCacheLimit);
				}
				else {
					cachedContent.write(ch);
				}
			}
			return ch;
		}

                                                                        调用栈

在EmptyBodyCheckingHttpInputMessage将inputStream对象包装成PushbackInputStream会调用inputStream的read方法,该方法会将第一个int类型写入cachedContent当中。

4、覆盖方法inputStream中的read(byte b[], int off, int len)

@Override
		public int read(final byte[] b, final int off, final int len) throws IOException {
			int count = this.is.read(b, off, len);
			writeToCache(b, off, count);
			return count;
		}

在调用该方法的时候先不直接返回,先将数据放入输出流缓存cachedContent当中,然后再返回给调用栈前一个方法。

三、小结

ContentCachingRequestWrapper通过覆盖inpuStream方法,将每次调用inputStream读的时候都将其放置到输出流当中。这样后续inputStream流被关闭了,我们还可以通过使用cachedContent内容读取入参内容。

四、疑问

①、可不可以通过reset重读流?

即使流没有关闭也不支持reset操作,可以省省了。(error:Resetting to invalid mark)

②、什么时候流关闭的?

在没有ContentCachingRequestWrapper包装器的时候,例如在json序列化的过程中读取了流数据,并且关闭了流。

java7中添加的新特性 try-with-resources 语法。当一个资源类实现了AutoCloseable接口会在以下两种情况下自动释放资源:

1、代码块抛异常,JVM会自动调用close 方法进行资源释放。

2、代码块正常退出。

JsonParser接口实现了Closeable接口,并且从使用上来看,JsonParser对象也是使用了try-with-resources 语法实现了资源的自动释放。

protected Object _readMapAndClose(JsonParser p0, JavaType valueType)
        throws IOException
    {
        try (JsonParser p = p0) {
            final Object result;
            final DeserializationConfig cfg = getDeserializationConfig();
            final DefaultDeserializationContext ctxt = createDeserializationContext(p, cfg);
            JsonToken t = _initForReading(p, valueType);
            if (t == JsonToken.VALUE_NULL) {
                // Ask JsonDeserializer what 'null value' to use:
                result = _findRootDeserializer(ctxt, valueType).getNullValue(ctxt);
            } else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) {
                result = null;
            } else {
                result = ctxt.readRootValue(p, valueType,
                        _findRootDeserializer(ctxt, valueType), null);
                ctxt.checkUnresolvedObjectId();
            }
            if (cfg.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) {
                _verifyNoTrailingTokens(p, ctxt, valueType);
            }
            return result;
        }
    }

                                                资源在jackson解析时被关闭

ps:如果你debug,这个close会莫名其妙被调用。这是因为jvm底层调用的,所以你看不到其之前的代码。凡是你发现自己跟见鬼了一样,莫名其妙调用了某个方法很可能是jvm的机制,要多多查找相关资料。

        由于json序列化在错误拦截器之前所以进入拦截器的时候流处于关闭状态

        在                                        错误处理代码中流已经被关闭

③、在有ContentCachingRequestWrapper包装下,居然流没有走关闭动作。这又是为啥呢?

还记得在ContentCachingRequestWrapper获取流的方法吗?

@Override
	public ServletInputStream getInputStream() throws IOException {
		if (this.inputStream == null) {
			this.inputStream = new ContentCachingInputStream(getRequest().getInputStream());
		}
		return this.inputStream;
	}

        在有拦截器的时候,serveletInputStream被ContentCachingRequestWrapper包装,所以传入jackson序列化之后调用的也是ContentCachingRequestWrapper的close方法, 而ContentCachingRequestWrapper并没有主动实现close方法关闭servletInputStream所以servletInputStream就一直处于未关闭状态,当然了,最终servletInputStream还会在tomcat的service()最终处理阶段被关闭,不会造成内存溢出。

                PushbackInputStream实现了close方法将传入的serveletInputStream关闭。

                        到了错误处理方法servletInputStream流已经显示关闭

传入PushbackInputStream的是ContentCachingRequestWrapper,其并未主动实现流关闭

                        到了异常处理类包装的servletInputStream并没有被关闭。

④、未关闭的ContentCachingRequestWrapper中serveletInputStream还能再读取了么?

并不能,因为markPos=-1,并且pos==limit所以我们没有办法重放流。换句话说网络就一旦读取一次就再不能重放了。

参考文献

浅谈 Java 中的 AutoCloseable 接口 - vivo互联网技术 - 博客园

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值