Wie klone ich einen InputStream?

162

Ich habe einen InputStream, den ich an eine Methode übergebe, um etwas zu verarbeiten. Ich werde den gleichen InputStream in einer anderen Methode verwenden, aber nach der ersten Verarbeitung scheint der InputStream innerhalb der Methode geschlossen zu sein.

Wie kann ich den InputStream klonen, um ihn an die Methode zu senden, die ihn schließt? Gibt es eine andere Lösung?

BEARBEITEN: Die Methoden, die den InputStream schließen, sind eine externe Methode aus einer Bibliothek. Ich habe keine Kontrolle über das Schließen oder nicht.

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
quelle
1
Möchten Sie den Stream nach der Rückkehr der Methode "zurücksetzen"? Dh den Stream von Anfang an lesen?
Aioobe
Ja, die Methoden, die den InputStream schließen, geben den Zeichensatz zurück, den er codiert hat. Die zweite Methode besteht darin, den InputStream mithilfe des in der ersten Methode gefundenen Zeichensatzes in einen String zu konvertieren.
Renato Dinhani
In diesem Fall sollten Sie in der Lage sein, das zu tun, was ich in meiner Antwort beschreibe.
Kaj
Ich weiß nicht, wie ich es am besten lösen kann, aber ich löse mein Problem anders. Die Methode toString des Jericho HTML-Parsers gibt den im richtigen Format formatierten String zurück. Es ist alles was ich im Moment brauche.
Renato Dinhani

Antworten:

188

Wenn Sie nur dieselben Informationen mehrmals lesen möchten und die Eingabedaten klein genug sind, um in den Speicher zu passen, können Sie die Daten von Ihrem InputStreamin einen ByteArrayOutputStream kopieren .

Dann können Sie das zugehörige Array von Bytes erhalten und so viele "geklonte" ByteArrayInputStreams öffnen, wie Sie möchten .

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

Wenn Sie jedoch den ursprünglichen Stream wirklich offen halten müssen, um neue Daten zu erhalten, müssen Sie diese externe close()Methode verfolgen und verhindern, dass sie irgendwie aufgerufen wird.

UPDATE (2019):

Seit Java 9 können die mittleren Bits ersetzt werden durch InputStream.transferTo:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
input.transferTo(baos);
InputStream firstClone = new ByteArrayInputStream(baos.toByteArray()); 
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray()); 
Anthony Accioly
quelle
Ich stelle eine andere Lösung für mein Problem vor, bei der der InputStream nicht kopiert wird. Ich denke jedoch, dass dies die beste Lösung ist, wenn ich den InputStream kopieren muss.
Renato Dinhani
7
Dieser Ansatz verbraucht Speicher proportional zum vollen Inhalt des Eingabestreams. Besser TeeInputStreamwie in der Antwort hier beschrieben verwenden .
Aioobe
2
IOUtils (von Apache Commons) verfügt über eine Kopiermethode, mit der der Puffer in der Mitte Ihres Codes gelesen / geschrieben wird.
Rethab
31

Sie möchten Apaches verwenden CloseShieldInputStream:

Dies ist ein Wrapper, der verhindert, dass der Stream geschlossen wird. Du würdest so etwas tun.

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
quelle
Sieht gut aus, funktioniert aber hier nicht. Ich werde meinen Beitrag mit dem Code bearbeiten.
Renato Dinhani
CloseShieldfunktioniert nicht, weil Ihr ursprünglicher HttpURLConnectionEingabestream irgendwo geschlossen wird. Sollte Ihre Methode nicht IOUtils mit dem geschützten Stream aufrufen IOUtils.toString(csContent,charset)?
Anthony Accioly
Vielleicht kann das sein. Kann ich verhindern, dass die HttpURLConnection geschlossen wird?
Renato Dinhani
1
@ Renato. Vielleicht ist das Problem überhaupt nicht der close()Anruf, sondern die Tatsache, dass der Stream bis zum Ende gelesen wird. Da mark()und reset()nicht die besten Methoden für HTTP - Verbindungen sein kann, sollten Sie vielleicht einen Blick auf dem Byte - Array - Ansatz auf meiner Antwort beschrieben.
Anthony Accioly
1
Außerdem können Sie jederzeit eine neue Verbindung zu derselben URL herstellen. Siehe hier: stackoverflow.com/questions/5807340/…
Anthony Accioly
11

Sie können es nicht klonen, und wie Sie Ihr Problem lösen, hängt von der Datenquelle ab.

Eine Lösung besteht darin, alle Daten aus dem InputStream in ein Byte-Array zu lesen, dann einen ByteArrayInputStream um dieses Byte-Array herum zu erstellen und diesen Eingabestream an Ihre Methode zu übergeben.

Bearbeiten 1: Das heißt, wenn die andere Methode ebenfalls dieselben Daten lesen muss. Dh Sie möchten den Stream "zurücksetzen".

Kaj
quelle
Ich weiß nicht, bei welchem ​​Teil Sie Hilfe brauchen. Ich denke du weißt wie man aus einem Stream liest? Lesen Sie alle Daten aus dem InputStream und schreiben Sie die Daten in ByteArrayOutputStream. Rufen Sie toByteArray () im ByteArrayOutputStream auf, nachdem Sie alle Daten gelesen haben. Übergeben Sie dann dieses Byte-Array an den Konstruktor eines ByteArrayInputStream.
Kaj
8

Wenn die aus dem Stream gelesenen Daten groß sind, würde ich die Verwendung eines TeeInputStream von Apache Commons IO empfehlen. Auf diese Weise können Sie die Eingabe im Wesentlichen replizieren und eine t'd-Pipe als Klon übergeben.

Nathan Ryan
quelle
5

Dies funktioniert möglicherweise nicht in allen Situationen, aber ich habe Folgendes getan: Ich habe die FilterInputStream- Klasse erweitert und die erforderliche Verarbeitung der Bytes durchgeführt, während die externe Bibliothek die Daten liest.

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
    }

}

Dann übergeben Sie einfach eine Instanz, an der StreamBytesWithExtraProcessingInputStreamSie im Eingabestream übergeben hätten. Mit dem ursprünglichen Eingabestream als Konstruktorparameter.

Es sollte beachtet werden, dass dies Byte für Byte funktioniert. Verwenden Sie dies also nicht, wenn eine hohe Leistung erforderlich ist.

Diederik
quelle
3

UPD. Überprüfen Sie den Kommentar vor. Es ist nicht genau das, was gefragt wurde.

Wenn Sie verwenden apache.commons, können Sie Streams mit kopieren IOUtils.

Sie können folgenden Code verwenden:

InputStream = IOUtils.toBufferedInputStream(toCopy);

Hier ist das vollständige Beispiel, das für Ihre Situation geeignet ist:

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

Dieser Code erfordert einige Abhängigkeiten:

MAVEN

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

GRADLE

'commons-io:commons-io:2.4'

Hier ist die DOC-Referenz für diese Methode:

Ruft den gesamten Inhalt eines InputStream ab und repräsentiert dieselben Daten wie das Ergebnis InputStream. Diese Methode ist nützlich, wenn:

Source InputStream ist langsam. Es sind Netzwerkressourcen zugeordnet, sodass wir es nicht lange offen halten können. Es ist ein Netzwerk-Timeout zugeordnet.

Weitere Informationen finden Sie IOUtilshier: http://commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/IOUtils.html#toBufferedInputStream(java.io.InputStream)

Andrey E.
quelle
7
Dadurch wird der Eingabestream nicht geklont, sondern nur gepuffert. Das ist nicht dasselbe; Das OP möchte denselben Stream erneut lesen (eine Kopie davon).
Raphael
1

Unten ist die Lösung mit Kotlin.

Sie können Ihren InputStream in ByteArray kopieren

val inputStream = ...

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

val byteInputStream = ByteArrayInputStream(byteOutputStream.toByteArray())

Wenn Sie das byteInputStreammehrmals lesen müssen , rufen Sie an, byteInputStream.reset()bevor Sie es erneut lesen.

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

Desmond Lua
quelle
0

Die Klasse unten sollte den Trick machen. Erstellen Sie einfach eine Instanz, rufen Sie die "Multiplikations" -Methode auf und geben Sie den Quelleneingabestream und die Anzahl der benötigten Duplikate an.

Wichtig: Sie müssen alle geklonten Streams gleichzeitig in separaten Threads verwenden.

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();
                }
            }
        }
    }
}
vstrom coder
quelle
Beantwortet die Frage nicht. Er möchte den Stream in einer Methode verwenden, um den Zeichensatz zu bestimmen, und ihn dann zusammen mit dem Zeichensatz in einer zweiten Methode erneut lesen.
Marquis von Lorne
0

Das Klonen eines Eingabestreams ist möglicherweise keine gute Idee, da hierfür fundierte Kenntnisse über die Details des zu klonenden Eingabestreams erforderlich sind. Eine Problemumgehung besteht darin, einen neuen Eingabestream zu erstellen, der erneut aus derselben Quelle liest.

Wenn Sie also einige Java 8-Funktionen verwenden, sieht dies folgendermaßen aus:

public class Foo {

    private Supplier<InputStream> inputStreamSupplier;

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

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

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

Diese Methode hat den positiven Effekt, dass bereits vorhandener Code wiederverwendet wird - die Erstellung des eingekapselten Eingabestreams inputStreamSupplier. Für das Klonen des Streams muss kein zweiter Codepfad verwaltet werden.

Wenn andererseits das Lesen aus dem Stream teuer ist (weil es über eine Verbindung mit geringer Bandbreite erfolgt), verdoppelt diese Methode die Kosten. Dies könnte umgangen werden, indem ein bestimmter Anbieter verwendet wird, der den Stream-Inhalt zuerst lokal speichert und eine InputStreamfür diese jetzt lokale Ressource bereitstellt .

SpaceTrucker
quelle
Diese Antwort ist mir nicht klar. Wie initialisieren Sie den Lieferanten von einem bestehenden is?
user1156544
@ user1156544 Wie ich geschrieben habe, ist das Klonen eines Eingabestreams möglicherweise keine gute Idee, da dies fundierte Kenntnisse über die Details des zu klonenden Eingabestreams erfordert. Sie können den Lieferanten nicht verwenden, um einen Eingabestream aus einem vorhandenen zu erstellen. Der Lieferant kann beispielsweise ein java.io.Fileoder verwenden java.net.URL, um bei jedem Aufruf einen neuen Eingabestream zu erstellen.
SpaceTrucker
Ich sehe jetzt. Dies funktioniert nicht mit Inputstream, wie das OP explizit fragt, sondern mit Datei oder URL, wenn sie die ursprüngliche Datenquelle sind. Danke
user1156544