Bagaimana cara mengkloning InputStream?

162

Saya memiliki InputStream yang saya berikan ke metode untuk melakukan pemrosesan. Saya akan menggunakan InputStream yang sama dalam metode lain, tetapi setelah pemrosesan pertama, InputStream tampaknya ditutup di dalam metode.

Bagaimana saya bisa mengkloning InputStream untuk mengirim ke metode yang menutupnya? Ada solusi lain?

EDIT: metode yang menutup InputStream adalah metode eksternal dari lib. Saya tidak punya kendali tentang penutupan atau tidak.

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
sumber
2
Apakah Anda ingin "mengatur ulang" aliran setelah metode kembali? Yaitu, baca aliran dari awal?
aioobe
Ya, metode yang menutup InputStream mengembalikan charset yang dikodekan. Metode kedua adalah mengubah InputStream ke String menggunakan charset yang ditemukan di metode pertama.
Renato Dinhani
Anda harus dalam hal itu dapat melakukan apa yang saya jelaskan dalam jawaban saya.
Kaj
Saya tidak tahu cara terbaik untuk menyelesaikannya, tetapi saya menyelesaikan masalah saya sebaliknya. Metode toString dari Jericho HTML Parser mengembalikan String yang diformat dalam format yang benar. Itu yang saya butuhkan saat ini.
Renato Dinhani

Jawaban:

188

Jika semua yang ingin Anda lakukan adalah membaca informasi yang sama lebih dari satu kali, dan data input cukup kecil untuk masuk ke dalam memori, Anda dapat menyalin data dari Anda InputStreamke ByteArrayOutputStream .

Kemudian Anda dapat memperoleh array terkait byte dan membuka sebanyak "kloning" ByteArrayInputStream s yang Anda inginkan.

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

Tetapi jika Anda benar-benar harus menjaga aliran asli terbuka untuk menerima data baru, maka Anda perlu melacak close()metode eksternal ini dan mencegahnya dipanggil entah bagaimana.

PEMBARUAN (2019):

Karena Java 9 bit tengah dapat diganti dengan InputStream.transferTo:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
input.transferTo(baos);
InputStream firstClone = new ByteArrayInputStream(baos.toByteArray()); 
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray()); 
Anthony Accioly
sumber
Saya menemukan solusi lain untuk masalah saya yang tidak melibatkan menyalin InputStream, tapi saya pikir jika saya perlu menyalin InputStream, ini adalah solusi terbaik.
Renato Dinhani
7
Pendekatan ini mengkonsumsi memori yang sebanding dengan konten penuh dari aliran input. Lebih baik digunakan TeeInputStreamseperti dijelaskan dalam jawaban di sini .
aioobe
2
IOUtils (dari apache commons) memiliki metode salin yang akan melakukan buffer baca / tulis di tengah kode Anda.
rethab
31

Anda ingin menggunakan Apache CloseShieldInputStream:

Ini adalah pembungkus yang akan mencegah aliran ditutup. Anda akan melakukan sesuatu seperti ini.

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
sumber
Terlihat bagus, tapi jangan bekerja di sini. Saya akan mengedit posting saya dengan kode tersebut.
Renato Dinhani
CloseShieldtidak berfungsi karena HttpURLConnectionaliran input awal Anda ditutup di suatu tempat. Bukankah seharusnya metode Anda memanggil IOUtils dengan aliran yang dilindungi IOUtils.toString(csContent,charset)?
Anthony Accioly
Mungkin bisa begini. Saya dapat mencegah HttpURLConnection ditutup?
Renato Dinhani
1
@Renato. Mungkin masalahnya bukanlah close()panggilan sama sekali, tetapi kenyataannya Stream sedang dibaca sampai akhir. Karena mark()dan reset()mungkin bukan metode terbaik untuk koneksi http, mungkin Anda harus melihat pendekatan byte array yang dijelaskan pada jawaban saya.
Anthony Accioly
1
Satu hal lagi, Anda selalu dapat membuka koneksi baru ke URL yang sama. Lihat di sini: stackoverflow.com/questions/5807340/...
Anthony Accioly
11

Anda tidak dapat mengkloningnya, dan bagaimana Anda akan memecahkan masalah Anda tergantung pada apa sumber data tersebut.

Salah satu solusinya adalah membaca semua data dari InputStream ke dalam array byte, dan kemudian membuat ByteArrayInputStream di sekitar array byte itu, dan meneruskan aliran input itu ke metode Anda.

Sunting 1: Yaitu, jika metode lain juga perlu membaca data yang sama. Yaitu Anda ingin "mengatur ulang" aliran.

Kaj
sumber
Saya tidak tahu bagian mana yang perlu Anda bantu. Saya kira Anda tahu cara membaca dari aliran? Baca semua data dari InputStream, dan tulis data ke ByteArrayOutputStream. Panggil toByteArray () di ByteArrayOutputStream setelah Anda selesai membaca semua data. Kemudian meneruskan byte array itu ke konstruktor ByteArrayInputStream.
Kaj
8

Jika data yang dibaca dari aliran besar, saya akan merekomendasikan menggunakan TeeInputStream dari Apache Commons IO. Dengan begitu Anda pada dasarnya dapat mereplikasi input dan mengirimkan pipa sebagai klon Anda.

Nathan Ryan
sumber
5

Ini mungkin tidak berfungsi dalam semua situasi, tetapi inilah yang saya lakukan: Saya memperluas kelas FilterInputStream dan melakukan pemrosesan byte yang diperlukan karena lib eksternal membaca data.

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
    }

}

Kemudian Anda hanya melewati contoh di StreamBytesWithExtraProcessingInputStreammana Anda akan lulus dalam aliran input. Dengan input stream asli sebagai parameter konstruktor.

Perlu dicatat bahwa ini berfungsi byte untuk byte, jadi jangan gunakan ini jika kinerja tinggi adalah persyaratan.

Diederik
sumber
3

UPD. Periksa komentar sebelumnya. Bukan apa yang diminta.

Jika Anda menggunakan, apache.commonsAnda dapat menyalin aliran menggunakan IOUtils.

Anda dapat menggunakan kode berikut:

InputStream = IOUtils.toBufferedInputStream(toCopy);

Berikut ini adalah contoh lengkap yang cocok untuk situasi Anda:

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

Kode ini memerlukan beberapa dependensi:

MAVEN

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

GRADLE

'commons-io:commons-io:2.4'

Berikut ini adalah referensi DOC untuk metode ini:

Mengambil seluruh konten InputStream dan merepresentasikan data yang sama dengan InputStream hasil. Metode ini berguna di mana,

Sumber InputStream lambat. Ini memiliki sumber daya jaringan yang terkait, jadi kami tidak dapat membuatnya terbuka untuk waktu yang lama. Ini memiliki batas waktu jaringan yang terkait.

Anda dapat menemukan lebih banyak tentang di IOUtilssini: http://commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/IOUtils.html#toBufferedInputStream(java.io.InputStream)

Andrey E
sumber
7
Ini tidak mengklon input stream tetapi hanya buffer itu. Itu tidak sama; OP ingin membaca kembali (salinan) aliran yang sama.
Raphael
1

Di bawah ini solusinya dengan Kotlin.

Anda dapat menyalin InputStream Anda ke ByteArray

val inputStream = ...

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

val byteInputStream = ByteArrayInputStream(byteOutputStream.toByteArray())

Jika Anda perlu membaca byteInputStreamberulang kali, hubungi byteInputStream.reset()sebelum membaca lagi.

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

Desmond Lua
sumber
0

Kelas di bawah ini harus melakukan trik. Buat saja instance, panggil metode "multiply", dan berikan aliran input sumber dan jumlah duplikat yang Anda butuhkan.

Penting: Anda harus mengonsumsi semua aliran hasil kloning secara bersamaan di utas terpisah.

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
sumber
Tidak menjawab pertanyaan. Dia ingin menggunakan stream dalam satu metode untuk menentukan charset dan kemudian membacanya kembali bersama charsetnya dalam metode kedua.
Marquis of Lorne
0

Mengkloning aliran input mungkin bukan ide yang baik, karena ini membutuhkan pengetahuan mendalam tentang detail aliran input yang dikloning. Solusi untuk ini adalah membuat aliran input baru yang membaca dari sumber yang sama lagi.

Jadi menggunakan beberapa fitur Java 8 ini akan terlihat seperti ini:

public class Foo {

    private Supplier<InputStream> inputStreamSupplier;

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

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

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

Metode ini memiliki efek positif sehingga akan menggunakan kembali kode yang sudah ada - pembuatan input stream dienkapsulasi inputStreamSupplier. Dan tidak perlu mempertahankan jalur kode kedua untuk kloning aliran.

Di sisi lain, jika membaca dari sungai itu mahal (karena itu dilakukan melalui koneksi bandwidth rendah), maka metode ini akan menggandakan biaya. Ini dapat dielakkan dengan menggunakan pemasok tertentu yang akan menyimpan konten stream secara lokal terlebih dahulu dan menyediakan InputStreamsumber daya yang sekarang lokal.

SpaceTrucker
sumber
Jawaban ini tidak jelas bagi saya. Bagaimana Anda menginisialisasi pemasok dari yang sudah ada is?
user1156544
@ user1156544 Saat saya menulis Mengkloning input stream mungkin bukan ide yang baik, karena ini membutuhkan pengetahuan mendalam tentang detail stream input yang dikloning. Anda tidak dapat menggunakan pemasok untuk membuat aliran input dari yang sudah ada. Pemasok dapat menggunakan java.io.Fileatau java.net.URLmisalnya untuk membuat aliran input baru setiap kali dipanggil.
SpaceTrucker
Saya mengerti sekarang. Ini tidak akan bekerja dengan inputstream seperti yang diminta OP secara eksplisit, tetapi dengan File atau URL jika mereka adalah sumber data asli. Terima kasih
user1156544