จะโคลน InputStream ได้อย่างไร


162

ฉันมี InputStream ที่ฉันผ่านไปยังวิธีการที่จะทำการประมวลผลบางอย่าง ฉันจะใช้ InputStream เดียวกันในวิธีอื่น แต่หลังจากการประมวลผลครั้งแรก InputStream จะปรากฏขึ้นภายในเมธอด

ฉันจะโคลน InputStream เพื่อส่งไปยังวิธีที่ปิดเขาได้อย่างไร มีวิธีแก้ไขปัญหาอื่นหรือไม่?

แก้ไข: วิธีการที่ปิด InputStream เป็นวิธีการภายนอกจาก lib ฉันไม่สามารถควบคุมการปิดได้หรือไม่

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
คุณต้องการที่จะ "รีเซ็ต" กระแสหลังจากวิธีการที่ได้กลับมา? คืออ่านกระแสจากจุดเริ่มต้นหรือไม่
aioobe

ใช่วิธีการที่ปิด InputStream จะส่งกลับชุดอักขระที่ถูกเข้ารหัส วิธีที่สองคือการแปลง InputStream เป็น String โดยใช้ชุดอักขระที่พบในวิธีแรก
Renato Dinhani

ในกรณีนี้คุณควรจะสามารถทำสิ่งที่ฉันอธิบายในคำตอบของฉัน
Kaj

ฉันไม่รู้วิธีที่ดีที่สุดในการแก้ไข แต่ฉันแก้ไขปัญหาของฉันเป็นอย่างอื่น เมธอด toString ของ Jericho HTML Parser ส่งคืนสตริงที่จัดรูปแบบในรูปแบบที่ถูกต้อง มันคือทั้งหมดที่ฉันต้องการในขณะนี้
Renato Dinhani

คำตอบ:


188

หากสิ่งที่คุณต้องการจะทำคืออ่านข้อมูลเดียวกันมากกว่าหนึ่งครั้งและป้อนข้อมูลก็พอขนาดเล็กที่จะพอดีกับหน่วยความจำคุณสามารถคัดลอกข้อมูลจากคุณInputStreamไปByteArrayOutputStream

จากนั้นคุณสามารถรับอาร์เรย์ที่เกี่ยวข้องของไบต์และเปิดByteArrayInputStream "โคลน" ได้มากเท่าที่คุณต้องการ

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

แต่ถ้าคุณต้องการให้สตรีมดั้งเดิมเปิดอยู่เพื่อรับข้อมูลใหม่คุณจะต้องติดตามclose()วิธีการภายนอกนี้และป้องกันไม่ให้ถูกเรียกอย่างใด

อัปเดต (2019):

ตั้งแต่ Java 9 บิตกลางสามารถถูกแทนที่ด้วยInputStream.transferTo:

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

ฉันพบวิธีแก้ไขปัญหาอื่นที่ไม่เกี่ยวข้องกับการคัดลอก InputStream แต่ฉันคิดว่าถ้าฉันต้องการคัดลอก InputStream นี่เป็นทางออกที่ดีที่สุด
Renato Dinhani

7
วิธีการนี้ใช้หน่วยความจำตามสัดส่วนกับเนื้อหาทั้งหมดของอินพุตสตรีม ดีกว่าการใช้TeeInputStreamตามที่อธิบายไว้ในคำตอบมากกว่าที่นี่
aioobe

2
IOUtils (จาก apache ทั่วไป) มีวิธีการคัดลอกซึ่งจะทำการอ่าน / เขียนบัฟเฟอร์ในกลางรหัสของคุณ
rethab

31

คุณต้องการใช้ของ Apache CloseShieldInputStream:

นี่คือเสื้อคลุมที่จะป้องกันไม่ให้กระแสถูกปิด คุณต้องการทำอะไรเช่นนี้

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

ดูดี แต่ไม่ทำงานที่นี่ ฉันจะแก้ไขโพสต์ของฉันด้วยรหัส
Renato Dinhani

CloseShieldไม่ทำงานเนื่องจากHttpURLConnectionสตรีมอินพุตดั้งเดิมของคุณกำลังปิดอยู่ที่ไหนสักแห่ง เมธอดของคุณไม่ควรเรียกใช้ IOU กับสตรีมที่ป้องกันIOUtils.toString(csContent,charset)หรือไม่
Anthony Accioly

อาจเป็นได้ ฉันสามารถป้องกัน HttpURLConnection ได้ไหม
Renato Dinhani

1
@Renato บางทีปัญหาอาจไม่ใช่การclose()โทรเลย แต่ความจริงแล้วสตรีมกำลังอ่านจนจบ เนื่องจากmark()และreset()อาจไม่ใช่วิธีที่ดีที่สุดสำหรับการเชื่อมต่อ http บางทีคุณควรดูที่วิธีอาร์เรย์ไบต์ที่อธิบายไว้ในคำตอบของฉัน
Anthony Accioly

1
อีกอย่างหนึ่งคุณสามารถเปิดการเชื่อมต่อใหม่ไปยัง URL เดียวกันได้ตลอดเวลา ดูที่นี่: stackoverflow.com/questions/5807340/…
Anthony Accioly

11

คุณไม่สามารถโคลนมันได้และวิธีการที่คุณจะแก้ปัญหาของคุณขึ้นอยู่กับแหล่งที่มาของข้อมูล

ทางออกหนึ่งคือการอ่านข้อมูลทั้งหมดจาก InputStream ไปยังอาร์เรย์ไบต์แล้วสร้าง ByteArrayInputStream รอบอาร์เรย์ไบต์นั้นและส่งกระแสข้อมูลนั้นลงในวิธีการของคุณ

แก้ไข 1: นั่นคือถ้าวิธีอื่นต้องอ่านข้อมูลเดียวกัน เช่นคุณต้องการ "รีเซ็ต" สตรีม


ฉันไม่รู้ว่าคุณต้องการความช่วยเหลือในส่วนใด ฉันเดาว่าคุณรู้วิธีอ่านจากสตรีมหรือไม่ อ่านข้อมูลทั้งหมดจาก InputStream และเขียนข้อมูลไปยัง ByteArrayOutputStream โทรไปที่ ByteArray () บน ByteArrayOutputStream หลังจากที่คุณอ่านข้อมูลทั้งหมดเสร็จแล้ว จากนั้นส่งผ่านอาร์เรย์ไบต์นั้นไปยังตัวสร้างของ ByteArrayInputStream
Kaj

8

หากข้อมูลที่อ่านจากสตรีมมีขนาดใหญ่ฉันขอแนะนำให้ใช้ TeeInputStream จาก Apache Commons IO ด้วยวิธีนี้คุณสามารถทำซ้ำอินพุตและส่งไพพ์ไปเป็นโคลนของคุณ


5

สิ่งนี้อาจใช้งานไม่ได้ในทุกสถานการณ์ แต่นี่คือสิ่งที่ฉันทำ: ฉันขยายคลาสFilterInputStreamและทำการประมวลผลที่จำเป็นของไบต์เนื่องจาก lib ภายนอกอ่านข้อมูล

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
    }

}

จากนั้นคุณก็ผ่านตัวอย่างของการStreamBytesWithExtraProcessingInputStreamที่คุณจะได้ผ่านในกระแสข้อมูล ด้วยอินพุตสตรีมดั้งเดิมเป็นพารามิเตอร์ตัวสร้าง

ควรสังเกตว่าการทำงานนี้เป็นไบต์สำหรับไบต์ดังนั้นอย่าใช้สิ่งนี้หากประสิทธิภาพสูงเป็นข้อกำหนด


3

UPD ตรวจสอบความคิดเห็นก่อน มันไม่ใช่สิ่งที่ถูกถาม

หากคุณกำลังใช้คุณอาจคัดลอกลำธารใช้apache.commonsIOUtils

คุณสามารถใช้รหัสต่อไปนี้:

InputStream = IOUtils.toBufferedInputStream(toCopy);

นี่คือตัวอย่างเต็มรูปแบบที่เหมาะสมกับสถานการณ์ของคุณ:

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

รหัสนี้ต้องการการอ้างอิงบางอย่าง:

MAVEN

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

GRADLE

'commons-io:commons-io:2.4'

นี่คือการอ้างอิง DOC สำหรับวิธีนี้:

ดึงเนื้อหาทั้งหมดของ InputStream และแสดงข้อมูลเดียวกันกับผลลัพธ์ InputStream วิธีนี้มีประโยชน์ตรงไหน

Source InputStream ช้า มีทรัพยากรเครือข่ายที่เกี่ยวข้องดังนั้นเราจึงไม่สามารถเปิดได้นาน มีการหมดเวลาเครือข่ายที่เกี่ยวข้อง

คุณสามารถหาข้อมูลเพิ่มเติมเกี่ยวกับIOUtilsที่นี่: http://commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/IOUtils.html#toBufferedInputStream(java.io.InputStream)


7
นี่ไม่ได้โคลนสตรีมอินพุต แต่จะบัฟเฟอร์เท่านั้น มันไม่เหมือนกัน OP ต้องการอ่านอีกครั้ง (สำเนา) สตรีมเดียวกัน
Raphael

1

ด้านล่างเป็นวิธีแก้ปัญหาด้วย Kotlin

คุณสามารถคัดลอก InputStream ของคุณลงใน ByteArray

val inputStream = ...

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

val byteInputStream = ByteArrayInputStream(byteOutputStream.toByteArray())

หากคุณจำเป็นต้องอ่านbyteInputStreamหลาย ๆ ครั้งให้โทรbyteInputStream.reset()ก่อนอ่านอีกครั้ง

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


0

ชั้นด้านล่างควรทำเคล็ดลับ เพียงแค่สร้างอินสแตนซ์เรียกเมธอด "ทวีคูณ" แล้วระบุแหล่งอินพุตสตรีมและจำนวนการทำซ้ำที่คุณต้องการ

สำคัญ: คุณต้องใช้สตรีมที่โคลนทั้งหมดพร้อมกันในเธรดแยกต่างหาก

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

ไม่ตอบคำถาม เขาต้องการใช้สตรีมในวิธีการหนึ่งเพื่อกำหนดชุดอักขระจากนั้นอ่านใหม่พร้อมกับชุดอักขระในวิธีที่สอง
มาร์ควิสแห่ง Lorne

0

การโคลนอินพุตสตรีมอาจไม่ใช่ความคิดที่ดีเนื่องจากต้องมีความรู้เชิงลึกเกี่ยวกับรายละเอียดของอินพุตสตรีมที่ถูกโคลน วิธีแก้ปัญหาสำหรับสิ่งนี้คือการสร้างสตรีมอินพุตใหม่ที่อ่านจากแหล่งเดียวกันอีกครั้ง

ดังนั้นการใช้คุณสมบัติบางอย่างของ Java 8 จะมีลักษณะเช่นนี้:

public class Foo {

    private Supplier<InputStream> inputStreamSupplier;

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

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

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

วิธีการนี้จะมีผลในเชิงบวกว่ามันจะนำมาใช้ใหม่รหัสที่มีอยู่แล้วในสถานที่ - inputStreamSupplierการสร้างกระแสอินพุทที่ห่อหุ้มใน และไม่จำเป็นต้องบำรุงรักษาโค้ดพา ธ ที่สองสำหรับการโคลนสตรีม

ในทางกลับกันถ้าการอ่านจากสตรีมมีราคาแพง (เพราะมันทำผ่านการเชื่อมต่อแบนด์วิดท์ต่ำ) ดังนั้นวิธีนี้จะเพิ่มค่าใช้จ่ายเป็นสองเท่า สิ่งนี้สามารถหลีกเลี่ยงได้โดยการใช้ซัพพลายเออร์เฉพาะที่จะจัดเก็บเนื้อหาสตรีมในเครื่องก่อนและจัดหาInputStreamทรัพยากรท้องถิ่นในขณะนั้น


คำตอบนี้ไม่ชัดเจนสำหรับฉัน คุณจะเริ่มต้นซัพพลายเออร์จากที่มีอยู่ได้isอย่างไร
user1156544

@ user1156544 ขณะที่ฉันเขียนการโคลนสตรีมอินพุตอาจไม่ใช่ความคิดที่ดีเพราะต้องใช้ความรู้เชิงลึกเกี่ยวกับรายละเอียดของอินพุตสตรีมที่ถูกโคลน คุณไม่สามารถใช้ผู้จัดหาเพื่อสร้างอินพุตสตรีมจากอุปกรณ์ที่มีอยู่ ซัพพลายเออร์สามารถใช้java.io.Fileหรือjava.net.URLตัวอย่างเพื่อสร้างสตรีมอินพุตใหม่ในแต่ละครั้งที่มีการเรียกใช้
SpaceTrucker

ฉันเห็นแล้ว สิ่งนี้จะไม่ทำงานกับอินพุตสตรีมตามที่ OP ร้องขออย่างชัดเจน แต่ใช้ไฟล์หรือ URL หากเป็นแหล่งข้อมูลดั้งเดิม ขอบคุณ
user1156544
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.