如何为多次使用缓存InputStream

36

我有一个文件的InputStream,并且我使用Apache POI组件从中读取,就像这样:

POIFSFileSystem fileSystem = new POIFSFileSystem(inputStream);
问题在于我需要多次使用相同的流,而POIFSFileSystem在使用后会关闭该流。
最佳方法是缓存输入流的数据,然后为不同的POIFSFileSystem提供更多的输入流吗?
编辑1:
我所说的缓存是指将其存储以供以后使用,而不是作为加速应用程序的方法。此外,是将输入流读入数组或字符串,然后为每次使用创建输入流更好呢?
编辑2:
很抱歉重新打开问题,但在桌面和Web应用程序内部工作时,条件有所不同。 首先,我从tomcat web应用程序中获取的org.apache.commons.fileupload.FileItem的InputStream不支持标记,因此无法重置。
其次,我希望能够将文件保留在内存中,以便在处理文件时获得更快的访问速度和更少的io问题。

请查看我在帖子中的“编辑2”;我希望它能够正常工作。 - dfa
10个回答

23

尝试使用BufferedInputStream,它为另一个输入流添加了标记和重置功能,并只需覆盖其关闭方法:

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


3
不管你的InputStream是否支持,BufferedInputStream都会将其包装起来,并缓存输入并且支持自己的标记。 重写的close方法还会在使用时方便地将其重置。 - Tomasz
当你要求关闭它时,为什么要重置它?这真的有必要吗?我们不应该只在需要时调用reset()吗? - android developer
1
如果您正在使用一个需要 InputStream 并在使用后关闭它的库,例如。 - Timmos
@Timmos,我不确定你是否理解我的意思。我可以在“close()”方法中看到他调用了“reset()”。我不明白他为什么要这样做。难道一直保持inputStream处于活动状态不是很糟糕的事情吗? - android developer
@androiddeveloper @timmos 我记得我不想让某个库关闭我的 InputStream,因为我仍然需要它,所以最终我创建了一个重载的 close 方法,并添加了一个参数 public void close(bool reallyClose),这样当我完成操作后才会关闭它,而不是由库来关闭。 - Azder
显示剩余4条评论

23

你可以使用一个装饰器来装饰传递给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

您可以以字节数组的形式(抓取模式)读取整个文件,然后将其传递给 ByteArrayInputStream。


1
在使用anInputStream.mark(1 << 24)时,它可以处理多大的文件? - Azder
忘了它,你可以将其作为参数。 - dfa
3
我刚刚输入了Integer.MAX_VALUE,不管怎样,谢谢,它非常有效。 - Azder
@Michael-O mark 设置流在调用 reset 时返回的位置。https://docs.oracle.com/javase/7/docs/api/java/io/InputStream.html#mark(int) - Azder
这个(.mark(Integer.MAX_VALUE))只适用于小于 2GB 的文件。 - rwijngaa
@rwijngaa 对于这种情况,也许 InputStream 不是最好的选择。我更愿意尝试使用 FileChannel - bric3

6

这个是正确的:

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();
 }

2
请使用以下实现来进行更多自定义 -
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();
        }
    }
}

1
这个答案在之前的答案基础上迭代1|2,使用BufferInputStream。 主要变化是允许无限重用,并负责关闭原始源输入流以释放系统资源。 您的操作系统对这些资源有限制,您不希望程序耗尽文件句柄(这也是为什么您应该始终使用apache EntityUtils.consumeQuietly()等“消耗”响应的原因)。 编辑 更新代码以处理使用read(buffer, offset, length)的贪婪消费者,在这种情况下,可能会发生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();
        }
    }
}

要重复使用它,请先关闭它(如果尚未关闭)。
不过,有一个限制,即如果在读取原始流的全部内容之前关闭流,则此装饰器将具有不完整的数据,因此请确保在关闭之前读取整个流。

1
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是commons IO的一部分。


1

如果文件不是很大,将其读入一个byte[]数组中,并从该数组创建一个ByteArrayInputStream提供给POI使用。

如果文件很大,那么你不需要担心,因为操作系统会尽可能地为你缓存。

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

如果您想自己完成,请使用File对象获取长度,创建数组和循环读取文件中的字节。您必须循环,因为read(byte[], int offset, int len)可能读取少于len字节(通常如此)。


Read()方法返回int类型,我该如何拆分字节:小端还是大端? - Azder
读取操作返回的总是0-255或者-1。先检查是否为-1(流结束),然后你就可以安全地将其转换为字节(byte)。 - adrian.tarau

1

你所说的“缓存”是什么意思?你想让不同的POIFSFileSystem从流的开头开始吗?如果是这样,在你的Java代码中完全没有缓存任何东西的必要;操作系统会自动完成,只需打开一个新的流即可。

还是说你想在第一个POIFSFileSystem停止的地方继续读取?那不是缓存,而且非常难做到。我能想到的唯一方法是,如果无法避免流被关闭,就编写一个薄包装器来计算已读取的字节数,然后打开一个新的流并跳过相同数量的字节。但是当POIFSFileSystem内部使用类似于BufferedInputStream的东西时,这可能会失败。


1
假定输入流是可重置的并不明智。 - adrian.tarau

1

这是我会如何实现,以便安全地与任何InputStream一起使用:

  • 编写自己的InputStream包装器,在其中创建一个临时文件来镜像原始流内容
  • 将从原始输入流读取的所有内容转储到此临时文件中
  • 当流完全读取时,您将在临时文件中拥有所有镜像的数据
  • 使用InputStream.reset将内部流切换(初始化)为FileInputStream(mirrored_content_file)
  • 从现在开始,您将失去原始流的引用(可以被收集)
  • 添加一个新的方法release(),它将删除临时文件并释放任何打开的流。
  • 您甚至可以从finalize 调用release()以确保在忘记调用release()的情况下释放临时文件(大多数情况下应避免使用finalize ,始终调用释放对象资源的方法)。请参见{{link1:为什么要实现finalize()?}}

0

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

    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();
}

1
很高兴它对你有用,但你不应该提供解决方案来回答你的问题,而是要回答所问的问题 ;) - Azder
这是我关于如何缓存InputStream以供多次使用的解决方案。这不就是你提交的问题吗? - FuePi
2
我很感激你的努力。对于我提交的问题,有时候细节会让它变得不同。我提出的问题比较具体,需要多次使用 Stream 并由 Apache POI 消耗,这可能适用于 String 也可能不适用。所以你实际上回答了更一般的问题,而不是我发布的更具体的问题。这就是为什么最具体的答案获胜。 - Azder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接