如何缓存InputStream以供多种用途

时间:2009-05-29 08:34:44

标签: java caching inputstream apache-poi

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

POIFSFileSystem fileSystem = new POIFSFileSystem(inputStream);

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

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

编辑1:

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

编辑2:

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

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

10 个答案:

答案 0 :(得分:20)

尝试BufferedInputStream,它将标记和重置功能添加到另一个输入流,并覆盖其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

答案 1 :(得分:17)

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

答案 2 :(得分:4)

这是正常的:

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

答案 3 :(得分: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();
        }
    }
}

答案 4 :(得分:1)

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

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

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

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

答案 5 :(得分:1)

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

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

答案 6 :(得分:1)

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

  • 编写自己的InputStream包装器,在其中创建临时文件以镜像原始流内容
  • 将从原始输入流中读取的所有内容转储到此临时文件中
  • 当完全读取流时,您将在临时文件中镜像所有数据
  • 使用InputStream.reset将内部流切换(初始化)为FileInputStream(mirrored_content_file)
  • 从现在开始,您将丢失原始流的引用(可以收集)
  • 添加一个新方法release(),它将删除临时文件并释放任何打开的流。
  • 你甚至可以从 finalize 调用release()以确保临时文件是在你忘记调用release()时释放的(大多数时候你应该避免使用 finalize < / em>,总是调用一个方法来释放对象资源)。见Why would you ever implement finalize()?

答案 7 :(得分: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是公共IO的一部分。

答案 8 :(得分:1)

此答案基于BufferInputStream对以前的 1 | 2 进行迭代。主要的变化是它允许无限重用。并负责关闭原始源输入流以释放系统资源。您的操作系统定义了这些限制,并且您不希望程序用完文件句柄(这也是您应该总是'消费'响应的原因,例如使用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();
        }
    }
}

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

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

答案 9 :(得分: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();
}