如何克隆InputStream?


162

我有一个InputStream,我将其传递给方法来进行一些处理。我将在其他方法中使用相同的InputStream,但是在进行第一次处理后,InputStream似乎在该方法内部被关闭。

我如何克隆InputStream发送到关闭他的方法?还有另一种解决方法?

编辑:关闭InputStream的方法是来自lib的外部方法。我无法控制是否关闭。

private String getContent(HttpURLConnection con) {
    InputStream content = null;
    String charset = "";
    try {
        content = con.getInputStream();
        CloseShieldInputStream csContent = new CloseShieldInputStream(content);
        charset = getCharset(csContent);            
        return  IOUtils.toString(content,charset);
    } catch (Exception e) {
        System.out.println("Error downloading page: " + e);
        return null;
    }
}

private String getCharset(InputStream content) {
    try {
        Source parser = new Source(content);
        return parser.getEncoding();
    } catch (Exception e) {
        System.out.println("Error determining charset: " + e);
        return "UTF-8";
    }
}

1
您是否要在方法返回后“重置”流?即,从头开始阅读信息流?
aioobe 2011年

是的,关闭InputStream的方法返回它已编码的字符集。第二种方法是使用第一种方法中找到的字符集将InputStream转换为String。
Renato Dinhani 2011年

在这种情况下,您应该能够执行我在答案中描述的内容。
Kaj

我不知道解决此问题的最佳方法,但否则我将解决我的问题。Jericho HTML Parser的toString方法返回以正确格式设置的String。这就是我现在所需要的。
Renato Dinhani 2011年

Answers:


188

如果您要做的就是多次读取相同的信息,并且输入数据足够小以适合内存,则可以将数据从您的数据复制InputStreamByteArrayOutputStream

然后,您可以获取关联的字节数组,并根据需要打开尽可能多的“克隆” ByteArrayInputStream

ByteArrayOutputStream baos = new ByteArrayOutputStream();

// Fake code simulating the copy
// You can generally do better with nio if you need...
// And please, unlike me, do something about the Exceptions :D
byte[] buffer = new byte[1024];
int len;
while ((len = input.read(buffer)) > -1 ) {
    baos.write(buffer, 0, len);
}
baos.flush();

// Open new InputStreams using the recorded bytes
// Can be repeated as many times as you wish
InputStream is1 = new ByteArrayInputStream(baos.toByteArray()); 
InputStream is2 = new ByteArrayInputStream(baos.toByteArray()); 

但是,如果您确实需要使原始流保持打开状态以接收新数据,则需要跟踪此外部close()方法并防止以某种方式调用它。

更新(2019):

从Java 9开始,中间的位可以替换为InputStream.transferTo

ByteArrayOutputStream baos = new ByteArrayOutputStream();
input.transferTo(baos);
InputStream firstClone = new ByteArrayInputStream(baos.toByteArray()); 
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray()); 

我找到了解决我的问题的另一种方法,即不涉及复制InputStream,但是我认为如果需要复制InputStream,这是最好的解决方案。
Renato Dinhani 2011年

7
这种方法消耗的内存与输入流的全部内容成比例。最好TeeInputStream按照此处答案中的描述使用。
aioobe 2013年

2
IOUtils(来自Apache Commons)有一个copy方法,该方法可以在代码中间对缓冲区进行读/写操作。
rethab 2014年

31

您要使用Apache的CloseShieldInputStream

这是一个包装程序,可以防止流被关闭。你会做这样的事情。

InputStream is = null;

is = getStream(); //obtain the stream 
CloseShieldInputStream csis = new CloseShieldInputStream(is);

// call the bad function that does things it shouldn't
badFunction(csis);

// happiness follows: do something with the original input stream
is.read();

看起来不错,但在这里不起作用。我将使用代码编辑帖子。
Renato Dinhani 2011年

CloseShield无法正常工作,因为您的原始HttpURLConnection输入流在某处关闭了。您的方法不应该使用受保护的流调用IOUtils IOUtils.toString(csContent,charset)吗?
安东尼·

也许可以是这样。我可以防止从HttpURLConnection被关闭吗?
Renato Dinhani 2011年

1
@Renato。也许问题根本不是close()调用,而是Stream被读取到最后的事实。由于mark()并且reset()可能不是HTTP连接的最佳方法,因此也许您应该看看我的答案中介绍的字节数组方法。
安东尼·阿西里

1
还有一件事,您可以随时打开到相同URL的新连接。看到这里:stackoverflow.com/questions/5807340/…–
安东尼·阿西里

11

您无法克隆它,而如何解决问题取决于数据的来源。

一种解决方案是将所有数据从InputStream读取到一个字节数组中,然后围绕该字节数组创建一个ByteArrayInputStream,然后将该输入流传递到您的方法中。

编辑1:也就是说,如果其他方法也需要读取相同的数据。即您想“重置”流。


我不知道您需要什么帮助。我猜你知道如何从流中读取吗?从InputStream读取所有数据,并将数据写入ByteArrayOutputStream。完成读取所有数据后,在ByteArrayOutputStream上调用toByteArray()。然后将该字节数组传递到ByteArrayInputStream的构造函数中。
Kaj

8

如果从流中读取的数据很大,则建议使用Apache Commons IO的TeeInputStream。这样,您基本上可以复制输入并传递管道作为克隆。


5

这可能无法在所有情况下都起作用,但是这是我所做的:我扩展了FilterInputStream类,并在外部lib读取数据时对字节进行了必要的处理。

public class StreamBytesWithExtraProcessingInputStream extends FilterInputStream {

    protected StreamBytesWithExtraProcessingInputStream(InputStream in) {
        super(in);
    }

    @Override
    public int read() throws IOException {
        int readByte = super.read();
        processByte(readByte);
        return readByte;
    }

    @Override
    public int read(byte[] buffer, int offset, int count) throws IOException {
        int readBytes = super.read(buffer, offset, count);
        processBytes(buffer, offset, readBytes);
        return readBytes;
    }

    private void processBytes(byte[] buffer, int offset, int readBytes) {
       for (int i = 0; i < readBytes; i++) {
           processByte(buffer[i + offset]);
       }
    }

    private void processByte(int readByte) {
       // TODO do processing here
    }

}

然后,您只需StreamBytesWithExtraProcessingInputStream在输入流中传递一个传递位置的实例。使用原始输入流作为构造函数参数。

应该注意的是,这逐字节工作,因此,如果需要高性能,请不要使用它。


3

UPD。之前检查评论。这不完全是要求。

如果您正在使用apache.commons,则可以使用复制流IOUtils

您可以使用以下代码:

InputStream = IOUtils.toBufferedInputStream(toCopy);

这是适合您情况的完整示例:

public void cloneStream() throws IOException{
    InputStream toCopy=IOUtils.toInputStream("aaa");
    InputStream dest= null;
    dest=IOUtils.toBufferedInputStream(toCopy);
    toCopy.close();
    String result = new String(IOUtils.toByteArray(dest));
    System.out.println(result);
}

此代码需要一些依赖项:

玛文

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
</dependency>

梯度

'commons-io:commons-io:2.4'

这是此方法的DOC参考:

获取InputStream的全部内容,并表示与结果InputStream相同的数据。在以下情况下,此方法很有用:

源InputStream缓慢。它具有关联的网络资源,因此我们不能长时间保持打开状态。它具有关联的网络超时。

您可以IOUtils在此处找到更多信息:http : //commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/IOUtils.html#toBufferedInputStream(java.io.InputStream)


7
这不会克隆输入流,而只是对其进行缓冲。不一样 OP希望重新读取同一流(的副本)。
拉斐尔

1

以下是Kotlin的解决方案。

您可以将InputStream复制到ByteArray中

val inputStream = ...

val byteOutputStream = ByteArrayOutputStream()
inputStream.use { input ->
    byteOutputStream.use { output ->
        input.copyTo(output)
    }
}

val byteInputStream = ByteArrayInputStream(byteOutputStream.toByteArray())

如果需要byteInputStream多次byteInputStream.reset()阅读,请先致电,然后再阅读。

https://code.luasoftware.com/tutorials/kotlin/how-to-clone-inputstream/


0

下面的类应该可以解决问题。只需创建一个实例,调用“ multiply”方法,然后提供源输入流和所需的重复项数量即可。

重要:必须在单独的线程中同时使用所有克隆的流。

package foo.bar;

import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class InputStreamMultiplier {
    protected static final int BUFFER_SIZE = 1024;
    private ExecutorService executorService = Executors.newCachedThreadPool();

    public InputStream[] multiply(final InputStream source, int count) throws IOException {
        PipedInputStream[] ins = new PipedInputStream[count];
        final PipedOutputStream[] outs = new PipedOutputStream[count];

        for (int i = 0; i < count; i++)
        {
            ins[i] = new PipedInputStream();
            outs[i] = new PipedOutputStream(ins[i]);
        }

        executorService.execute(new Runnable() {
            public void run() {
                try {
                    copy(source, outs);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

        return ins;
    }

    protected void copy(final InputStream source, final PipedOutputStream[] outs) throws IOException {
        byte[] buffer = new byte[BUFFER_SIZE];
        int n = 0;
        try {
            while (-1 != (n = source.read(buffer))) {
                //write each chunk to all output streams
                for (PipedOutputStream out : outs) {
                    out.write(buffer, 0, n);
                }
            }
        } finally {
            //close all output streams
            for (PipedOutputStream out : outs) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

不回答问题。他想在一种方法中使用流来确定字符集,然后在第二种方法中重新读取它及其字符集。
2014年

0

克隆输入流可能不是一个好主意,因为这需要深入了解要克隆的输入流的详细信息。一种解决方法是创建一个新的输入流,该输入流再次从同一源读取。

因此,使用某些Java 8功能将如下所示:

public class Foo {

    private Supplier<InputStream> inputStreamSupplier;

    public void bar() {
        procesDataThisWay(inputStreamSupplier.get());
        procesDataTheOtherWay(inputStreamSupplier.get());
    }

    private void procesDataThisWay(InputStream) {
        // ...
    }

    private void procesDataTheOtherWay(InputStream) {
        // ...
    }
}

这种方法的积极作用是,它将重用已经存在的代码-创建封装在中的输入流inputStreamSupplier。而且,无需维护用于克隆流的第二条代码路径。

另一方面,如果从流中读取数据很昂贵(因为它是通过低带宽连接完成的),则此方法将使成本增加一倍。可以通过使用特定的供应商来规避该问题,该供应商将首先在本地存储流内容并InputStream为该本地资源提供资源。


这个答案对我来说还不清楚。您如何初始化现有供应商is
user1156544

@ user1156544在我撰写本文时,克隆输入流可能不是一个好主意,因为这需要深入了解要克隆的输入流的详细信息。您不能使用供应商从现有供应商创建输入流。每次调用时,供应商可以使用java.io.Filejava.net.URL创建新的输入流。
SpaceTrucker

我现在明白了。如OP明确要求的那样,这不适用于inputstream,但如果它们是原始数据源,则不适用于File或URL。谢谢
user1156544 17-4-12
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.