¿Cómo clonar un InputStream?

162

Tengo un InputStream que paso a un método para hacer algún procesamiento. Usaré el mismo InputStream en otro método, pero después del primer procesamiento, el InputStream parece estar cerrado dentro del método.

¿Cómo puedo clonar el InputStream para enviarlo al método que lo cierra? ¿Hay otra solución?

EDITAR: los métodos que cierran InputStream es un método externo de una lib. No tengo control sobre el cierre o no.

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";
    }
}
Renato Dinhani
fuente
2
¿Desea "restablecer" la secuencia después de que el método haya regresado? Es decir, leer la secuencia desde el principio?
aioobe
Sí, los métodos que cierran InputStream devuelven el juego de caracteres que fue codificado. El segundo método es convertir el InputStream a una Cadena usando el juego de caracteres encontrado en el primer método.
Renato Dinhani
En ese caso, deberías poder hacer lo que estoy describiendo en mi respuesta.
Kaj
No sé la mejor manera de resolverlo, pero resuelvo mi problema de otra manera. El método toString del Jericho HTML Parser devuelve la cadena formateada en el formato correcto. Es todo lo que necesito en este momento.
Renato Dinhani,

Respuestas:

188

Si todo lo que quiere hacer es leer la misma información más de una vez, y los datos de entrada son lo suficientemente pequeños como para caber en la memoria, puede copiar los datos desde su InputStreama un ByteArrayOutputStream .

Luego puede obtener la matriz de bytes asociada y abrir tantos ByteArrayInputStream "clonados" como desee.

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

Pero si realmente necesita mantener abierta la transmisión original para recibir nuevos datos, deberá realizar un seguimiento de este close()método externo y evitar que se llame de alguna manera.

ACTUALIZACIÓN (2019):

Desde Java 9, los bits medios se pueden reemplazar con InputStream.transferTo:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
input.transferTo(baos);
InputStream firstClone = new ByteArrayInputStream(baos.toByteArray()); 
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray()); 
Anthony Accioly
fuente
Busco otra solución a mi problema que no implica copiar InputStream, pero creo que si necesito copiar InputStream, esta es la mejor solución.
Renato Dinhani
77
Este enfoque consume memoria proporcional al contenido completo de la secuencia de entrada. Es mejor usar TeeInputStreamcomo se describe en la respuesta aquí .
aioobe
2
IOUtils (de apache commons) tiene un método de copia que haría la lectura / escritura del búfer en el medio de su código.
Rethab
31

Quieres usar Apache's CloseShieldInputStream :

Este es un contenedor que evitará que la secuencia se cierre. Harías algo como esto.

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();
Femi
fuente
Se ve bien, pero no funciona aquí. Editaré mi publicación con el código.
Renato Dinhani
CloseShieldno funciona porque su HttpURLConnectionflujo de entrada original está cerrado en alguna parte. ¿No debería su método llamar a IOUtils con la secuencia protegida IOUtils.toString(csContent,charset)?
Anthony Accioly
Quizás puede ser esto. ¿Puedo evitar que se cierre la HttpURLConnection?
Renato Dinhani
1
@Renato. Quizás el problema no sea la close()llamada en absoluto, sino el hecho de que Stream se está leyendo hasta el final. Dado mark()y reset()no son necesariamente los mejores métodos para conexiones HTTP, tal vez debería echar un vistazo a la matriz de bytes enfoque descrito en mi respuesta.
Anthony Accioly
1
Una cosa más, siempre puede abrir una nueva conexión a la misma URL. Ver aquí: stackoverflow.com/questions/5807340/…
Anthony Accioly
11

No puede clonarlo, y cómo va a resolver su problema depende de cuál sea la fuente de los datos.

Una solución es leer todos los datos del InputStream en una matriz de bytes, y luego crear un ByteArrayInputStream alrededor de esa matriz de bytes, y pasar esa secuencia de entrada a su método.

Edición 1: es decir, si el otro método también necesita leer los mismos datos. Es decir, desea "restablecer" la transmisión.

Kaj
fuente
No sé con qué parte necesitas ayuda. ¿Supongo que sabes leer de una transmisión? Lea todos los datos de InputStream y escriba los datos en ByteArrayOutputStream. Llame a ByByArray () en ByteArrayOutputStream una vez que haya completado la lectura de todos los datos. Luego pase esa matriz de bytes al constructor de un ByteArrayInputStream.
Kaj
8

Si los datos leídos de la transmisión son grandes, recomendaría usar un TeeInputStream de Apache Commons IO. De esa manera, esencialmente puede replicar la entrada y pasar una tubería t'd como su clon.

Nathan Ryan
fuente
5

Esto podría no funcionar en todas las situaciones, pero esto es lo que hice: extendí la clase FilterInputStream y realicé el procesamiento requerido de los bytes a medida que la biblioteca externa lee los datos.

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
    }

}

Luego, simplemente pasa una instancia de StreamBytesWithExtraProcessingInputStreamdónde habría pasado en la secuencia de entrada. Con la secuencia de entrada original como parámetro constructor.

Cabe señalar que esto funciona byte por byte, así que no lo use si se requiere un alto rendimiento.

Diederik
fuente
3

UPD Mira el comentario antes. No es exactamente lo que se le preguntó.

Si está utilizando apache.commons, puede copiar transmisiones utilizando IOUtils.

Puedes usar el siguiente código:

InputStream = IOUtils.toBufferedInputStream(toCopy);

Aquí está el ejemplo completo adecuado para su situación:

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

Este código requiere algunas dependencias:

MAVEN

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
</dependency>

GRADLE

'commons-io:commons-io:2.4'

Aquí está la referencia DOC para este método:

Obtiene todo el contenido de un InputStream y representa los mismos datos que el resultado InputStream. Este método es útil donde,

Source InputStream es lento. Tiene recursos de red asociados, por lo que no podemos mantenerlo abierto durante mucho tiempo. Tiene un tiempo de espera de red asociado.

Puede encontrar más información IOUtilsaquí: http://commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/IOUtils.html#toBufferedInputStream(java.io.InputStream)

Andrey E
fuente
77
Esto no clona la secuencia de entrada, sino que solo la almacena. Eso no es lo mismo; el OP quiere volver a leer (una copia de) la misma secuencia.
Raphael el
1

A continuación se muestra la solución con Kotlin.

Puede copiar su InputStream en ByteArray

val inputStream = ...

val byteOutputStream = ByteArrayOutputStream()
inputStream.use { input ->
    byteOutputStream.use { output ->
        input.copyTo(output)
    }
}

val byteInputStream = ByteArrayInputStream(byteOutputStream.toByteArray())

Si necesita leer byteInputStreamvarias veces, llame byteInputStream.reset()antes de volver a leer.

https://code.luasoftware.com/tutorials/kotlin/how-to-clone-inputstream/

Desmond Lua
fuente
0

La siguiente clase debería hacer el truco. Simplemente cree una instancia, llame al método de "multiplicación" y proporcione el flujo de entrada de origen y la cantidad de duplicados que necesita.

Importante: debe consumir todas las secuencias clonadas simultáneamente en subprocesos separados.

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();
                }
            }
        }
    }
}
codificador vstrom
fuente
No responde la pregunta. Quiere usar la secuencia en un método para determinar el juego de caracteres y luego volver a leerlo junto con su juego de caracteres en un segundo método.
Marqués de Lorne
0

La clonación de una secuencia de entrada podría no ser una buena idea, ya que esto requiere un conocimiento profundo sobre los detalles de la secuencia de entrada que se está clonando. Una solución para esto es crear una nueva secuencia de entrada que lea de la misma fuente nuevamente.

Entonces, usando algunas características de Java 8, esto se vería así:

public class Foo {

    private Supplier<InputStream> inputStreamSupplier;

    public void bar() {
        procesDataThisWay(inputStreamSupplier.get());
        procesDataTheOtherWay(inputStreamSupplier.get());
    }

    private void procesDataThisWay(InputStream) {
        // ...
    }

    private void procesDataTheOtherWay(InputStream) {
        // ...
    }
}

Este método tiene el efecto positivo de que reutilizará el código que ya está en su lugar: la creación de la secuencia de entrada encapsulada en inputStreamSupplier . Y no es necesario mantener una segunda ruta de código para la clonación de la secuencia.

Por otro lado, si la lectura de la transmisión es costosa (porque se realiza a través de una conexión de bajo ancho de banda), entonces este método duplicará los costos. Esto podría evitarse utilizando un proveedor específico que almacenará el contenido de la secuencia localmente primero y proporcionará un InputStreamrecurso local para ese momento.

SpaceTrucker
fuente
Esta respuesta no está clara para mí. ¿Cómo se inicializa el proveedor de un existente is?
user1156544
@ user1156544 Mientras escribía Clonar, una secuencia de entrada podría no ser una buena idea, porque esto requiere un conocimiento profundo sobre los detalles de la secuencia de entrada que se está clonando. no puede usar el proveedor para crear una secuencia de entrada desde una existente. El proveedor podría usar un java.io.Fileo, java.net.URLpor ejemplo, para crear una nueva secuencia de entrada cada vez que se invoca.
SpaceTrucker
Ya lo veo. Esto no funcionará con inputstream como el OP pregunta explícitamente, sino con File o URL si son la fuente original de datos. Gracias
user1156544