ストリームを2回読み取る


127

どのようにして同じ入力ストリームを2回読み取りますか?どういうわけかそれをコピーすることは可能ですか?

Webから画像を取得し、ローカルに保存してから、保存した画像を返す必要があります。私は、ダウンロードしたコンテンツに新しいストリームを開始してからもう一度読み取るのではなく、同じストリームを使用する方が速くなると考えました。


1
多分マークアンドリセットを使用する
Vyacheslav Shylkin

回答:


113

を使用org.apache.commons.io.IOUtils.copyして、InputStreamの内容をバイト配列にコピーし、ByteArrayInputStreamを使用してバイト配列から繰り返し読み取ることができます。例えば:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
org.apache.commons.io.IOUtils.copy(in, baos);
byte[] bytes = baos.toByteArray();

// either
while (needToReadAgain) {
    ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
    yourReadMethodHere(bais);
}

// or
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
while (needToReadAgain) {
    bais.reset();
    yourReadMethodHere(bais);
}

1
マークがすべてのタイプでサポートされているわけではないため、これが唯一の有効な解決策だと思います。
Warpzit

3
@Paul Grime:IOUtils.toByeArrayも内部からcopyメソッドを内部的に呼び出します。
Ankit

4
@Ankitが言うように、入力は内部で読み取られ、再利用できないため、このソリューションは私には有効ではありません。
Xtreme Biker 2014年

30
私はこのコメントが期限切れであることを知っていますが、ここで最初のオプションでは、入力ストリームをバイト配列として読み取る場合、すべてのデータをメモリにロードしていることを意味しませんか?大きなファイルのようなものをロードしている場合、これは大きな問題になる可能性がありますか?
jaxkodex 2015年

2
IOUtils.toByteArray(InputStream)を使用して、1回の呼び出しでバイト配列を取得できます。
有用

30

InputStreamのソースによっては、リセットできない場合があります。を使用mark()reset()てサポートされているかどうかを確認できmarkSupported()ます。

そうであればreset()、InputStreamを呼び出して最初に戻ることができます。そうでない場合は、ソースからInputStreamを再度読み取る必要があります。


1
InputStreamは 'mark'をサポートしていません-ISでmarkを呼び出すことができますが、何もしません。同様に、ISでリセットを呼び出すと、例外がスローされます。
アヤワスカ2017

4
@ayahuascaのInputStreamサブクラスBufferedInputStreamは「マーク」をサポートしていますか
Dmitry Bogdanovich

10

あなたのInputStreamサポートがマークを使用しているなら、あなたはmark()あなたのinputStreamをしてそれからreset()それをすることができます。あなたのInputStremマークがサポートされていない場合は、クラスを使用java.io.BufferedInputStreamできるので、BufferedInputStreamこのような中にストリームを埋め込むことができます

    InputStream bufferdInputStream = new BufferedInputStream(yourInputStream);
    bufferdInputStream.mark(some_value);
    //read your bufferdInputStream 
    bufferdInputStream.reset();
    //read it again

1
バッファリングされた入力ストリームは、バッファサイズにマークバックすることしかできないため、ソースが適合しない場合、最初に戻ることはできません。
L.ブラン

@ L.Blanc申し訳ありませんが、それは正しくないようです。を見てください。BufferedInputStream.fill()「バッファの拡張」セクションがあり、新しいバッファサイズがmarklimitおよびとのみ比較されMAX_BUFFER_SIZEます。
eugene82

8

入力ストリームをPushbackInputStreamでラップできます。PushbackInputStreamを使用すると、すでに読み取られたバイトを未読(「書き戻し」)できるため、次のようにできます。

public class StreamTest {
  public static void main(String[] args) throws IOException {
    byte[] bytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

    InputStream originalStream = new ByteArrayInputStream(bytes);

    byte[] readBytes = getBytes(originalStream, 3);
    printBytes(readBytes); // prints: 1 2 3

    readBytes = getBytes(originalStream, 3);
    printBytes(readBytes); // prints: 4 5 6

    // now let's wrap it with PushBackInputStream

    originalStream = new ByteArrayInputStream(bytes);

    InputStream wrappedStream = new PushbackInputStream(originalStream, 10); // 10 means that maximnum 10 characters can be "written back" to the stream

    readBytes = getBytes(wrappedStream, 3);
    printBytes(readBytes); // prints 1 2 3

    ((PushbackInputStream) wrappedStream).unread(readBytes, 0, readBytes.length);

    readBytes = getBytes(wrappedStream, 3);
    printBytes(readBytes); // prints 1 2 3


  }

  private static byte[] getBytes(InputStream is, int howManyBytes) throws IOException {
    System.out.print("Reading stream: ");

    byte[] buf = new byte[howManyBytes];

    int next = 0;
    for (int i = 0; i < howManyBytes; i++) {
      next = is.read();
      if (next > 0) {
        buf[i] = (byte) next;
      }
    }
    return buf;
  }

  private static void printBytes(byte[] buffer) throws IOException {
    System.out.print("Reading stream: ");

    for (int i = 0; i < buffer.length; i++) {
      System.out.print(buffer[i] + " ");
    }
    System.out.println();
  }


}

PushbackInputStreamはバイトの内部バッファを格納するため、「書き戻された」バイトを保持するバッファをメモリ内に実際に作成することに注意してください。

このアプローチを知っているので、さらに進んでFilterInputStreamと組み合わせることができます。FilterInputStreamは、元の入力ストリームをデリゲートとして格納します。これにより、元のデータを自動的に「未読」にする新しいクラス定義を作成できます。このクラスの定義は次のとおりです。

public class TryReadInputStream extends FilterInputStream {
  private final int maxPushbackBufferSize;

  /**
  * Creates a <code>FilterInputStream</code>
  * by assigning the  argument <code>in</code>
  * to the field <code>this.in</code> so as
  * to remember it for later use.
  *
  * @param in the underlying input stream, or <code>null</code> if
  *           this instance is to be created without an underlying stream.
  */
  public TryReadInputStream(InputStream in, int maxPushbackBufferSize) {
    super(new PushbackInputStream(in, maxPushbackBufferSize));
    this.maxPushbackBufferSize = maxPushbackBufferSize;
  }

  /**
   * Reads from input stream the <code>length</code> of bytes to given buffer. The read bytes are still avilable
   * in the stream
   *
   * @param buffer the destination buffer to which read the data
   * @param offset  the start offset in the destination <code>buffer</code>
   * @aram length how many bytes to read from the stream to buff. Length needs to be less than
   *        <code>maxPushbackBufferSize</code> or IOException will be thrown
   *
   * @return number of bytes read
   * @throws java.io.IOException in case length is
   */
  public int tryRead(byte[] buffer, int offset, int length) throws IOException {
    validateMaxLength(length);

    // NOTE: below reading byte by byte instead of "int bytesRead = is.read(firstBytes, 0, maxBytesOfResponseToLog);"
    // because read() guarantees to read a byte

    int bytesRead = 0;

    int nextByte = 0;

    for (int i = 0; (i < length) && (nextByte >= 0); i++) {
      nextByte = read();
      if (nextByte >= 0) {
        buffer[offset + bytesRead++] = (byte) nextByte;
      }
    }

    if (bytesRead > 0) {
      ((PushbackInputStream) in).unread(buffer, offset, bytesRead);
    }

    return bytesRead;

  }

  public byte[] tryRead(int maxBytesToRead) throws IOException {
    validateMaxLength(maxBytesToRead);

    ByteArrayOutputStream baos = new ByteArrayOutputStream(); // as ByteArrayOutputStream to dynamically allocate internal bytes array instead of allocating possibly large buffer (if maxBytesToRead is large)

    // NOTE: below reading byte by byte instead of "int bytesRead = is.read(firstBytes, 0, maxBytesOfResponseToLog);"
    // because read() guarantees to read a byte

    int nextByte = 0;

    for (int i = 0; (i < maxBytesToRead) && (nextByte >= 0); i++) {
      nextByte = read();
      if (nextByte >= 0) {
        baos.write((byte) nextByte);
      }
    }

    byte[] buffer = baos.toByteArray();

    if (buffer.length > 0) {
      ((PushbackInputStream) in).unread(buffer, 0, buffer.length);
    }

    return buffer;

  }

  private void validateMaxLength(int length) throws IOException {
    if (length > maxPushbackBufferSize) {
      throw new IOException(
        "Trying to read more bytes than maxBytesToRead. Max bytes: " + maxPushbackBufferSize + ". Trying to read: " +
        length);
    }
  }

}

このクラスには2つのメソッドがあります。1つは既存のバッファーへの読み取り用です(定義はpublic int read(byte b[], int off, int len)InputStreamクラスの呼び出しに類似しています)。2番目は、新しいバッファーを返します(読み取るバッファーのサイズが不明な場合、これはより効果的です)。

次に、クラスの動作を見てみましょう。

public class StreamTest2 {
  public static void main(String[] args) throws IOException {
    byte[] bytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

    InputStream originalStream = new ByteArrayInputStream(bytes);

    byte[] readBytes = getBytes(originalStream, 3);
    printBytes(readBytes); // prints: 1 2 3

    readBytes = getBytes(originalStream, 3);
    printBytes(readBytes); // prints: 4 5 6

    // now let's use our TryReadInputStream

    originalStream = new ByteArrayInputStream(bytes);

    InputStream wrappedStream = new TryReadInputStream(originalStream, 10);

    readBytes = ((TryReadInputStream) wrappedStream).tryRead(3); // NOTE: no manual call to "unread"(!) because TryReadInputStream handles this internally
    printBytes(readBytes); // prints 1 2 3

    readBytes = ((TryReadInputStream) wrappedStream).tryRead(3); 
    printBytes(readBytes); // prints 1 2 3

    readBytes = ((TryReadInputStream) wrappedStream).tryRead(3);
    printBytes(readBytes); // prints 1 2 3

    // we can also call normal read which will actually read the bytes without "writing them back"
    readBytes = getBytes(wrappedStream, 3);
    printBytes(readBytes); // prints 1 2 3

    readBytes = getBytes(wrappedStream, 3);
    printBytes(readBytes); // prints 4 5 6

    readBytes = ((TryReadInputStream) wrappedStream).tryRead(3); // now we can try read next bytes
    printBytes(readBytes); // prints 7 8 9

    readBytes = ((TryReadInputStream) wrappedStream).tryRead(3); 
    printBytes(readBytes); // prints 7 8 9


  }



}

5

の実装を使用している場合はInputStream、その結果を確認してInputStream#markSupported()、メソッドmark()/を使用できるかどうかを確認できますreset()

読んだときにストリームにマークを付けることができる場合は、呼び出しreset()て戻って始めます。

できない場合は、もう一度ストリームを開く必要があります。

別の解決策は、InputStreamをバイト配列に変換し、必要な回数だけ配列を反復処理することです。この投稿では、サードパーティのライブラリを使用しているかどうかを問わず、JavaでInputStreamをバイト配列に変換する方法をいくつか紹介しています。注意、読み取ったコンテンツが大きすぎると、メモリの問題が発生する可能性があります。

最後に、画像を読み取る必要がある場合は、次を使用します。

BufferedImage image = ImageIO.read(new URL("http://www.example.com/images/toto.jpg"));

を使用ImageIO#read(java.net.URL)すると、キャッシュを使用することもできます。


1
使用時の警告ImageIO#read(java.net.URL):一部のWebサーバーとCDNは、によるの呼び出し(つまり、呼び出しがWebブラウザーからの呼び出しであるとサーバーに信じ込ませるユーザーエージェントがない場合)を拒否する場合がありますImageIO#read。その場合、URLConnection.openConnection()ユーザーエージェントをその接続に設定して+ `ImageIO.read(InputStream)を使用して、ほとんどの場合トリックを実行します。
クリントイーストウッド

InputStreamはインターフェースではありません
Briceの

3

どうですか:

if (stream.markSupported() == false) {

        // lets replace the stream object
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        IOUtils.copy(stream, baos);
        stream.close();
        stream = new ByteArrayInputStream(baos.toByteArray());
        // now the stream should support 'mark' and 'reset'

    }

5
それはひどい考えです。このようにして、ストリームのコンテンツ全体をメモリに配置します。
Niels Doucet

3

InputStreamを2つに分割し、すべてのデータをメモリにロードすることを避け、それらを個別に処理する場合:

  1. OutputStream正確にいくつかを作成します。PipedOutputStream
  2. 各PipedOutputStreamをPipedInputStreamに接続します。これらPipedInputStreamは返されInputStreamます。
  3. 作成したばかりのソースInputStreamを接続しますOutputStream。したがって、すべてがソースから読み取らInputStreamれ、両方で記述されOutputStreamます。TeeInputStream(commons.io)ですでに実装されているため、実装する必要はありません。
  4. 分離されたスレッド内でソースinputStream全体を読み取り、暗黙的に入力データがターゲットinputStreamsに転送されます。

    public static final List<InputStream> splitInputStream(InputStream input) 
        throws IOException 
    { 
        Objects.requireNonNull(input);      
    
        PipedOutputStream pipedOut01 = new PipedOutputStream();
        PipedOutputStream pipedOut02 = new PipedOutputStream();
    
        List<InputStream> inputStreamList = new ArrayList<>();
        inputStreamList.add(new PipedInputStream(pipedOut01));
        inputStreamList.add(new PipedInputStream(pipedOut02));
    
        TeeOutputStream tout = new TeeOutputStream(pipedOut01, pipedOut02);
    
        TeeInputStream tin = new TeeInputStream(input, tout, true);
    
        Executors.newSingleThreadExecutor().submit(tin::readAllBytes);  
    
        return Collections.unmodifiableList(inputStreamList);
    }

消費された後はinputStreamsを閉じ、実行中のスレッドを閉じることに注意してください。 TeeInputStream.readAllBytes()

場合によっては、2つではなく複数InputStream分割する必要があります。前のコードのフラグメントでTeeOutputStream、独自の実装のクラスを置き換えます。これによりList<OutputStream>OutputStreamインターフェースがカプセル化され、オーバーライドされます。

public final class TeeListOutputStream extends OutputStream {
    private final List<? extends OutputStream> branchList;

    public TeeListOutputStream(final List<? extends OutputStream> branchList) {
        Objects.requireNonNull(branchList);
        this.branchList = branchList;
    }

    @Override
    public synchronized void write(final int b) throws IOException {
        for (OutputStream branch : branchList) {
            branch.write(b);
        }
    }

    @Override
    public void flush() throws IOException {
        for (OutputStream branch : branchList) {
            branch.flush();
        }
    }

    @Override
    public void close() throws IOException {
        for (OutputStream branch : branchList) {
            branch.close();
        }
    }
}

ステップ4についてもう少し説明してもらえますか?手動で読み取りをトリガーする必要があるのはなぜですか?pipedInputStreamの読み取りがソースinputStreamの読み取りをトリガーしないのはなぜですか?そして、なぜそれを非同期で呼び出すのですか?
ДмитрийКулешов

2

inputstreamをバイトに変換し、それをsavefile関数に渡して、同じものをinputstreamにアセンブルします。また、元の関数ではバイトを使用して他のタスクに使用します


5
私はこれについて悪い考えを言います、結果の配列は巨大になる可能性があり、メモリのデバイスを奪います。
Kevin Parker、

0

誰かがSpring Bootアプリで実行されていて、RestTemplate(のでストリームを2回読みたいのはそのためです)の応答本文を読みたい場合、これを行うよりクリーンな(より)方法があります。

まず、Springを使用しStreamUtilsてストリームをStringにコピーする必要があります。

String text = StreamUtils.copyToString(response.getBody(), Charset.defaultCharset()))

しかし、それだけではありません。次のように、ストリームをバッファリングできるリクエストファクトリを使用する必要もあります。

ClientHttpRequestFactory factory = new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory());
RestTemplate restTemplate = new RestTemplate(factory);

または、ファクトリBeanを使用している場合(これはKotlinですが、それでも):

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
fun createRestTemplate(): RestTemplate = RestTemplateBuilder()
  .requestFactory { BufferingClientHttpRequestFactory(SimpleClientHttpRequestFactory()) }
  .additionalInterceptors(loggingInterceptor)
  .build()

ソース:https : //objectpartners.com/2018/03/01/log-your-resttemplate-request-and-response-without-destroying-the-body/


0

RestTemplateを使用してhttp呼び出しを行う場合は、インターセプターを追加するだけです。応答本文は、ClientHttpResponseの実装によってキャッシュされます。入力ストリームは、必要な回数だけresposeから取得できるようになりました。

ClientHttpRequestInterceptor interceptor =  new ClientHttpRequestInterceptor() {

            @Override
            public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                    ClientHttpRequestExecution execution) throws IOException {
                ClientHttpResponse  response = execution.execute(request, body);

                  // additional work before returning response
                  return response 
            }
        };

    // Add the interceptor to RestTemplate Instance 

         restTemplate.getInterceptors().add(interceptor); 
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.