Jak sklonować InputStream?

162

Mam InputStream, który przekazuję do metody, aby wykonać pewne przetwarzanie. Użyję tego samego InputStream w innej metodzie, ale po pierwszym przetworzeniu InputStream wydaje się być zamknięty wewnątrz metody.

Jak mogę sklonować InputStream, aby wysłać go do metody, która go zamyka? Czy jest inne rozwiązanie?

EDYCJA: metody, które zamykają InputStream to metoda zewnętrzna z biblioteki. Nie mam kontroli nad zamykaniem, czy nie.

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
źródło
2
Czy chcesz „zresetować” strumień po zwróceniu metody? Czyli czytać strumień od początku?
aioobe
Tak, metody zamykające InputStream zwracają zestaw znaków, który został zakodowany. Druga metoda polega na przekonwertowaniu InputStream na String przy użyciu zestawu znaków znalezionego w pierwszej metodzie.
Renato Dinhani
W takim przypadku powinieneś być w stanie zrobić to, co opisuję w mojej odpowiedzi.
Kaj
Nie znam najlepszego sposobu rozwiązania tego problemu, ale w inny sposób rozwiązuję swój problem. Metoda toString Jericho HTML Parser zwraca ciąg sformatowany w odpowiednim formacie. W tej chwili to wszystko, czego potrzebuję.
Renato Dinhani

Odpowiedzi:

188

Jeśli wszystko, co chcesz zrobić, to przeczytać te same informacje więcej niż raz, a dane wejściowe są wystarczająco małe, aby zmieścić się w pamięci, możesz skopiować dane ze swojego InputStreamdo ByteArrayOutputStream .

Następnie możesz uzyskać powiązaną tablicę bajtów i otworzyć tyle „sklonowanych” ByteArrayInputStream, ile chcesz.

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

Ale jeśli naprawdę chcesz pozostawić oryginalny strumień otwarty, aby otrzymywać nowe dane, będziesz musiał śledzić ten zewnętrzny close() metodę i w jakiś sposób zapobiec jej wywołaniu.

AKTUALIZACJA (2019):

Od wersji Java 9 środkowe bity można zastąpić InputStream.transferTo:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
input.transferTo(baos);
InputStream firstClone = new ByteArrayInputStream(baos.toByteArray()); 
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray()); 
Anthony Accioly
źródło
Znalazłem inne rozwiązanie mojego problemu, które nie polega na kopiowaniu InputStream, ale myślę, że jeśli potrzebuję skopiować InputStream, to jest najlepsze rozwiązanie.
Renato Dinhani
7
Takie podejście zużywa pamięć proporcjonalnie do pełnej zawartości strumienia wejściowego. Lepiej użyć, TeeInputStreamjak opisano w odpowiedzi tutaj .
aioobe
2
IOUtils (z apache commons) ma metodę kopiowania, która czyta / zapisuje bufor w środku twojego kodu.
rethab
31

Chcesz użyć Apache CloseShieldInputStream:

To jest opakowanie, które zapobiegnie zamknięciu strumienia. Zrobiłbyś coś takiego.

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
źródło
Wygląda dobrze, ale tutaj nie działa. Będę edytować swój post za pomocą kodu.
Renato Dinhani
CloseShieldnie działa, ponieważ pierwotny HttpURLConnectionstrumień wejściowy jest gdzieś zamknięty. Czy twoja metoda nie powinna wywoływać IOUtils z chronionym strumieniem IOUtils.toString(csContent,charset)?
Anthony Accioly
Może to być. Mogę zapobiec zamknięciu HttpURLConnection?
Renato Dinhani
1
@Renato. Może problemem wcale nie jest close()wezwanie, ale fakt, że Stream jest odczytywany do końca. Ponieważ mark()i reset()może nie być najlepszą metodą połączeń http, może powinieneś przyjrzeć się podejściu do tablicy bajtów opisanej w mojej odpowiedzi.
Anthony Accioly
1
I jeszcze jedno, zawsze możesz otworzyć nowe połączenie z tym samym adresem URL. Zobacz tutaj: stackoverflow.com/questions/5807340/…
Anthony Accioly
11

Nie możesz go sklonować, a sposób rozwiązania problemu zależy od źródła danych.

Jednym z rozwiązań jest odczytanie wszystkich danych z InputStream do tablicy bajtów, a następnie utworzenie ByteArrayInputStream wokół tej tablicy bajtów i przekazanie tego strumienia wejściowego do metody.

Edycja 1: to znaczy, jeśli druga metoda również musi odczytać te same dane. To znaczy chcesz „zresetować” strumień.

Kaj
źródło
Nie wiem, w jakiej części potrzebujesz pomocy. Chyba wiesz, jak czytać ze strumienia? Odczytaj wszystkie dane z InputStream i zapisz je do ByteArrayOutputStream. Wywołaj toByteArray () na ByteArrayOutputStream po zakończeniu odczytu wszystkich danych. Następnie przekaż tę tablicę bajtów do konstruktora obiektu ByteArrayInputStream.
Kaj
8

Jeśli dane odczytane ze strumienia są duże, polecam użycie TeeInputStream z Apache Commons IO. W ten sposób możesz zasadniczo powielić dane wejściowe i przekazać potok t'd jako klon.

Nathan Ryan
źródło
5

To może nie działać we wszystkich sytuacjach, ale oto, co zrobiłem: rozszerzyłem klasę FilterInputStream i wykonałem wymagane przetwarzanie bajtów, gdy zewnętrzna biblioteka odczytuje dane.

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
    }

}

Następnie po prostu przekazujesz wystąpienie miejsca, w StreamBytesWithExtraProcessingInputStreamktórym przeszedłbyś w strumieniu wejściowym. Z oryginalnym strumieniem wejściowym jako parametrem konstruktora.

Należy zauważyć, że działa to bajt po bajcie, więc nie używaj tego, jeśli wymagana jest wysoka wydajność.

Diederik
źródło
3

UPD. Sprawdź komentarz wcześniej. To nie jest dokładnie to, o co pytano.

Jeśli używasz apache.commons, możesz kopiować strumienie za pomocąIOUtils .

Możesz użyć następującego kodu:

InputStream = IOUtils.toBufferedInputStream(toCopy);

Oto pełny przykład odpowiedni dla Twojej sytuacji:

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

Ten kod wymaga pewnych zależności:

MAVEN

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

GRADLE

'commons-io:commons-io:2.4'

Oto odniesienie DOC dla tej metody:

Pobiera całą zawartość InputStream i reprezentuje te same dane co wynik InputStream. Ta metoda jest przydatna, gdy

Źródłowy strumień wejściowy jest wolny. Ma powiązane zasoby sieciowe, więc nie możemy go długo otwierać. Ma powiązany limit czasu sieci.

Więcej na ten temat można znaleźć IOUtilstutaj: http://commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/IOUtils.html#toBufferedInputStream(java.io.InputStream)

Andrey E.
źródło
7
To nie klonuje strumienia wejściowego, ale tylko go buforuje. To nie to samo; OP chce ponownie przeczytać (kopię) ten sam strumień.
Raphael
1

Poniżej znajduje się rozwiązanie z Kotlinem.

Możesz skopiować swój InputStream do ByteArray

val inputStream = ...

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

val byteInputStream = ByteArrayInputStream(byteOutputStream.toByteArray())

Jeśli chcesz przeczytać byteInputStreamkilka razy, zadzwoń byteInputStream.reset()przed ponownym przeczytaniem.

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

Desmond Lua
źródło
0

Poniższa klasa powinna załatwić sprawę. Po prostu utwórz instancję, wywołaj metodę „multiply” i podaj źródłowy strumień wejściowy oraz wymaganą liczbę duplikatów.

Ważne: musisz używać wszystkich sklonowanych strumieni jednocześnie w oddzielnych wątkach.

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();
                }
            }
        }
    }
}
koder vstrom
źródło
Nie odpowiada na pytanie. Chce użyć strumienia w jednej metodzie, aby określić zestaw znaków, a następnie ponownie odczytać go wraz z zestawem znaków w drugiej metodzie.
Markiz Lorne
0

Klonowanie strumienia wejściowego może nie być dobrym pomysłem, ponieważ wymaga to dogłębnej wiedzy na temat szczegółów klonowanego strumienia wejściowego. Aby obejść ten problem, należy utworzyć nowy strumień wejściowy, który ponownie odczytuje dane z tego samego źródła.

Tak więc przy użyciu niektórych funkcji Java 8 wyglądałoby to tak:

public class Foo {

    private Supplier<InputStream> inputStreamSupplier;

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

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

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

Ta metoda ma pozytywny wpływ na to, że ponownie użyje kodu, który jest już na miejscu - utworzenie strumienia wejściowego hermetyzowanego w inputStreamSupplier. Nie ma też potrzeby utrzymywania drugiej ścieżki kodu do klonowania strumienia.

Z drugiej strony, jeśli odczytywanie ze strumienia jest drogie (ponieważ odbywa się to przez połączenie o niskiej przepustowości), ta metoda podwoi koszty. Można to obejść, używając określonego dostawcy, który najpierw zapisze zawartość strumienia lokalnie i zapewni InputStreamdla tego teraz lokalny zasób.

SpaceTrucker
źródło
Ta odpowiedź nie jest dla mnie jasna. Jak zainicjować dostawcę z istniejącego is?
user1156544
@ user1156544 Tak jak pisałem Klonowanie strumienia wejściowego może nie być dobrym pomysłem, ponieważ wymaga to głębokiej wiedzy na temat szczegółów klonowanego strumienia wejściowego. nie możesz użyć dostawcy do utworzenia strumienia wejściowego z istniejącego. Dostawca może użyć java.io.Filelub, java.net.URLna przykład, do utworzenia nowego strumienia wejściowego za każdym razem, gdy jest on wywoływany.
SpaceTrucker,
Teraz widzę. Nie będzie to działać w przypadku strumienia wejściowego, o co wyraźnie prosi OP, ale z plikiem lub adresem URL, jeśli są one oryginalnym źródłem danych. Dzięki
user1156544