Stream zweimal lesen


127

Wie liest du denselben Inputstream zweimal? Ist es möglich, es irgendwie zu kopieren?

Ich muss ein Bild aus dem Web abrufen, es lokal speichern und dann das gespeicherte Bild zurückgeben. Ich dachte nur, es wäre schneller, denselben Stream zu verwenden, anstatt einen neuen Stream für den heruntergeladenen Inhalt zu starten und ihn dann erneut zu lesen.


1
Vielleicht Mark verwenden und zurücksetzen
Vyacheslav Shylkin

Antworten:


113

Sie können org.apache.commons.io.IOUtils.copyden Inhalt des InputStream in ein Byte-Array kopieren und dann mit einem ByteArrayInputStream wiederholt aus dem Byte-Array lesen. Z.B:

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
Ich denke, dies ist die einzig gültige Lösung, da Mark nicht für alle Typen unterstützt wird.
Warpzit

3
@Paul Grime: IOUtils.toByeArray ruft die Kopiermethode auch intern von innen auf.
Ankit

4
Wie @Ankit sagt, ist diese Lösung für mich nicht gültig, da die Eingabe intern gelesen wird und nicht wiederverwendet werden kann.
Xtreme Biker

29
Ich weiß, dass dieser Kommentar keine Zeit mehr hat, aber wenn Sie hier in der ersten Option den Eingabestream als Byte-Array lesen, bedeutet dies nicht, dass Sie alle Daten in den Speicher laden? Was könnte ein großes Problem sein, wenn Sie so etwas wie große Dateien laden?
Jaxkodex

2
Man könnte IOUtils.toByteArray (InputStream) verwenden, um ein Byte-Array in einem Aufruf abzurufen.
nützlich

30

Je nachdem, woher der InputStream kommt, können Sie ihn möglicherweise nicht zurücksetzen. Sie können überprüfen, ob mark()und reset()unterstützt werdenmarkSupported() .

Wenn dies reset()der Fall ist, können Sie den InputStream aufrufen , um zum Anfang zurückzukehren. Wenn nicht, müssen Sie den InputStream erneut von der Quelle lesen.


1
InputStream unterstützt 'mark' nicht - Sie können mark auf einem IS aufrufen, aber es tut nichts. Ebenso wird beim Aufrufen von reset auf einem IS eine Ausnahme ausgelöst.
Ayahuasca

4
@ayahuasca InputStreamUnterklassen wie BufferedInputStreamunterstützt "Mark"
Dmitry Bogdanovich

10

Wenn Sie InputStreamUnterstützung mit Mark unterstützen, können Sie mark()Ihren inputStream und dann reset()es. wenn Ihr InputStremnicht Zeichen nicht unterstützt , dann können Sie die Klasse verwenden java.io.BufferedInputStream, so können Sie einbetten Stream innerhalb eines BufferedInputStreamwie dieses

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

1
Ein gepufferter Eingabestream kann nur auf die Puffergröße zurückgesetzt werden. Wenn die Quelle nicht passt, können Sie nicht bis zum Anfang zurückkehren.
L. Blanc

@ L.Blanc sorry aber das scheint nicht richtig zu sein. Schauen Sie sich an BufferedInputStream.fill(), es gibt den Abschnitt "Wachstumspuffer", in dem die neue Puffergröße nur mit marklimitund verglichen wird MAX_BUFFER_SIZE.
Eugen82 vom

8

Sie können den Eingabestream mit PushbackInputStream umbrechen. Mit PushbackInputStream können bereits gelesene Bytes ungelesen (" zurückgeschrieben ") werden. So können Sie Folgendes tun:

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


}

Bitte beachten Sie, dass PushbackInputStream den internen Byte-Puffer speichert, sodass tatsächlich ein Puffer im Speicher erstellt wird, der "zurückgeschriebene" Bytes enthält.

Wenn wir diesen Ansatz kennen, können wir noch weiter gehen und ihn mit FilterInputStream kombinieren. FilterInputStream speichert den ursprünglichen Eingabestream als Delegat. Auf diese Weise kann eine neue Klassendefinition erstellt werden, mit der Originaldaten automatisch " ungelesen " werden können. Die Definition dieser Klasse lautet wie folgt:

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

}

Diese Klasse hat zwei Methoden. Eine zum Einlesen in vorhandenen Puffer (Definition ist analog zum Aufrufenpublic int read(byte b[], int off, int len) der InputStream-Klasse). Zweitens, der neuen Puffer zurückgibt (dies kann effektiver sein, wenn die Größe des zu lesenden Puffers unbekannt ist).

Jetzt sehen wir unsere Klasse in Aktion:

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

Wenn Sie eine Implementierung von verwenden InputStream, können Sie anhand des Ergebnisses überprüfen InputStream#markSupported(), ob Sie die Methode mark()/ verwenden können.reset() .

Wenn Sie den Stream beim Lesen markieren können, rufen Sie reset()an, um zurück zu gehen und zu beginnen.

Wenn Sie nicht können, müssen Sie einen Stream erneut öffnen.

Eine andere Lösung wäre, InputStream in ein Byte-Array zu konvertieren und dann so oft wie nötig über das Array zu iterieren. In diesem Beitrag finden Sie verschiedene Lösungen. Konvertieren Sie InputStream in ein Byte-Array in Java mit Bibliotheken von Drittanbietern oder nicht. Achtung, wenn der gelesene Inhalt zu groß ist, können Speicherprobleme auftreten.

Wenn Sie das Bild lesen möchten, verwenden Sie:

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

Mit ImageIO#read(java.net.URL)können Sie auch den Cache verwenden.


1
Ein Wort der Warnung bei der Verwendung ImageIO#read(java.net.URL): Einige Webserver und CDNs lehnen möglicherweise bloße Anrufe ab (dh ohne einen Benutzeragenten, der den Server glauben lässt, dass der Anruf von einem Webbrowser stammt) von ImageIO#read. In diesem Fall reicht es URLConnection.openConnection()meistens aus, den Benutzeragenten mit ImageIO.read (InputStream) auf diese Verbindung + einzustellen.
Clint Eastwood

InputStreamist keine Schnittstelle
Brice

3

Wie wäre es mit:

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
Das ist eine schreckliche Idee. Sie speichern den gesamten Stream-Inhalt so.
Niels Doucet

3

So teilen Sie eine InputStreamin zwei Teile, ohne zu vermeiden, dass alle Daten in den Speicher geladen und dann unabhängig voneinander verarbeitet werden:

  1. Erstellen Sie ein paar OutputStream, genau:PipedOutputStream
  2. Verbinden Sie jeden PipedOutputStream mit einem PipedInputStream. Dies PipedInputStreamsind die zurückgegebenen InputStream.
  3. Verbinden Sie den Sourcing InputStream mit dem gerade erstellten OutputStream. Also, alles, was es aus dem Sourcing liest InputStream, würde in beiden geschrieben sein OutputStream. TeeInputStreamDies muss nicht implementiert werden, da dies bereits in (commons.io) erfolgt.
  4. Lesen Sie innerhalb eines getrennten Threads den gesamten Sourcing-InputStream, und implizit werden die Eingabedaten an die Ziel-InputStreams übertragen.

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

Achten Sie darauf, die inputStreams nach dem Konsumieren zu schließen und den ausgeführten Thread zu schließen: TeeInputStream.readAllBytes()

In diesem Fall müssen Sie es in mehrereInputStream statt nur in zwei Teile aufteilen . Ersetzen Sie im vorherigen Codefragment die Klasse TeeOutputStreamfür Ihre eigene Implementierung, die a einkapseln List<OutputStream>und die OutputStreamSchnittstelle überschreiben würde :

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

Könnten Sie bitte Schritt 4 etwas näher erläutern? Warum müssen wir das Lesen manuell auslösen? Warum löst das Lesen von pipedInputStream NICHT das Lesen des Source InputStream aus? Und warum machen wir diesen Anruf asynchron?
24митрий

2

Konvertieren Sie den Eingabestream in Bytes und übergeben Sie ihn dann an die Funktion savefile, wo Sie denselben in den Eingabestream zusammensetzen. Verwenden Sie auch in der ursprünglichen Funktion Bytes, um sie für andere Aufgaben zu verwenden


4
Ich sage schlechte Idee in diesem Fall, das resultierende Array könnte riesig sein und das Gerät des Speichers berauben.
Kevin Parker

0

Falls jemand in einer Spring Boot-App ausgeführt wird und Sie den Antworttext von a lesen möchten RestTemplate (weshalb ich einen Stream zweimal lesen möchte), gibt es eine saubere (er) Möglichkeit, dies zu tun.

Zunächst müssen Sie Spring's verwenden, StreamUtilsum den Stream in einen String zu kopieren:

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

Aber das ist nicht alles. Sie müssen auch eine Anforderungsfactory verwenden, die den Stream für Sie puffern kann, wie folgt:

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

Oder wenn Sie die Factory Bean verwenden (dann ist dies Kotlin, aber trotzdem):

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

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


0

Wenn Sie RestTemplate verwenden, um http-Aufrufe zu tätigen, fügen Sie einfach einen Interceptor hinzu. Der Antworttext wird von der Implementierung von ClientHttpResponse zwischengespeichert. Jetzt kann der Inputstream so oft wie nötig aus der Antwort abgerufen werden

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); 
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.