Leggi lo stream due volte


127

Come leggi due volte lo stesso inputstream? È possibile copiarlo in qualche modo?

Devo ottenere un'immagine dal Web, salvarla in locale e quindi restituire l'immagine salvata. Ho solo pensato che sarebbe stato più veloce utilizzare lo stesso flusso invece di avviare un nuovo flusso sul contenuto scaricato e poi leggerlo di nuovo.


1
Forse usa mark e reset
Vyacheslav Shylkin,

Risposte:


114

È possibile utilizzare org.apache.commons.io.IOUtils.copyper copiare il contenuto di InputStream in una matrice di byte e quindi leggere ripetutamente dalla matrice di byte utilizzando ByteArrayInputStream. Per esempio:

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
Penso che questa sia l'unica soluzione valida in quanto il marchio non è supportato per tutti i tipi.
Warpzit

3
@ Paul Grime: IOUtils.toByeArray chiama internamente anche il metodo di copia dall'interno.
Ankit

4
Come dice @Ankit, questa soluzione non è valida per me, poiché l'input viene letto internamente e non può essere riutilizzato.
Xtreme Biker

30
So che questo commento è scaduto, ma, qui nella prima opzione, se leggi l'inputstream come un array di byte, non significa che stai caricando tutti i dati in memoria? quale potrebbe essere un grosso problema se stai caricando qualcosa come file di grandi dimensioni?
jaxkodex

2
Si potrebbe usare IOUtils.toByteArray (InputStream) per ottenere un array di byte in una chiamata.
utile

30

A seconda della provenienza di InputStream, potresti non essere in grado di ripristinarlo. Puoi verificare se mark()e reset()sono supportati utilizzando markSupported().

Se lo è, puoi chiamare reset()InputStream per tornare all'inizio. In caso contrario, è necessario leggere nuovamente InputStream dalla sorgente.


1
InputStream non supporta "mark": puoi chiamare mark su un IS ma non fa nulla. Allo stesso modo, la chiamata a reset su un IS genererà un'eccezione.
ayahuasca

4
@ayahuasca InputStreamsottoclasse come BufferedInputStreamsupporta 'mark'
Dmitry Bogdanovich

10

se il tuo InputStreamsupporto usa mark, puoi mark()usare inputStream e poi reset()esso. se il tuo InputStremmarchio non supporta, puoi usare la classe java.io.BufferedInputStream, in modo da poter incorporare il tuo stream in un BufferedInputStreamsimile

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

1
Un flusso di input bufferizzato può solo riportare la dimensione del buffer, quindi se la sorgente non si adatta, non puoi tornare indietro fino all'inizio.
L. Blanc

@ L.Blanc scusa ma non sembra corretto. Dai un'occhiata a BufferedInputStream.fill(), c'è la sezione "grow buffer", dove la nuova dimensione del buffer viene confrontata solo con marklimite MAX_BUFFER_SIZE.
eugene82

8

Puoi eseguire il wrapping del flusso di input con PushbackInputStream. PushbackInputStream consente di non leggere (" riscrivere ") byte che erano già stati letti, quindi puoi fare così:

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


}

Si noti che PushbackInputStream memorizza il buffer interno di byte, quindi crea davvero un buffer in memoria che contiene i byte "riscritti".

Conoscendo questo approccio possiamo andare oltre e combinarlo con FilterInputStream. FilterInputStream archivia il flusso di input originale come delegato. Questo permette di creare una nuova definizione di classe che permette di " non leggere " automaticamente i dati originali. La definizione di questa classe è la seguente:

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

}

Questa classe ha due metodi. Uno per la lettura nel buffer esistente (la definizione è analoga alla chiamata public int read(byte b[], int off, int len)della classe InputStream). Secondo che restituisce un nuovo buffer (potrebbe essere più efficace se la dimensione del buffer da leggere è sconosciuta).

Ora vediamo la nostra classe in azione:

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

Se stai usando un'implementazione di InputStream, puoi controllare il risultato di InputStream#markSupported()che ti dice se puoi usare o meno il metodo mark()/ reset().

Se puoi contrassegnare lo stream quando leggi, chiama reset()per tornare indietro e iniziare.

Se non puoi, dovrai aprire di nuovo uno stream.

Un'altra soluzione sarebbe convertire InputStream in array di byte, quindi iterare sull'array tutte le volte che è necessario. Puoi trovare diverse soluzioni in questo post Converti InputStream in array di byte in Java utilizzando o meno librerie di terze parti. Attenzione, se il contenuto letto è troppo grande potresti riscontrare alcuni problemi di memoria.

Infine, se hai bisogno di leggere l'immagine, usa:

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

L'uso ImageIO#read(java.net.URL)consente anche di utilizzare la cache.


1
una parola di avvertimento durante l'utilizzo ImageIO#read(java.net.URL): alcuni server web e CDN potrebbero rifiutare chiamate semplici (cioè senza un agente utente che fa credere al server che la chiamata proviene da un browser web) effettuate da ImageIO#read. In tal caso, utilizzando l' URLConnection.openConnection()impostazione dell'agente utente su quella connessione + utilizzando `ImageIO.read (InputStream), la maggior parte delle volte, farà il trucco.
Clint Eastwood

InputStreamnon è un'interfaccia
Brice

3

Che ne dite di:

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
È un'idea terribile. Metti l'intero contenuto del flusso in memoria in questo modo.
Niels Doucet

3

Per dividere un InputStreamin due, evitando di caricare tutti i dati in memoria e quindi elaborarli in modo indipendente:

  1. Crea un paio di OutputStream, precisamente:PipedOutputStream
  2. Connetti ogni PipedOutputStream con un PipedInputStream, questi PipedInputStreamsono i file restituiti InputStream.
  3. Connetti il ​​sourcing InputStream con il file appena creato OutputStream. Quindi, tutto ciò che viene letto dal sourcing InputStream, sarebbe scritto in entrambi OutputStream. Non è necessario implementarlo, perché è già fatto in TeeInputStream(commons.io).
  4. All'interno di un thread separato, legge l'intero inputStream di origine e implicitamente i dati di input vengono trasferiti all'ingressoStream di destinazione.

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

Fare attenzione a chiudere inputStreams dopo essere stati consumati e chiudere il thread che viene eseguito: TeeInputStream.readAllBytes()

In caso, è necessario dividerlo in piùInputStream , invece di solo due. Sostituisci nel frammento di codice precedente la classe TeeOutputStreamper la tua implementazione, che incapsulerebbe a List<OutputStream>e sovrascriverebbe l' OutputStreaminterfaccia:

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

Per favore, potresti spiegare un po 'di più il passaggio 4? Perché dobbiamo attivare la lettura manualmente? Perché la lettura di pipedInputStream NON attiva la lettura della sorgente inputStream? E perché facciamo quella chiamata in modo asincrono?
Дмитрий Кулешов

2

Converti inputstream in byte e poi passalo alla funzione savefile dove assembli lo stesso in inputstream. Anche nella funzione originale utilizzare i byte da utilizzare per altre attività


5
Dico una cattiva idea su questo, l'array risultante potrebbe essere enorme e ruberà la memoria al dispositivo.
Kevin Parker

0

Nel caso in cui qualcuno stia eseguendo un'app Spring Boot e desideri leggere il corpo della risposta di a RestTemplate(motivo per cui voglio leggere uno stream due volte), c'è un modo pulito (ehm) per farlo.

Prima di tutto, devi usare Spring's StreamUtilsper copiare lo stream su una stringa:

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

Ma non è tutto. È inoltre necessario utilizzare una factory di richiesta che possa eseguire il buffer del flusso per te, in questo modo:

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

Oppure, se stai usando il bean di fabbrica, allora (questo è Kotlin ma comunque):

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

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


0

Se stai usando RestTemplate per effettuare chiamate http, aggiungi semplicemente un intercettore. Il corpo della risposta viene memorizzato nella cache dall'implementazione di ClientHttpResponse. Ora inputstream può essere recuperato da respose tutte le volte che è necessario

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); 
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.