Leer transmisión dos veces


127

¿Cómo se lee el mismo flujo de entrada dos veces? ¿Es posible copiarlo de alguna manera?

Necesito obtener una imagen de la web, guardarla localmente y luego devolver la imagen guardada. Simplemente pensé que sería más rápido usar la misma secuencia en lugar de comenzar una nueva secuencia para el contenido descargado y luego volver a leerla.


1
Tal vez use marcar y reiniciar
Vyacheslav Shylkin

Respuestas:


113

Puede usar org.apache.commons.io.IOUtils.copypara copiar el contenido de InputStream en una matriz de bytes, y luego leer repetidamente desde la matriz de bytes utilizando un ByteArrayInputStream. P.ej:

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
Creo que esta es la única solución válida, ya que Mark no es compatible con todos los tipos.
Warpzit

3
@Paul Grime: IOUtils.toByeArray llama internamente al método de copia desde adentro también.
Ankit

44
Como dice @Ankit, esta solución no es válida para mí, ya que la entrada se lee internamente y no se puede reutilizar.
Xtreme Biker

30
Sé que este comentario está fuera de tiempo, pero, aquí en la primera opción, si lees el flujo de entrada como una matriz de bytes, ¿no significa que estás cargando todos los datos en la memoria? ¿Cuál podría ser un gran problema si está cargando algo como archivos grandes?
jaxkodex

2
Se podría usar IOUtils.toByteArray (InputStream) para obtener una matriz de bytes en una llamada.
útil el

30

Dependiendo de dónde provenga InputStream, es posible que no pueda restablecerlo. Puede verificar si mark()y reset()son compatibles conmarkSupported() .

Si es así, puede llamar reset()al InputStream para volver al principio. De lo contrario, debe leer InputStream desde la fuente nuevamente.


1
InputStream no admite 'marca'; puede llamar a marca en un IS pero no hace nada. Del mismo modo, llamar a reset en un IS arrojará una excepción.
ayahuasca

44
@ayahuasca InputStreamsubclases como BufferedInputStreamadmite 'marca'
Dmitry Bogdanovich

10

Si es InputStreamcompatible con Mark, entonces puede mark()su inputStream y luego reset(). si su InputStremmarca no es compatible, entonces puede usar la clase java.io.BufferedInputStream, por lo que puede incrustar su transmisión dentro de un estilo BufferedInputStreamcomo este

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

1
Una secuencia de entrada almacenada solo puede volver a marcar el tamaño del búfer, por lo que si la fuente no se ajusta, no puede volver al principio.
L. Blanc

@ L.Blanc lo siento, pero eso no parece correcto. Eche un vistazo BufferedInputStream.fill(), existe la sección "crecer buffer", donde el nuevo tamaño de buffer se compara solo con marklimity MAX_BUFFER_SIZE.
eugene82

8

Puede ajustar la secuencia de entrada con PushbackInputStream. PushbackInputStream permite no leer (" escribir ") bytes que ya se leyeron, por lo que puede hacer esto:

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


}

Tenga en cuenta que PushbackInputStream almacena el búfer interno de bytes, por lo que realmente crea un búfer en la memoria que contiene los bytes "reescritos".

Conociendo este enfoque podemos ir más allá y combinarlo con FilterInputStream. FilterInputStream almacena la secuencia de entrada original como delegado. Esto permite crear una nueva definición de clase que permite " no leer " los datos originales automáticamente. La definición de esta clase es la siguiente:

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

}

Esta clase tiene dos métodos. Uno para leer en el búfer existente (la definición es análoga a la llamadapublic int read(byte b[], int off, int len) de la clase InputStream). Segundo, que devuelve un nuevo búfer (esto puede ser más efectivo si se desconoce el tamaño del búfer para leer).

Ahora veamos a nuestra clase en acción:

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

Si está utilizando una implementación de InputStream, puede verificar el resultado InputStream#markSupported()que le indica si puede usar el método mark()/reset() .

Si puede marcar la transmisión cuando lee, entonces llame reset()para regresar para comenzar.

Si no puede, tendrá que abrir una transmisión nuevamente.

Otra solución sería convertir InputStream en una matriz de bytes, luego iterar sobre la matriz tantas veces como sea necesario. Puede encontrar varias soluciones en esta publicación Convertir InputStream a matriz de bytes en Java utilizando bibliotecas de terceros o no. Precaución, si el contenido leído es demasiado grande, puede experimentar algunos problemas de memoria.

Finalmente, si su necesidad es leer la imagen, use:

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

Usar ImageIO#read(java.net.URL)también le permite usar caché.


1
una palabra de advertencia cuando se usa ImageIO#read(java.net.URL): algunos servidores web y CDN pueden rechazar llamadas desnudas (es decir, sin un Agente de usuario que haga creer al servidor que la llamada proviene de un navegador web) realizada por ImageIO#read. En ese caso, usar la URLConnection.openConnection()configuración del agente de usuario para esa conexión + usar `ImageIO.read (InputStream), la mayoría de las veces, hará el truco.
Clint Eastwood

InputStreamno es una interfaz
Brice

3

Qué tal si:

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'

    }

55
Esa es una idea terrible. Pones todo el contenido de la secuencia en la memoria de esa manera.
Niels Doucet

3

Para dividir un InputStreamen dos, evitando cargar todos los datos en la memoria y luego procesarlos de forma independiente:

  1. Crea un par de OutputStream, precisamente:PipedOutputStream
  2. Conecte cada PipedOutputStream con un PipedInputStream, estos PipedInputStreamson los devueltos InputStream.
  3. Conecte la fuente de InputStream con el recién creado OutputStream. Entonces, todo lo que se lee del abastecimiento InputStream, se escribiría en ambos OutputStream. No es necesario implementar eso, porque ya está hecho en TeeInputStream(commons.io).
  4. Dentro de un hilo separado, lea toda la fuente inputStream, e implícitamente los datos de entrada se transfieren a los inputStreams de destino.

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

Tenga cuidado de cerrar inputStreams después de ser consumido y cierre el hilo que se ejecuta: TeeInputStream.readAllBytes()

En caso de que necesite dividirlo en múltiplesInputStream , en lugar de solo dos. Reemplace en el fragmento de código anterior la clase TeeOutputStreampara su propia implementación, que encapsularía List<OutputStream>ay anularía la OutputStreaminterfaz:

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

Por favor, ¿podrías explicar un poco más el paso 4? ¿Por qué tenemos que activar la lectura manualmente? ¿Por qué la lectura de cualquiera de pipedInputStream NO activa la lectura de la fuente inputStream? ¿Y por qué hacemos esa llamada asíncronamente?
Дмитрий Кулешов

2

Convierta inputstream en bytes y luego páselo a la función savefile donde ensambla el mismo en inputstream. También en la función original, use bytes para otras tareas


55
Digo mala idea en este caso, la matriz resultante podría ser enorme y robará el dispositivo de memoria.
Kevin Parker

0

En caso de que alguien esté ejecutando una aplicación Spring Boot y desee leer el cuerpo de respuesta de un RestTemplate (por eso quiero leer una secuencia dos veces), hay una manera limpia (er) de hacerlo.

En primer lugar, debe usar Spring's StreamUtilspara copiar la secuencia a una Cadena:

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

Pero eso no es todo. También debe usar una fábrica de solicitudes que pueda almacenar en búfer la transmisión por usted, de esta manera:

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

O, si está utilizando el bean de fábrica, entonces (esto es Kotlin pero de todos modos):

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

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


0

Si está utilizando RestTemplate para hacer llamadas http Simplemente agregue un interceptor. El cuerpo de respuesta se almacena en caché por la implementación de ClientHttpResponse. Ahora inputstream se puede recuperar de respose tantas veces como sea necesario

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); 
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.