如何克隆InputStream?

201

我有一个输入流(InputStream),我将其传递给一个方法进行一些处理。我将在另一个方法中使用相同的输入流,但在第一个处理后,该输入流似乎在方法内部被关闭了。

我如何克隆该输入流并将其发送到关闭它的方法?是否还有其他解决方案?

编辑:关闭输入流的方法是来自库的外部方法。我无法控制其关闭或不关闭。

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";
    }
}

2
你想在方法返回后“重置”流吗?也就是,从开头读取流? - aioobe
是的,关闭InputStream的方法会返回它所编码的字符集。第二种方法是使用第一种方法中找到的字符集将InputStream转换为String。 - Renato Dinhani
在这种情况下,您应该能够按照我的答案所描述的做到。 - Kaj
我不知道解决它的最佳方法,但我以其他方式解决了我的问题。Jericho HTML解析器的toString方法返回格式正确的字符串。这是我目前所需要的。 - Renato Dinhani
10个回答

243
如果你只是想重复读取相同的信息,并且输入数据足够小可以放入内存中,你可以将数据从你的InputStream复制到一个ByteArrayOutputStream中。接着,你可以获取相关的字节数组,并打开任意数量的“克隆”ByteArrayInputStream
ByteArrayOutputStream baos = new ByteArrayOutputStream();

// Code simulating the copy
// You could alternatively use NIO
// 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 recorded bytes
// Can be repeated as many times as you wish
InputStream is1 = new ByteArrayInputStream(baos.toByteArray()); 
InputStream is2 = new ByteArrayInputStream(baos.toByteArray()); 

但是,如果您确实需要保持原始流以接收新数据,则需要跟踪对 close() 的外部调用。您需要以某种方式防止调用 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
12
使用这种方法会消耗与输入流完整内容成比例的内存。最好使用TeeInputStream,如此答案中所述这里 - aioobe
2
IOUtils(来自Apache Commons)有一个copy方法,可以在代码中间进行缓冲读写操作。 - rethab
1
如果您已经安装了Java 9,为什么不使用 ByteArrayInputStream(input.readAllBytes()) 呢? - Philippe Gioseffi
1
@PhilippeGioseffi,InputStream.readAllBytes在第一次调用后将返回一个空的字节数组(因此需要中间的byte[]ByteArrayOutputStream)。另外,ByteArrayOutputStream.toByteArray返回原始字节数组的副本,这可能是不必要的(上面的代码创建了独立的流“克隆”)。 - Anthony Accioly
显示剩余2条评论

36

你想使用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();

1
看起来不错,但这里不起作用。我会编辑我的帖子并附上代码。 - Renato Dinhani
CloseShield 不起作用是因为您原始的 HttpURLConnection 输入流在某个地方被关闭了。难道您的方法不应该使用受保护的流 IOUtils.toString(csContent,charset) 调用 IOUtils 吗? - Anthony Accioly
1
@Renato。也许问题并不在于close()调用,而是流被读取到了末尾。由于mark()reset()可能不是处理HTTP连接的最佳方法,也许你应该看一下我回答中描述的字节数组方法。 - Anthony Accioly
1
还有一件事,您可以随时打开到相同URL的新连接。 请参见此处:http://stackoverflow.com/questions/5807340/how-to-reset-urlconnection-in-java - Anthony Accioly
对于一些输入流,如InflatorInputStream,不支持标记/重置操作。 - Sumit Kumar Saha
显示剩余4条评论

13

你不能克隆它,你如何解决问题取决于数据源是什么。

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

编辑1: 也就是说,如果另一个方法也需要读取相同的数据。也就是说,您希望“重置”该流。


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

11
如果从流中读取的数据较大,我建议使用Apache Commons IO中的TeeInputStream。这样你就可以复制输入并将一个"tee'd"管道作为克隆传递。

No code answer.... - msangel

6

更新。

请查看前面的评论,这不完全是要求的内容。

如果你正在使用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);
}

这段代码需要一些依赖项:

MAVEN

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

GRADLE

'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)

14
这并不是对输入流进行克隆,而仅仅是对其进行缓存。这两者不同;原帖作者想要重新读取(副本)相同的流。 - Raphael

6

这种方法可能并不适用于所有情况,但下面是我所做的:扩展了FilterInputStream类,并对外部库读取数据时进行所需的字节处理。

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
以下是使用 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/


1
克隆输入流可能不是一个好主意,因为这需要深入了解被克隆的输入流的细节。解决方法是创建一个新的输入流,再次从同一源读取。因此,使用一些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
正如我写的那样,“克隆输入流可能不是一个好主意,因为这需要对被克隆的输入流的细节有深入的了解”,你不能使用供应商从现有的输入流创建输入流。供应商可以使用 java.io.Filejava.net.URL 来创建新的输入流,每次调用时都会创建一个新的输入流。 - SpaceTrucker
我现在明白了。这不适用于输入流,因为OP明确要求使用文件或URL作为数据的原始来源。谢谢。 - user1156544

0

使用示例增强@Anthony Accioly

InputStream:克隆字节流并作为列表集合提供多个副本。

public static List<InputStream> multiplyBytes(InputStream input, int cloneCount) throws IOException {
    List<InputStream> copies = new ArrayList<InputStream>();
    
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    copy(input, baos);
    
    for (int i = 0; i < cloneCount; i++) {
        copies.add(new ByteArrayInputStream(baos.toByteArray()));
    }
    return copies;
}
// IOException - If reading the Reader or Writing into the Writer goes wrong.
public static void copy(Reader in, Writer out) throws IOException {
    try {
        char[] buffer = new char[1024];
        int nrOfBytes = -1;
        while ((nrOfBytes = in.read(buffer)) != -1) {
            out.write(buffer, 0, nrOfBytes);
        }
        out.flush();
    } finally {
        close(in);
        close(out);
    }
}
Reader:克隆chars-Stream并提供作为列表集合的副本数量。
public static List<Reader> multiplyChars(Reader reader, int cloneCOunt) throws IOException {
    List<Reader> copies = new ArrayList<Reader>();
    BufferedReader bufferedInput = new BufferedReader(reader);
    StringBuffer buffer = new StringBuffer();
    String delimiter = System.getProperty("line.separator");
    String line;
    while ((line = bufferedInput.readLine()) != null) {
        if (!buffer.toString().equals(""))
            buffer.append(delimiter);
        buffer.append(line);
    }
    close(bufferedInput);
    for (int i = 0; i < cloneCOunt; i++) {
        copies.add(new StringReader(buffer.toString()));
    }
    return copies;
}
public static void copy(InputStream in, OutputStream out) throws IOException {
    try {
        byte[] buffer = new byte[1024];
        int nrOfBytes = -1;
        while ((nrOfBytes = in.read(buffer)) != -1) {
            out.write(buffer, 0, nrOfBytes);
        }
        out.flush();
    } finally {
        close(in);
        close(out);
    }
}

完整示例:

public class SampleTest {

    public static void main(String[] args) throws IOException {
        String filePath = "C:/Yash/StackoverflowSSL.cer";
        InputStream fileStream = new FileInputStream(new File(filePath) );
        
        List<InputStream> bytesCopy = multiplyBytes(fileStream, 3);
        for (Iterator<InputStream> iterator = bytesCopy.iterator(); iterator.hasNext();) {
            InputStream inputStream = (InputStream) iterator.next();
            System.out.println("Byte Stream:"+ inputStream.available()); // Byte Stream:1784
        }
        printInputStream(bytesCopy.get(0));
        
        //java.sql.Clob clob = ((Clob) getValue(sql)); - clob.getCharacterStream();
        Reader stringReader = new StringReader("StringReader that reads Characters from the specified string.");
        List<Reader> charsCopy = multiplyChars(stringReader, 3);
        for (Iterator<Reader> iterator = charsCopy.iterator(); iterator.hasNext();) {
            Reader reader = (Reader) iterator.next();
            System.out.println("Chars Stream:"+reader.read()); // Chars Stream:83
        }
        printReader(charsCopy.get(0));
    }
    
    // Reader, InputStream - Prints the contents of the reader to System.out.
    public static void printReader(Reader reader) throws IOException {
        BufferedReader br = new BufferedReader(reader);
        String s;
        while ((s = br.readLine()) != null) {
            System.out.println(s);
        }
    }
    public static void printInputStream(InputStream inputStream) throws IOException {
        printReader(new InputStreamReader(inputStream));
    }

    // Closes an opened resource, catching any exceptions.
    public static void close(Closeable resource) {
        if (resource != null) {
            try {
                resource.close();
            } catch (IOException e) {
                System.err.println(e);
            }
        }
    }
}


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

不回答问题。他想在一个方法中使用流来确定字符集,然后在第二个方法中重新读取它以及它的字符集。 - user207421

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