Làm cách nào để sao chép InputStream?


162

Tôi có một InputStream mà tôi chuyển đến một phương thức để thực hiện một số xử lý. Tôi sẽ sử dụng cùng InputStream trong phương thức khác, nhưng sau lần xử lý đầu tiên, InputStream xuất hiện sẽ bị đóng bên trong phương thức.

Làm cách nào tôi có thể sao chép InputStream để gửi đến phương thức đóng anh ta? Có giải pháp nào khác?

EDIT: các phương thức đóng InputStream là một phương thức bên ngoài từ lib. Tôi không có quyền kiểm soát về việc đóng cửa hay không.

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

1
Bạn có muốn "thiết lập lại" luồng sau khi phương thức đã trở lại không? Tức là, đọc các luồng từ đầu?
aioobe

Có, các phương thức đóng InputStream trả về bộ ký tự được mã hóa. Phương thức thứ hai là chuyển đổi InputStream thành Chuỗi bằng cách sử dụng bộ ký tự được tìm thấy trong phương thức đầu tiên.
Renato Dinhani

Trong trường hợp đó bạn có thể làm những gì tôi mô tả trong câu trả lời của tôi.
Kaj

Tôi không biết cách tốt nhất để giải quyết nó, nhưng tôi giải quyết vấn đề của mình bằng cách khác. Phương thức toString của Jericho HTML Parser trả về Chuỗi được định dạng theo đúng định dạng. Đó là tất cả những gì tôi cần vào lúc này.
Renato Dinhani

Câu trả lời:


188

Nếu tất cả những gì bạn muốn làm là đọc cùng một thông tin nhiều lần và dữ liệu đầu vào đủ nhỏ để phù hợp với bộ nhớ, bạn có thể sao chép dữ liệu từ của bạn InputStreamsang ByteArrayOutputStream .

Sau đó, bạn có thể có được mảng byte liên quan và mở bao nhiêu ByteArrayInputStream tùy thích.

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

Nhưng nếu bạn thực sự cần giữ luồng gốc mở để nhận dữ liệu mới, thì bạn sẽ cần theo dõi bên ngoài này close() phương thức này và ngăn không cho nó được gọi bằng cách nào đó.

CẬP NHẬT (2019):

Vì Java 9, các bit giữa có thể được thay thế bằng InputStream.transferTo:

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

Tôi tìm ra giải pháp khác cho vấn đề của mình, không liên quan đến việc sao chép InputStream, nhưng tôi nghĩ nếu tôi cần sao chép InputStream, đây là giải pháp tốt nhất.
Renato Dinhani

7
Cách tiếp cận này tiêu thụ bộ nhớ tỷ lệ thuận với toàn bộ nội dung của luồng đầu vào. Tốt hơn để sử dụng TeeInputStreamnhư được mô tả trong câu trả lời ở đây .
aioobe

2
IOUtils (từ apache commons) có một phương thức sao chép để đọc / ghi bộ đệm ở giữa mã của bạn.
suy nghĩ lại

31

Bạn muốn sử dụng Apache CloseShieldInputStream:

Đây là một trình bao bọc sẽ ngăn luồng bị đóng. Bạn sẽ làm một cái gì đó như thế này.

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

Có vẻ tốt, nhưng không hoạt động ở đây. Tôi sẽ chỉnh sửa bài viết của tôi với mã.
Renato Dinhani

CloseShieldkhông hoạt động vì HttpURLConnectionluồng đầu vào ban đầu của bạn đang đóng ở đâu đó. Không nên phương pháp của bạn gọi IOUtils với luồng được bảo vệ IOUtils.toString(csContent,charset)?
Anthony Accioly

Có lẽ có thể là cái này. Tôi có thể ngăn chặn kết nối httpURLC không?
Renato Dinhani

1
@Renato. Có thể vấn đề không phải là close()cuộc gọi nào cả, nhưng thực tế là Stream đang được đọc đến cuối. Vì mark()reset()có thể không phải là phương pháp tốt nhất cho các kết nối http, có lẽ bạn nên xem cách tiếp cận mảng byte được mô tả trong câu trả lời của tôi.
Anthony Accioly

1
Một điều nữa, bạn luôn có thể mở một kết nối mới tới cùng một URL. Xem tại đây: stackoverflow.com/questions/5807340/ Lời
Anthony Accioly

11

Bạn không thể sao chép nó và cách bạn sẽ giải quyết vấn đề của mình tùy thuộc vào nguồn dữ liệu là gì.

Một giải pháp là đọc tất cả dữ liệu từ InputStream thành một mảng byte và sau đó tạo ByteArrayInputStream xung quanh mảng byte đó và truyền luồng đầu vào đó vào phương thức của bạn.

Chỉnh sửa 1: Nghĩa là, nếu phương thức khác cũng cần đọc cùng một dữ liệu. Tức là bạn muốn "thiết lập lại" luồng.


Tôi không biết phần nào bạn cần giúp đỡ. Tôi đoán bạn biết làm thế nào để đọc từ một luồng? Đọc tất cả dữ liệu từ InputStream và ghi dữ liệu vào ByteArrayOutputStream. Gọi tớiByteArray () trên ByteArrayOutputStream sau khi bạn đọc xong tất cả dữ liệu. Sau đó chuyển mảng byte đó vào hàm tạo của ByteArrayInputStream.
Kaj

8

Nếu dữ liệu đọc từ luồng lớn, tôi khuyên bạn nên sử dụng TeeInputStream từ Apache Commons IO. Bằng cách đó, về cơ bản bạn có thể sao chép đầu vào và vượt qua một ống t'd làm bản sao của mình.


5

Điều này có thể không hoạt động trong mọi tình huống, nhưng đây là những gì tôi đã làm: Tôi đã mở rộng lớp FilterInputStream và thực hiện xử lý các byte cần thiết khi lib bên ngoài đọc dữ liệu.

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
    }

}

Sau đó, bạn chỉ cần truyền một thể hiện của StreamBytesWithExtraProcessingInputStreamnơi bạn sẽ vượt qua trong luồng đầu vào. Với luồng đầu vào ban đầu là tham số constructor.

Cần lưu ý rằng điều này hoạt động theo byte cho byte, vì vậy đừng sử dụng điều này nếu hiệu suất cao là một yêu cầu.


3

CẬP NHẬT. Kiểm tra bình luận trước. Đó không phải là chính xác những gì được yêu cầu.

Nếu bạn đang sử dụng, apache.commonsbạn có thể sao chép luồng bằng cách sử dụng IOUtils.

Bạn có thể sử dụng mã sau đây:

InputStream = IOUtils.toBufferedInputStream(toCopy);

Dưới đây là ví dụ đầy đủ phù hợp với tình huống của bạn:

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

Mã này yêu cầu một số phụ thuộc:

MÙA

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

LỚP

'commons-io:commons-io:2.4'

Dưới đây là tài liệu tham khảo DOC cho phương pháp này:

Tìm nạp toàn bộ nội dung của InputStream và thể hiện cùng một dữ liệu với kết quả InputStream. Phương pháp này hữu ích ở đâu,

Nguồn InputStream chậm. Nó có tài nguyên mạng liên quan, vì vậy chúng tôi không thể giữ nó mở trong thời gian dài. Nó có thời gian chờ mạng liên quan.

Bạn có thể tìm hiểu thêm về IOUtilsđây: http://commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/IOUtils.html#toBufferedInputStream(java.io.InputStream)


7
Điều này không sao chép luồng đầu vào mà chỉ đệm nó. Điều đó không giống nhau; OP muốn đọc lại (một bản sao) của cùng một luồng.
Raphael

1

Dưới đây là giải pháp với Kotlin.

Bạn có thể sao chép InputStream của mình vào ByteArray

val inputStream = ...

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

val byteInputStream = ByteArrayInputStream(byteOutputStream.toByteArray())

Nếu bạn cần đọc byteInputStreamnhiều lần, hãy gọibyteInputStream.reset() trước khi đọc lại.

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


0

Các lớp dưới đây nên làm thủ thuật. Chỉ cần tạo một cá thể, gọi phương thức "nhân" và cung cấp luồng đầu vào nguồn và số lượng trùng lặp bạn cần.

Quan trọng: bạn phải tiêu thụ tất cả các luồng nhân bản đồng thời trong các luồng riêng biệt.

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

Không trả lời câu hỏi. Anh ta muốn sử dụng luồng trong một phương thức để xác định bộ ký tự và sau đó đọc lại nó cùng với bộ ký tự của nó trong một phương thức thứ hai.
Hầu tước Lorne

0

Nhân bản một luồng đầu vào có thể không phải là một ý tưởng hay, bởi vì điều này đòi hỏi kiến ​​thức sâu sắc về các chi tiết của luồng đầu vào được nhân bản. Một cách giải quyết cho vấn đề này là tạo ra một luồng đầu vào mới đọc lại từ cùng một nguồn.

Vì vậy, sử dụng một số tính năng của Java 8 sẽ như thế này:

public class Foo {

    private Supplier<InputStream> inputStreamSupplier;

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

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

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

Phương pháp này có tác động tích cực là nó sẽ sử dụng lại mã đã có sẵn - việc tạo ra luồng đầu vào được gói gọn trong inputStreamSupplier. Và không cần phải duy trì một đường dẫn mã thứ hai để nhân bản luồng.

Mặt khác, nếu việc đọc từ luồng là tốn kém (vì nó được thực hiện qua kết nối băng thông thấp), thì phương pháp này sẽ tăng gấp đôi chi phí. Điều này có thể được tránh bằng cách sử dụng một nhà cung cấp cụ thể sẽ lưu trữ nội dung luồng đầu tiên tại địa phương và cung cấp InputStreamtài nguyên cục bộ hiện tại.


Câu trả lời này không rõ ràng với tôi. Làm thế nào để bạn khởi tạo nhà cung cấp từ một hiện có is?
dùng1156544

@ user1156544 Như tôi đã viết Nhân bản một luồng đầu vào có thể không phải là một ý tưởng hay, bởi vì điều này đòi hỏi kiến ​​thức sâu sắc về các chi tiết của luồng đầu vào được sao chép. bạn không thể sử dụng nhà cung cấp để tạo một luồng đầu vào từ một cái hiện có. Nhà cung cấp có thể sử dụng một java.io.Filehoặc java.net.URLví dụ để tạo luồng đầu vào mới mỗi lần được gọi.
SpaceTrucker

Tôi thấy bây giờ Điều này sẽ không hoạt động với dòng đầu vào như OP yêu cầu rõ ràng, nhưng với Tệp hoặc URL nếu chúng là nguồn dữ liệu ban đầu. Cảm ơn
user1156544
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.