InputStream其实是一个数据通道,只负责数据的流通,并不负责数据的处理和存储等其他工作。但是在有的场合中,我们需要重复利用InputStream的数据,比如:
对于文件流,我们可能需要先读取InputStream中的前一些字节来判断文件的编码方式或者内容格式等,然后再具体地使用
对于一个请求,我们可能需要对请求的数据流进行重复读取。
根据输入流的来源的不同,有以下方式来实现重复读取。
主动获取流
即我们可以对流的来源进行主动控制,或者说我们可以主动地去获取流,这种情况,可以“曲线救国”,通过再次获取输入的方式达到重复读输入流的目的。比如,我们要读取一个文件,且可以直接从该文件获取流,则可以这样来“重复读”:
InputStream inputStream = new FileInputStream(path);
//利用inputStream
inputStream = new FileInputStream(path);
//再次利用inputStream
被动使用流
这种情况是指,我们无法从源来获取输入流,只能获取到一个输入流,如下所示,doSomething(InputStream is)是别人提供的一个接口方法:
public void doSomething(InputStream is){
//use InputStream here
}
这种情况其实是真正需要重复读的情况。有两种方案:一是使用缓存,而是通过mark和reset方法。
1、使用缓存
public class InputStreamCacher {
/**
* 将InputStream中的字节保存到ByteArrayOutputStream中。
*/
private ByteArrayOutputStream byteArrayOutputStream = null;
public InputStreamCacher(InputStream inputStream) {
if (ObjectUtils.isNull(inputStream))
return;
byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
try {
while ((len = inputStream.read(buffer)) > -1 ) {
byteArrayOutputStream.write(buffer, 0, len);
}
byteArrayOutputStream.flush();
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
}
public InputStream getInputStream() {
if (ObjectUtils.isNull(byteArrayOutputStream))
return null;
return new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
}
}
//使用
public void doSomething(InputStream is){
InputStreamCacher cacher = new InputStreamCacher(is);
InputStream stream = cacher.getInputStream();
//读取stream
stream = cacher.getInputStream();
}
对于实际的请求,可以使用这样的方式来实现重复读取:
public class RepeatedlyReadRequestWrapper extends HttpServletRequestWrapper {
private Logger logger = LoggerFactory.getLogger(RepeatedlyReadRequestWrapper.class);
private static final int BUFFER_START_POSITION = 0;
private static final int CHAR_BUFFER_LENGTH = 1024;
/**
* 将input stream 缓存到body
*/
private final String body;
/**
* @param request {@link HttpServletRequest} object.
*/
public RepeatedlyReadRequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder stringBuilder = new StringBuilder();
InputStream inputStream = null;
try {
inputStream = request.getInputStream();
} catch (IOException e) {
logger.error("Error reading the request body…", e);
}
if (inputStream != null) {
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
char[] charBuffer = new char[CHAR_BUFFER_LENGTH];
int bytesRead;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, BUFFER_START_POSITION, bytesRead);
}
} catch (IOException e) {
logger.error("Fail to read input stream", e);
}
} else {
stringBuilder.append("");
}
body = stringBuilder.toString();
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
return new DelegatingServletInputStream(byteArrayInputStream);
}
}
这种缓存的方式,其实是将输入流对应的源数据直接以字节的形式缓存到内存中,如果数据量比较大,则占用内存较多。
2、使用mark和reset方法
//InputStream是否支持mark,默认不支持
public boolean markSupported() {
return false;
}
//mark接口,该接口在InputStream中默认实现不做任何事情。
public synchronized void mark(int readlimit) {}
//reset接口,该接口在InputStream中实现,调用就会抛异常。
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
调用mark方法,会记下当前调用mark方法的时刻,InputStream被读到的位置
对于BufferedInputStream,readlimit表示:InputStream调用mark方法的时刻起,在读取readlimit个字节之前,标记的该位置是有效的。如果读取的字节数大于readlimit,可能标记的位置会失效。
对于ByteArrayInputStream,readlimit参数没有用,调用mark方法的时候写多少都无所谓。
对于常见的InputStream实现类,FileInputStream不支持mark,BufferInputStream及其父类FilterInputStream支持。
参考
重复读取InputStream的方法
通过mark和reset方法重复利用InputStream
输入流InputStream的reset()和mark()方法注意事项