问题描述
在基于spring开发java项目时,可能需要重复读取http请求体中的数据,例如使用拦截器打印入参信息等,但当我们重复调用getinputstream()或者getreader()时,通常会遇到类似以下的错误信息:

大体的意思是当前request的getinputstream()已经被调用过了。那为什么会出现这个问题呢?
原因分析
主要原因有两个,一是java自身的设计中,inputstream作为数据管道本身只支持读取一次,如果要支持重复读取的话就需要重新初始化;二是servlet容器中request的实现问题,我们以默认的tomcat为例,可以发现在request有两个boolean类型的属性,分别是usingreader和usinginputstream,当调用getinputstream()或getreader()时会分别检查两个属性的值,并在执行后将对应的属性设置为true,如果在检查时变量的值已经为true了,那么就会报出以上错误信息。

解决方案
不太可行的方案:简单粗暴的反射机制
涉及到变量的修改,我们首先想到的就是有没有提供方法进行修改,不过可惜的是usingreader和usinginputstream并未提供,所以想要在使用过程中修改这两个属性估计只能靠反射了,在使用过程中每次调用后通过反射将usingreader和usinginputstream设置为false,每次根据读取出的内容把数据流初始化回去,理论上就可以再次读取了。
首先说反射机制本身就是通过破坏类的封装来实现动态修改的,有点过于粗暴了,其次也是主要原因,我们只能针对我们自己实现的代码进行处理,框架本身如果调用getinputstream()和getreader()的话,我们就没法通过这个办法干预了,所以这个方案在给予spring的web项目中并不可行。
理论上可行的方案:httpservletrequest接口
httpservletrequest是一个接口,理论上我们只需要创建一个实现类就可以自定义getinputstream()和getreader()的行为,自然也就能解决requestbody不能重复读取的问题,但这个方案的问题在于httpservletrequest有70个方法,而我们只需要修改其中两个而已,通过这种方式去解决有点得不偿失。
部分场景可行的方案:contentcachingrequestwrapper
spring本身提供了一个request包装类来处理重复读取的问题,即contentcachingrequestwrapper,其实现思路就是在读取requestbody时将内存缓存到它内部的一个字节流中,后续读取可以通过调用getcontentasstring()或getcontentasbytearray()获取到缓存下来的内容。
之所以说这个方案是部分场景可行主要是两个方面,一是contentcachingrequestwrapper没有重写getinputstream()和getreader()方法,所以框架中使用这两个方法的地方依然获取不到缓存下来的内容,仅支持自定义的业务逻辑;第二点和第一点有所关联,因为其没有修改getinputstream()和getreader()方法,所以我们在使用时只能在使用requestbody注解后使用contentcachingrequestwrapper,否则就会出现requestbody注解修饰的参数无法正常读取请求体的问题,也就限定了它的使用范围如下图所示:

如果仅需要在业务代码后再次读取请求体内容,那么使用contentcachingrequestwrapper也足以满足需求,具体使用方法请参考下一节的说明。
目前的最佳实践:继承httpservletrequestwrapper
之前我们提到实现httpservletrequest需要实现70个方法,所以不太可能自行实现,这个方案算是进阶版本,继承httpservletrequest的实现类,之后再自定义我们需要修改的两个方法。
httpservletrequest作为一个接口,肯定会有其实现去支撑它的业务功能,因为servlet容器的选择较多,我们也不能使用某一方提供的实现,所以选择的范围也就被限制到了java ee(现在叫jakarta ee)标准范围内,通过查看httpservletrequest的实现,可以发现在标准内提供了一个包装类:httpservletrequestwrapper,我们的方案也是围绕它展开。
思路简述
- 自定义子类,继承httpservletrequestwrapper,在子类的构造方法中将requestbody缓存到自定义的属性中。
- 自定义getinputstream()和getreader()的业务逻辑,不再校验usingreader和usinginputstream,且在调用时读取缓存下来的内容。
- 自定义filter,将默认的httpservletrequest替换为自定义的包装类。
代码展示
- 继承httpservletrequestwrapper,实现子类customrequestwrapper,并自定义getinputstream()和getreader()的业务逻辑
// 1.继承httpservletrequestwrapper
public class customrequestwrapper extends httpservletrequestwrapper {
// 2.定义final属性,用于缓存请求体内容
private final byte[] content;
public customrequestwrapper(httpservletrequest request) throws ioexception {
super(request);
// 3.构造方法中将请求体内容缓存到内部属性中
this.content = streamutils.copytobytearray(request.getinputstream());
}
// 4.重新getinputstream()
@override
public servletinputstream getinputstream() {
// 5.将缓存下来的内容转换为字节流
final bytearrayinputstream bytearrayinputstream = new bytearrayinputstream(content);
return new servletinputstream() {
@override
public boolean isfinished() {
return false;
}
@override
public boolean isready() {
return false;
}
@override
public void setreadlistener(readlistener listener) {
}
@override
public int read() {
// 6.读取时读取第5步初始化的字节流
return bytearrayinputstream.read();
}
};
}
// 7.重写getreader()方法,这里复用getinputstream()的逻辑
@override
public bufferedreader getreader() {
return new bufferedreader(new inputstreamreader(getinputstream()));
}
}
- 自定义filter将默认的httpservletrequest替换为自定义的customrequestwrapper
// 1.实现filter接口,此处也可以选择继承httpfilter
public class requestwrapperfilter implements filter {
// 2. 重写或实现dofilter方法
@override
public void dofilter(servletrequest request, servletresponse response, filterchain chain) throws ioexception, servletexception {
// 3.此处判断是为了缩小影响范围,本身customrequestwrapper只是针对httpservletrequest,不进行判断可能会影响其他类型的请求
if (request instanceof httpservletrequest) {
// 4.将默认的httpservletrequest转换为自定义的customrequestwrapper
customrequestwrapper requestwrapper = new customrequestwrapper((httpservletrequest) request);
// 5.将转换后的request传递至调用链中
chain.dofilter(requestwrapper, response);
} else {
chain.dofilter(request, response);
}
}
}
- 将filter注册到spring容器,这一步可以通过多种方式执行,这里采用比较传统但比较灵活的bean方式注册,如果图方便可以通过servletcomponentscan注解+ webfilter注解的方式。
/**
* 过滤器配置,支持第三方过滤器
*/
@configuration
public class filterconfigure {
/**
* 请求体封装
* @return
*/
@bean
public filterregistrationbean<requestwrapperfilter> filterregistrationbean(){
filterregistrationbean<requestwrapperfilter> bean = new filterregistrationbean<>();
bean.setfilter(new requestwrapperfilter());
bean.addurlpatterns("/*");
return bean;
}
}
至此我们就可以在项目中重复读取请求体了,如果选择使用spring提供的contentcachingrequestwrapper,那么在filter中将customrequestwrapper替换为contentcachingrequestwrapper即可,不过需要注意在上一节提到的可用范围较小的问题。
以上就是springboot项目中http请求体只能读一次的解决方案的详细内容,更多关于springboot http请求只读一次的资料请关注代码网其它相关文章!
发表评论