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

はい、InputStreamを閉じるメソッドは、エンコードされた文字セットを返します。2番目の方法は、最初の方法で見つかった文字セットを使用して、InputStreamをStringに変換することです。
Renato Dinhani、

その場合、私の答えで説明していることを実行できるはずです。
Kaj

私はそれを解決する最良の方法を知りませんが、それ以外の場合は問題を解決します。Jericho HTMLパーサーのメソッドtoStringは、正しいフォーマットでフォーマットされたストリングを返します。現時点で必要なのはそれだけです。
Renato Dinhani、2011年

回答:


188

あなたがしたいすべてが複数回同じ情報を読み出して、入力データがメモリに収まるように十分に小さい場合は、あなたからデータをコピーすることができますInputStreamするByteArrayOutputStream

次に、関連付けられたバイト配列を取得し、「クローン」された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
(Apache Commonsの)IOUtilsには、コードの途中でバッファーの読み取り/書き込みを行うコピーメソッドがあります。
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、

CloseShield元のHttpURLConnection入力ストリームがどこかで閉じられているため、機能していません。あなたのメソッドは保護されたストリームでIOUtilsを呼び出すべきではありませんIOUtils.toString(csContent,charset)か?
Anthony Accioly、

多分これかもしれません。HttpURLConnectionが閉じられるのを防ぐことができますか?
Renato Dinhani、

1
@レナート。おそらく問題はclose()呼び出しではなく、ストリームが最後まで読み取られているという事実です。以来mark()およびreset()HTTP接続のための最善の方法ではないかもしれないが、多分あなたは私の答えで説明したバイト配列のアプローチを見てみる必要があります。
Anthony Accioly、

1
さらに、いつでも同じURLへの新しい接続を開くことができます。ここを参照してください:stackoverflow.com/questions/5807340/...
アンソニーAccioly

11

データを複製することはできません。問題の解決方法は、データのソースが何であるかによって異なります。

1つの解決策は、InputStreamからすべてのデータをバイト配列に読み取り、そのバイト配列の周囲にByteArrayInputStreamを作成し、その入力ストリームをメソッドに渡すことです。

編集1:つまり、他のメソッドも同じデータを読み取る必要がある場合。つまり、ストリームを「リセット」します。


どの部分に助けが必要か分かりません。ストリームから読み取る方法を知っていると思いますか?InputStreamからすべてのデータを読み取り、ByteArrayOutputStreamにデータを書き込みます。すべてのデータの読み取りが完了したら、ByteArrayOutputStreamでtoByteArray()を呼び出します。次に、そのバイト配列をByteArrayInputStreamのコンストラクターに渡します。
Kaj

8

ストリームから読み取ったデータが大きい場合は、Apache Commons IOのTeeInputStreamを使用することをお勧めします。そうすれば、基本的に入力を複製してt'dパイプをクローンとして渡すことができます。


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

このコードにはいくつかの依存関係が必要です。

MAVEN

<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。また、ストリームのクローン作成のために2番目のコードパスを維持する必要はありません。

一方、ストリームからの読み取りが高価な場合(低帯域幅の接続で行われるため)、このメソッドはコストを2倍にします。これは、最初にストリームコンテンツをローカルに格納し、InputStreamそのローカルリソースにを提供する特定のサプライヤーを使用することで回避できます。


この答えははっきりしない。既存のサプライヤーからサプライヤーをどのように初期化しますisか?
user1156544 2017

@ user1156544私が書いたように、入力ストリームの複製は、複製される入力ストリームの詳細について深い知識が必要になるため、良い考えではないかもしれません。サプライヤを使用して既存のストリームから入力ストリームを作成することはできません。サプライヤは、java.io.Fileまたはjava.net.URLを使用して、呼び出されるたびに新しい入力ストリームを作成できます。
SpaceTrucker 2017

なるほど。これはOPが明示的に要求するinputstreamでは機能しませんが、元のデータソースである場合はFileまたはURLで機能します。ありがとう
user1156544
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.