如何缓存InputStream以供多种用途

我有一个文件的InputStream,我使用apache poi组件来读取它像这样:

POIFSFileSystem fileSystem = new POIFSFileSystem(inputStream); 

问题是我需要多次使用相同的流,POIFSFileSystem在使用后关闭流。

从输入流缓存数据然后将更多输入流提供给不同的POIFSFileSystem的最佳方法是什么?

编辑1:

通过缓存我的意思是存储供以后使用,而不是作为加速应用程序的方法。 也可以将输入流读入数组或字符串,然后为每次使用创建输入流?

编辑2:

很抱歉重新打开这个问题,但在桌面和Web应用程序中工作时条件有所不同。 首先,我从tomcat web app中的org.apache.commons.fileupload.FileItem获取的InputStream不支持标记,因此无法重置。

其次,我希望能够将文件保存在内存中,以便在处理文件时更快地访问和减少io问题。

你可以使用一个版本来装饰传递给POIFSFileSystem的 InputStream,当调用close()时它会以reset()响应:

 class ResetOnCloseInputStream extends InputStream { private final InputStream decorated; public ResetOnCloseInputStream(InputStream anInputStream) { if (!anInputStream.markSupported()) { throw new IllegalArgumentException("marking not supported"); } anInputStream.mark( 1 << 24); // magic constant: BEWARE decorated = anInputStream; } @Override public void close() throws IOException { decorated.reset(); } @Override public int read() throws IOException { return decorated.read(); } } 

测试用例

 static void closeAfterInputStreamIsConsumed(InputStream is) throws IOException { int r; while ((r = is.read()) != -1) { System.out.println(r); } is.close(); System.out.println("========="); } public static void main(String[] args) throws IOException { InputStream is = new ByteArrayInputStream("sample".getBytes()); ResetOnCloseInputStream decoratedIs = new ResetOnCloseInputStream(is); closeAfterInputStreamIsConsumed(decoratedIs); closeAfterInputStreamIsConsumed(decoratedIs); closeAfterInputStreamIsConsumed(is); } 

编辑2

你可以用byte [](slurp模式)读取整个文件,然后将它传递给ByteArrayInputStream

尝试BufferedInputStream,它将标记和重置function添加到另一个输入流,并覆盖其close方法:

 public class UnclosableBufferedInputStream extends BufferedInputStream { public UnclosableBufferedInputStream(InputStream in) { super(in); super.mark(Integer.MAX_VALUE); } @Override public void close() throws IOException { super.reset(); } } 

所以:

 UnclosableBufferedInputStream bis = new UnclosableBufferedInputStream (inputStream); 

并在之前使用inputStream的地方使用bis

这工作正常:

 byte[] bytes = getBytes(inputStream); POIFSFileSystem fileSystem = new POIFSFileSystem(new ByteArrayInputStream(bytes)); 

其中getBytes是这样的:

 private static byte[] getBytes(InputStream is) throws IOException { byte[] buffer = new byte[8192]; ByteArrayOutputStream baos = new ByteArrayOutputStream(2048); int n; baos.reset(); while ((n = is.read(buffer, 0, buffer.length)) != -1) { baos.write(buffer, 0, n); } return baos.toByteArray(); } 

使用以下实现进行更多自定义用途 –

 public class ReusableBufferedInputStream extends BufferedInputStream { private int totalUse; private int used; public ReusableBufferedInputStream(InputStream in, Integer totalUse) { super(in); if (totalUse > 1) { super.mark(Integer.MAX_VALUE); this.totalUse = totalUse; this.used = 1; } else { this.totalUse = 1; this.used = 1; } } @Override public void close() throws IOException { if (used < totalUse) { super.reset(); ++used; } else { super.close(); } } } 

你对“缓存”究竟是什么意思? 您是否希望在流的开头启动不同的POIFSFileSystem? 如果是这样,那么Java代码中的任何内容都没有任何缓存; 它将由操作系统完成,只需打开一个新流。

或者你想继续阅读第一个POIFSFileSystem停止的点吗? 这不是缓存,而且很难做到。 我能想到的唯一方法是,如果你无法避免流被关闭,那就是编写一个瘦包装器来计算已经读取了多少字节,然后打开一个新流并跳过那么多字节。 但是当POIFSFileSystem内部使用类似BufferedInputStream的东西时,这可能会失败。

如果文件不是那么大,请将其读入byte[]数组,并为POI提供从该数组创建的ByteArrayInputStream

如果文件很大,那么你应该不在乎,因为操作系统会尽可能为你做缓存。

[编辑]使用Apache commons-io以有效的方式将文件读入字节数组。 不要使用int read()因为它逐字节读取文件,这非常慢!

如果您想自己动手,请使用File对象获取长度,创建数组以及从文件中读取字节的循环。 你必须循环,因为read(byte[], int offset, int len)可以读取小于len个字节(通常是)。

这是我实现的方式,可以安全地与任何InputStream一起使用:

  • 编写自己的InputStream包装器,在其中创建临时文件以镜像原始流内容
  • 将从原始输入流中读取的所有内容转储到此临时文件中
  • 完全读取流后,您将在临时文件中镜像所有数据
  • 使用InputStream.reset将内部流切换(初始化)为FileInputStream(mirrored_content_file)
  • 从现在开始你将松开原始流的引用(可以收集)
  • 添加一个新方法release(),它将删除临时文件并释放任何打开的流。
  • 你甚至可以从finalize调用release()来确保临时文件是在你忘记调用release()时释放的(大多数时候你应该避免使用finalize ,总是调用一个方法来释放对象资源)。 看你为什么要实现finalize()?
 public static void main(String[] args) throws IOException { BufferedInputStream inputStream = new BufferedInputStream(IOUtils.toInputStream("Foobar")); inputStream.mark(Integer.MAX_VALUE); System.out.println(IOUtils.toString(inputStream)); inputStream.reset(); System.out.println(IOUtils.toString(inputStream)); } 

这很有效。 IOUtils是公共IO的一部分。

这个答案重复以前的1 | 2基于BufferInputStream 。 主要的变化是它允许无限重用。 并负责关闭原始源输入流以释放系统资源。 您的操作系统定义了这些限制,并且您不希望程序用完文件句柄( 这也是您应该总是’消费’响应的原因,例如使用apache EntityUtils.consumeQuietly() )。 编辑更新了代码以处理使用read(buffer, offset, length) gready使用者,在这种情况下, BufferedInputStream可能会尝试查看源代码,此代码可以防止使用。

 public class CachingInputStream extends BufferedInputStream { public CachingInputStream(InputStream source) { super(new PostCloseProtection(source)); super.mark(Integer.MAX_VALUE); } @Override public synchronized void close() throws IOException { if (!((PostCloseProtection) in).decoratedClosed) { in.close(); } super.reset(); } private static class PostCloseProtection extends InputStream { private volatile boolean decoratedClosed = false; private final InputStream source; public PostCloseProtection(InputStream source) { this.source = source; } @Override public int read() throws IOException { return decoratedClosed ? -1 : source.read(); } @Override public int read(byte[] b) throws IOException { return decoratedClosed ? -1 : source.read(b); } @Override public int read(byte[] b, int off, int len) throws IOException { return decoratedClosed ? -1 : source.read(b, off, len); } @Override public long skip(long n) throws IOException { return decoratedClosed ? 0 : source.skip(n); } @Override public int available() throws IOException { return source.available(); } @Override public void close() throws IOException { decoratedClosed = true; source.close(); } @Override public void mark(int readLimit) { source.mark(readLimit); } @Override public void reset() throws IOException { source.reset(); } @Override public boolean markSupported() { return source.markSupported(); } } } 

要重复使用它,如果不是,请先关闭它。

但有一个限制是,如果在读取原始流的整个内容之前关闭流,则此装饰器将具有不完整的数据,因此请确保在关闭之前读取整个流。

我只是在这里添加我的解决方案,因为这对我有用。 它基本上是前两个答案的组合:)

  private String convertStreamToString(InputStream is) { Writer w = new StringWriter(); char[] buf = new char[1024]; Reader r; is.mark(1 << 24); try { r = new BufferedReader(new InputStreamReader(is, "UTF-8")); int n; while ((n=r.read(buf)) != -1) { w.write(buf, 0, n); } is.reset(); } catch(UnsupportedEncodingException e) { Logger.debug(this.getClass(), "Cannot convert stream to string.", e); } catch(IOException e) { Logger.debug(this.getClass(), "Cannot convert stream to string.", e); } return w.toString(); }