Come clonare un InputStream?


162

Ho un InputStream che passo a un metodo per eseguire alcune elaborazioni. Userò lo stesso InputStream in un altro metodo, ma dopo la prima elaborazione, InputStream sembra essere chiuso all'interno del metodo.

Come posso clonare InputStream per inviarlo al metodo che lo chiude? C'è un'altra soluzione?

EDIT: i metodi che chiudono InputStream è un metodo esterno da una lib. Non ho il controllo sulla chiusura o meno.

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

2
Vuoi "resettare" il flusso dopo che il metodo è tornato? Cioè, leggi il flusso dall'inizio?
aioobe,

Sì, i metodi che chiudono InputStream restituiscono il set di caratteri che è stato codificato. Il secondo metodo consiste nel convertire InputStream in una stringa utilizzando il set di caratteri trovato nel primo metodo.
Renato Dinhani,

In tal caso dovresti essere in grado di fare ciò che sto descrivendo nella mia risposta.
Kaj

Non conosco il modo migliore per risolverlo, ma altrimenti risolvo il mio problema. Il metodo toString del parser HTML Jericho restituisce la stringa formattata nel formato corretto. Al momento è tutto ciò di cui ho bisogno.
Renato Dinhani,

Risposte:


188

Se tutto ciò che vuoi fare è leggere le stesse informazioni più di una volta e i dati di input sono abbastanza piccoli da adattarsi alla memoria, puoi copiarli da InputStreamun ByteArrayOutputStream .

Quindi è possibile ottenere l'array di byte associato e aprire tutti i ByteArrayInputStream "clonati" desiderati.

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

Ma se hai davvero bisogno di mantenere aperto lo stream originale per ricevere nuovi dati, dovrai tenere traccia di questo close()metodo esterno e impedire che venga chiamato in qualche modo.

AGGIORNAMENTO (2019):

Da Java 9 i bit centrali possono essere sostituiti con InputStream.transferTo:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
input.transferTo(baos);
InputStream firstClone = new ByteArrayInputStream(baos.toByteArray()); 
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray()); 

Ho trovato un'altra soluzione al mio problema che non comporta la copia di InputStream, ma penso che se ho bisogno di copiare InputStream, questa è la soluzione migliore.
Renato Dinhani,

7
Questo approccio consuma memoria proporzionale al contenuto completo del flusso di input. Meglio usare TeeInputStreamcome descritto nella risposta qui .
aioobe,

2
IOUtils (dai comuni di Apache) ha un metodo di copia che farebbe leggere / scrivere il buffer nel mezzo del codice.
rethab

31

Vuoi usare Apache CloseShieldInputStream:

Questo è un wrapper che impedirà la chiusura del flusso. Faresti qualcosa del genere.

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

Sembra buono, ma non funziona qui. Modificherò il mio post con il codice.
Renato Dinhani,

CloseShieldnon funziona perché il HttpURLConnectionflusso di input originale viene chiuso da qualche parte. Il tuo metodo non dovrebbe chiamare IOUtils con il flusso protetto IOUtils.toString(csContent,charset)?
Anthony Accioly

Forse può essere questo. Posso impedire la chiusura di HttpURLConnection?
Renato Dinhani,

1
@Renato. Forse il problema non è affatto la close()chiamata, ma il fatto che lo Stream venga letto fino alla fine. Poiché mark()e reset()potrebbero non essere i metodi migliori per le connessioni http, forse dovresti dare un'occhiata all'approccio array di byte descritto nella mia risposta.
Anthony Accioly

1
Ancora una cosa, puoi sempre aprire una nuova connessione allo stesso URL. Vedi qui: stackoverflow.com/questions/5807340/…
Anthony Accioly

11

Non è possibile clonarlo e il modo in cui si risolverà il problema dipende dalla fonte dei dati.

Una soluzione è leggere tutti i dati dall'InputStream in una matrice di byte, quindi creare un ByteArrayInputStream attorno alla matrice di byte e passare quel flusso di input nel metodo.

Modifica 1: Cioè, se anche l'altro metodo deve leggere gli stessi dati. Vale a dire che si desidera "ripristinare" il flusso.


Non so con quale parte hai bisogno di aiuto. Immagino che tu sappia leggere da uno stream? Leggi tutti i dati da InputStream e scrivi i dati su ByteArrayOutputStream. Chiamare toByteArray () su ByteArrayOutputStream dopo aver completato la lettura di tutti i dati. Quindi passare quella matrice di byte nel costruttore di un ByteArrayInputStream.
Kaj

8

Se i dati letti dallo stream sono di grandi dimensioni, consiglierei di utilizzare un TeeInputStream di Apache Commons IO. In questo modo puoi essenzialmente replicare l'input e passare una pipe t'd come clone.


5

Questo potrebbe non funzionare in tutte le situazioni, ma ecco cosa ho fatto: ho esteso la classe FilterInputStream e faccio l'elaborazione richiesta dei byte mentre la lib esterna legge i dati.

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
    }

}

Quindi si passa semplicemente a un'istanza di StreamBytesWithExtraProcessingInputStreamdove si sarebbe passati nel flusso di input. Con il flusso di input originale come parametro del costruttore.

Va notato che funziona byte per byte, quindi non utilizzarlo se sono richieste prestazioni elevate.


3

UPD. Controlla il commento prima. Non è esattamente quello che è stato chiesto.

Se stai usando apache.commonspuoi copiare i flussi usando IOUtils.

Puoi usare il seguente codice:

InputStream = IOUtils.toBufferedInputStream(toCopy);

Ecco l'esempio completo adatto alla tua situazione:

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

Questo codice richiede alcune dipendenze:

ESPERTO DI

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

Gradle

'commons-io:commons-io:2.4'

Ecco il riferimento DOC per questo metodo:

Recupera l'intero contenuto di un InputStream e rappresenta gli stessi dati del risultato InputStream. Questo metodo è utile dove,

SourceStream Stream è lento. Ha risorse di rete associate, quindi non possiamo tenerlo aperto per lungo tempo. Ha un timeout di rete associato.

Puoi trovare ulteriori informazioni IOUtilsqui: http://commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/IOUtils.html#toBufferedInputStream(java.io.InputStream)


7
Questo non clona il flusso di input ma lo buffer. Non è lo stesso; l'OP vuole rileggere (una copia di) lo stesso flusso.
Raffaello,

1

Di seguito è la soluzione con Kotlin.

È possibile copiare InputStream in ByteArray

val inputStream = ...

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

val byteInputStream = ByteArrayInputStream(byteOutputStream.toByteArray())

Se è necessario leggere byteInputStreampiù volte, chiamare byteInputStream.reset()prima di rileggere.

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


0

La classe seguente dovrebbe fare il trucco. Basta creare un'istanza, chiamare il metodo "moltiplica" e fornire il flusso di input di origine e la quantità di duplicati necessari.

Importante: è necessario consumare tutti i flussi clonati contemporaneamente in thread separati.

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

Non risponde alla domanda Vuole utilizzare il flusso in un metodo per determinare il set di caratteri e quindi rileggerlo insieme al relativo set di caratteri in un secondo metodo.
Marchese di Lorne,

0

La clonazione di un flusso di input potrebbe non essere una buona idea, poiché ciò richiede una conoscenza approfondita dei dettagli del flusso di input da clonare. Una soluzione alternativa è quella di creare un nuovo flusso di input che legge nuovamente dalla stessa fonte.

Quindi, usando alcune funzionalità di Java 8 questo sarebbe simile al seguente:

public class Foo {

    private Supplier<InputStream> inputStreamSupplier;

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

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

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

Questo metodo ha l'effetto positivo di riutilizzare il codice già in atto: la creazione del flusso di input incapsulato inputStreamSupplier. E non è necessario mantenere un secondo percorso di codice per la clonazione del flusso.

D'altra parte, se la lettura dallo stream è costosa (perché viene eseguita su una banda bassa con connessione), questo metodo raddoppierà i costi. Ciò potrebbe essere eluso utilizzando un fornitore specifico che memorizzerà prima il contenuto dello stream localmente e fornirà una InputStreamrisorsa per quella ora locale.


Questa risposta non mi è chiara. Come si inizializza il fornitore da un esistente is?
user1156544

@ user1156544 Come ho scritto Clonare un flusso di input potrebbe non essere una buona idea, perché ciò richiede una profonda conoscenza dei dettagli del flusso di input da clonare. non è possibile utilizzare il fornitore per creare un flusso di input da uno esistente. Il fornitore può utilizzare a java.io.Fileo, java.net.URLad esempio, per creare un nuovo flusso di input ogni volta che viene invocato.
SpaceTrucker,

Ora vedo. Questo non funzionerà con inputstream come esplicitamente richiesto dall'OP, ma con File o URL se sono la fonte di dati originale. Grazie
user1156544
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.