解决HttpServletRequest的输入流只能读取一次的问题.docx
- 文档编号:14328610
- 上传时间:2023-06-22
- 格式:DOCX
- 页数:15
- 大小:122.16KB
解决HttpServletRequest的输入流只能读取一次的问题.docx
《解决HttpServletRequest的输入流只能读取一次的问题.docx》由会员分享,可在线阅读,更多相关《解决HttpServletRequest的输入流只能读取一次的问题.docx(15页珍藏版)》请在冰点文库上搜索。
解决HttpServletRequest的输入流只能读取一次的问题
解决HttpServletRequest的输入流只能读取一次的问题
背景
通常对安全性有要求的接口都会对请求参数做一些签名验证,而我们一般会把验签的逻辑统一放到过滤器或拦截器里,这样就不用每个接口都去重复编写验签的逻辑。
在一个项目中会有很多的接口,而不同的接口可能接收不同类型的数据,例如表单数据和json数据,表单数据还好说,调用request的getParameterMap就能全部取出来。
而json数据就有些麻烦了,因为json数据放在body中,我们需要通过request的输入流去读取。
但问题在于request的输入流只能读取一次不能重复读取,所以我们在过滤器或拦截器里读取了request的输入流之后,请求走到controller层时就会报错。
而本文的目的就是介绍如何解决在这种场景下遇到HttpServletRequest的输入流只能读取一次的问题。
注:
本文代码基于SpringBoot框架
HttpServletRequest的输入流只能读取一次的原因
我们先来看看为什么HttpServletRequest的输入流只能读一次,当我们调用getInputStream()方法获取输入流时得到的是一个InputStream对象,而实际类型是ServletInputStream,它继承于InputStream。
InputStream的read()方法内部有一个postion,标志当前流被读取到的位置,每读取一次,该标志就会移动一次,如果读到最后,read()会返回-1,表示已经读取完了。
如果想要重新读取则需要调用reset()方法,position就会移动到上次调用mark的位置,mark默认是0,所以就能从头再读了。
调用reset()方法的前提是已经重写了reset()方法,当然能否reset也是有条件的,它取决于markSupported()方法是否返回true。
InputStream默认不实现reset(),并且markSupported()默认也是返回false,这一点查看其源码便知:
我们再来看看ServletInputStream,可以看到该类没有重写mark(),reset()以及markSupported()方法:
综上,InputStream默认不实现reset的相关方法,而ServletInputStream也没有重写reset的相关方法,这样就无法重复读取流,这就是我们从request对象中获取的输入流就只能读取一次的原因。
使用HttpServletRequestWrapper+Filter解决输入流不能重复读取问题
既然ServletInputStream不支持重新读写,那么为什么不把流读出来后用容器存储起来,后面就可以多次利用了。
那么问题就来了,要如何存储这个流呢?
所幸JavaEE提供了一个HttpServletRequestWrapper类,从类名也可以知道它是一个http请求包装器,其基于装饰者模式实现了HttpServletRequest界面,部分源码如下:
从上图中的部分源码可以看到,该类并没有真正去实现HttpServletRequest的方法,而只是在方法内又去调用HttpServletRequest的方法,所以我们可以通过继承该类并实现想要重新定义的方法以达到包装原生HttpServletRequest对象的目的。
首先我们要定义一个容器,将输入流里面的数据存储到这个容器里,这个容器可以是数组或集合。
然后我们重写getInputStream方法,每次都从这个容器里读数据,这样我们的输入流就可以读取任意次了。
具体的实现代码如下:
packagecom.example.wrapperdemo.controller.wrapper;
importlombok.extern.slf4j.Slf4j;
importjavax.servlet.ReadListener;importjavax.servlet.ServletInputStream;importjavax.servlet.ServletRequest;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletRequestWrapper;importjava.io.*;importjava.nio.charset.Charset;
/**
*@author01
*@programwrapper-demo
*@description包装HttpServletRequest,目的是让其输入流可重复读
*@create2018-12-2420:
48
*@since1.0
**/@Slf4jpublicclassRequestWrapperextendsHttpServletRequestWrapper{
/**
*存储body数据的容器
*/
privatefinalbyte[]body;
publicRequestWrapper(HttpServletRequestrequest)throwsIOException{
super(request);
//将body数据存储起来
StringbodyStr=getBodyString(request);
body=bodyStr.getBytes(Charset.defaultCharset());
}
/**
*获取请求Body
*
*@paramrequestrequest
*@returnString
*/
publicStringgetBodyString(finalServletRequestrequest){
try{
returninputStream2String(request.getInputStream());
}catch(IOExceptione){
log.error("",e);
thrownewRuntimeException(e);
}
}
/**
*获取请求Bodyhttp:
//www.f-1.cc
*
*@returnString
*/
publicStringgetBodyString(){
finalInputStreaminputStream=newByteArrayInputStream(body);
returninputStream2String(inputStream);
}
/**
*将inputStream里的数据读取出来并转换成字符串
*
*@paraminputStreaminputStream
*@returnString
*/
privateStringinputStream2String(InputStreaminputStream){
StringBuildersb=newStringBuilder();
BufferedReaderreader=null;
try{
reader=newBufferedReader(newInputStreamReader(inputStream,Charset.defaultCharset()));
Stringline;
while((line=reader.readLine())!
=null){
sb.append(line);
}
}catch(IOExceptione){
log.error("",e);
thrownewRuntimeException(e);
}finally{
if(reader!
=null){
try{
reader.close();
}catch(IOExceptione){
log.error("",e);
}
}
}
returnsb.toString();
}
@Override
publicBufferedReadergetReader()throwsIOException{
returnnewBufferedReader(newInputStreamReader(getInputStream()));
}
@Override
publicServletInputStreamgetInputStream()throwsIOException{
finalByteArrayInputStreaminputStream=newByteArrayInputStream(body);
returnnewServletInputStream(){
@Override
publicintread()throwsIOException{
returninputStream.read();
}
@Override
publicbooleanisFinished(){
returnfalse;
}
@Override
publicbooleanisReady(){
returnfalse;
}
@Override
publicvoidsetReadListener(ReadListenerreadListener){
}
};
}
}
除了要写一个包装器外,我们还需要在过滤器里将原生的HttpServletRequest对象替换成我们的RequestWrapper对象,代码如下:
packagecom.example.wrapperdemo.controller.filter;
importcom.example.wrapperdemo.controller.wrapper.RequestWrapper;importlombok.extern.slf4j.Slf4j;
importjavax.servlet.*;importjavax.servlet.http.HttpServletRequest;importjava.io.IOException;
/**
*@author01
*@programwrapper-demo
*@description替换HttpServletRequest
*@create2018-12-2421:
04
*@since1.0
**/@Slf4jpublicclassReplaceStreamFilterimplementsFilter{
@Override
publicvoidinit(FilterConfigfilterConfig)throwsServletException{
log.info("StreamFilter初始化...");
}
@Override
publicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)throwsIOException,ServletException{
ServletRequestrequestWrapper=newRequestWrapper((HttpServletRequest)request);
chain.doFilter(requestWrapper,response);
}
@Override
publicvoiddestroy(){
log.info("StreamFilter销毁...");
}
}
然后我们就可以在拦截器中愉快的获取json数据也不慌controller层会报错了:
packagecom.example.wrapperdemo.controller.interceptor;
importcom.example.wrapperdemo.controller.wrapper.RequestWrapper;importlombok.extern.slf4j.Slf4j;importorg.springframework.http.MediaType;importorg.springframework.web.servlet.HandlerInterceptor;importorg.springframework.web.servlet.ModelAndView;
importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;
/**
*@author01
*@programwrapper-demo
*@description签名拦截器
*@create2018-12-2421:
08
*@since1.0
**/@Slf4jpublicclassSignatureInterceptorimplementsHandlerInterceptor{
@Override
publicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{
log.info("[preHandle]executing...requesturiis{}",request.getRequestURI());
if(isJson(request)){
//获取json字符串
StringjsonParam=newRequestWrapper(request).getBodyString();
log.info("[preHandle]json数据:
{}",jsonParam);
//验签逻辑...略...
}
returntrue;
}
@Override
publicvoidpostHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,ModelAndViewmodelAndView)throwsException{
}
@Override
publicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{
}
/**
*判断本次请求的数据类型是否为json
*
*@paramrequestrequest
*@returnboolean
*/
privatebooleanisJson(HttpServletRequestrequest){
if(request.getContentType()!
=null){
returnrequest.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)||
request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE);
}
returnfalse;
}
}
编写完以上的代码后,还需要将过滤器和拦截器在配置类中进行注册才会生效,过滤器配置类代码如下:
packagecom.example.wrapperdemo.config;
importcom.example.wrapperdemo.controller.filter.ReplaceStreamFilter;importorg.springframework.boot.web.servlet.FilterRegistrationBean;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;
importjavax.servlet.Filter;
/**
*@author01
*@programwrapper-demo
*@description过滤器配置类
*@create2018-12-2421:
06
*@since1.0
**/@ConfigurationpublicclassFilterConfig{
/**
*注册过滤器
*
*@returnFilterRegistrationBean
*/
@Bean
publicFilterRegistrationBeansomeFilterRegistration(){
FilterRegistrationBeanregistration=newFilterRegistrationBean();
registration.setFilter(replaceStreamFilter());
registration.addUrlPatterns("/*");
registration.setName("streamFilter");
returnregistration;
}
/**
*实例化StreamFilter
*
*@returnFilter
*/
@Bean(name="replaceStreamFilter")
publicFilterreplaceStreamFilter(){
returnnewReplaceStreamFilter();
}
}
拦截器配置类代码如下:
packagecom.example.wrapperdemo.config;
importcom.example.wrapperdemo.controller.interceptor.SignatureInterceptor;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.web.servlet.config.annotation.InterceptorRegistry;importorg.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
*@author01
*@programwrapper-demo
*@description
*@create2018-12-2421:
16
*@since1.0
**/@ConfigurationpublicclassInterceptorConfigimplementsWebMvcConfigurer{
@Bean
publicSignatureInterceptorgetSignatureInterceptor(){
returnnewSignatureInterceptor();
}
/**
*注册拦截器
*
*@paramregistryregistry
*/
@Override
publicvoidaddInterceptors(InterceptorRegistryregistry){
registry.addInterceptor(getSignatureInterceptor())
.addPathPatterns("/**");
}
}
接下来我们就可以测试一下在拦截器中读取了输入流后在controller层是否还能正常接收数据,首先定义一个实体类,代码如下:
packagecom.example.wrapperdemo.param;
importlombok.AllArgsConstructor;importlombok.Builder;importlombok.Data;importlombok.NoArgsConstructor;
/**
*@author01
*@programwrapper-demo
*@description
*@create2018-12-2421:
11
*@since1.0
**/@Data@Builder@NoArgsConstructor@AllArgsConstructorpublicclassUserParam{
privateStringuserName;
privateStringphone;
privateStringpassword;
}
然后写一个简单的Controller,代码如下:
packagecom.example.wrapperdemo.controller;
importcom.example.wrapperdemo.param.UserParam;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestBody;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;
/**
*@author01
*@programwrapper-demo
*@description
*@create2018-12-2420:
47
*@since1.0
**/@RestController@RequestMapping("/user")publicclassDemoController{
@PostMapping("/register")
publicUserParamregister(@RequestBodyUserParam
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 解决 HttpServletRequest 输入 只能 读取 一次 问题
![提示](https://static.bingdoc.com/images/bang_tan.gif)