Đọc luồng hai lần


127

Làm thế nào để bạn đọc cùng một dòng đầu vào hai lần? Có thể sao chép nó bằng cách nào đó không?

Tôi cần lấy hình ảnh từ web, lưu cục bộ rồi trả lại hình ảnh đã lưu. Tôi chỉ nghĩ rằng sẽ nhanh hơn nếu sử dụng cùng một luồng thay vì bắt đầu một luồng mới với nội dung đã tải xuống và sau đó đọc lại.


1
Có thể sử dụng đánh dấu và đặt lại
Vyacheslav Shylkin 29/02/12

Câu trả lời:


114

Bạn có thể sử dụng org.apache.commons.io.IOUtils.copyđể sao chép nội dung của InputStream vào một mảng byte, sau đó đọc lặp lại từ mảng byte bằng ByteArrayInputStream. Ví dụ:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
org.apache.commons.io.IOUtils.copy(in, baos);
byte[] bytes = baos.toByteArray();

// either
while (needToReadAgain) {
    ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
    yourReadMethodHere(bais);
}

// or
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
while (needToReadAgain) {
    bais.reset();
    yourReadMethodHere(bais);
}

1
Tôi nghĩ đây là giải pháp hợp lệ duy nhất vì mark không được hỗ trợ cho tất cả các loại.
Warpzit

3
@Paul Grime: IOUtils.toByeArray cũng gọi nội bộ phương thức sao chép từ bên trong.
Ankit

4
Như @Ankit đã nói, giải pháp này không hợp lệ đối với tôi, vì đầu vào được đọc nội bộ và không thể sử dụng lại.
Xtreme Biker

30
Tôi biết nhận xét này đã hết thời gian, nhưng, ở đây trong tùy chọn đầu tiên, nếu bạn đọc dòng đầu vào dưới dạng một mảng byte, điều đó có nghĩa là bạn đang tải tất cả dữ liệu vào bộ nhớ? đó có thể là một vấn đề lớn nếu bạn đang tải một cái gì đó giống như các tệp lớn?
jaxkodex

2
Người ta có thể sử dụng IOUtils.toByteArray (InputStream) để lấy mảng byte trong một lần gọi.
hữu ích

30

Tùy thuộc vào nguồn InputStream đến từ đâu, bạn có thể không đặt lại được. Bạn có thể kiểm tra nếu mark()reset()được hỗ trợ bằng cách sử dụng markSupported().

Nếu đúng như vậy, bạn có thể gọi reset()InputStream để quay lại ban đầu. Nếu không, bạn cần đọc lại InputStream từ nguồn.


1
InputStream không hỗ trợ 'mark' - bạn có thể gọi mark trên IS nhưng nó không có tác dụng gì. Tương tự như vậy, việc gọi đặt lại trên IS sẽ đưa ra một ngoại lệ.
ayahuasca

4
@ayahuasca các lớp InputStreamcon như BufferedInputStreamkhông hỗ trợ 'mark'
Dmitry Bogdanovich

10

nếu InputStreamhỗ trợ của bạn bằng cách sử dụng mark, thì bạn có thể mark()inputStream và sau đó là reset()nó. nếu bạn InputStremkhông hỗ trợ dấu thì bạn có thể sử dụng lớp java.io.BufferedInputStream, vì vậy bạn có thể nhúng luồng của mình bên trong một BufferedInputStreamnhư thế này

    InputStream bufferdInputStream = new BufferedInputStream(yourInputStream);
    bufferdInputStream.mark(some_value);
    //read your bufferdInputStream 
    bufferdInputStream.reset();
    //read it again

1
Luồng đầu vào có bộ đệm chỉ có thể đánh dấu trở lại kích thước bộ đệm, vì vậy nếu nguồn không phù hợp, bạn không thể quay lại từ đầu.
L. Blanc

@ L.Blanc xin lỗi nhưng điều đó có vẻ không đúng. Hãy nhìn vào BufferedInputStream.fill(), có phần "bộ đệm phát triển", nơi kích thước bộ đệm mới chỉ được so sánh với marklimitMAX_BUFFER_SIZE.
eugene82

8

Bạn có thể bao bọc luồng đầu vào bằng PushbackInputStream. PushbackInputStream cho phép chưa đọc (" ghi lại ") các byte đã được đọc, vì vậy bạn có thể làm như sau:

public class StreamTest {
  public static void main(String[] args) throws IOException {
    byte[] bytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

    InputStream originalStream = new ByteArrayInputStream(bytes);

    byte[] readBytes = getBytes(originalStream, 3);
    printBytes(readBytes); // prints: 1 2 3

    readBytes = getBytes(originalStream, 3);
    printBytes(readBytes); // prints: 4 5 6

    // now let's wrap it with PushBackInputStream

    originalStream = new ByteArrayInputStream(bytes);

    InputStream wrappedStream = new PushbackInputStream(originalStream, 10); // 10 means that maximnum 10 characters can be "written back" to the stream

    readBytes = getBytes(wrappedStream, 3);
    printBytes(readBytes); // prints 1 2 3

    ((PushbackInputStream) wrappedStream).unread(readBytes, 0, readBytes.length);

    readBytes = getBytes(wrappedStream, 3);
    printBytes(readBytes); // prints 1 2 3


  }

  private static byte[] getBytes(InputStream is, int howManyBytes) throws IOException {
    System.out.print("Reading stream: ");

    byte[] buf = new byte[howManyBytes];

    int next = 0;
    for (int i = 0; i < howManyBytes; i++) {
      next = is.read();
      if (next > 0) {
        buf[i] = (byte) next;
      }
    }
    return buf;
  }

  private static void printBytes(byte[] buffer) throws IOException {
    System.out.print("Reading stream: ");

    for (int i = 0; i < buffer.length; i++) {
      System.out.print(buffer[i] + " ");
    }
    System.out.println();
  }


}

Xin lưu ý rằng PushbackInputStream lưu trữ bộ đệm bên trong của các byte, vì vậy nó thực sự tạo ra một bộ đệm trong bộ nhớ chứa các byte "được ghi lại".

Biết cách tiếp cận này, chúng ta có thể đi xa hơn và kết hợp nó với FilterInputStream. FilterInputStream lưu trữ luồng đầu vào ban đầu dưới dạng đại biểu. Điều này cho phép tạo định nghĩa lớp mới cho phép tự động " chưa đọc " dữ liệu gốc. Định nghĩa của lớp này như sau:

public class TryReadInputStream extends FilterInputStream {
  private final int maxPushbackBufferSize;

  /**
  * Creates a <code>FilterInputStream</code>
  * by assigning the  argument <code>in</code>
  * to the field <code>this.in</code> so as
  * to remember it for later use.
  *
  * @param in the underlying input stream, or <code>null</code> if
  *           this instance is to be created without an underlying stream.
  */
  public TryReadInputStream(InputStream in, int maxPushbackBufferSize) {
    super(new PushbackInputStream(in, maxPushbackBufferSize));
    this.maxPushbackBufferSize = maxPushbackBufferSize;
  }

  /**
   * Reads from input stream the <code>length</code> of bytes to given buffer. The read bytes are still avilable
   * in the stream
   *
   * @param buffer the destination buffer to which read the data
   * @param offset  the start offset in the destination <code>buffer</code>
   * @aram length how many bytes to read from the stream to buff. Length needs to be less than
   *        <code>maxPushbackBufferSize</code> or IOException will be thrown
   *
   * @return number of bytes read
   * @throws java.io.IOException in case length is
   */
  public int tryRead(byte[] buffer, int offset, int length) throws IOException {
    validateMaxLength(length);

    // NOTE: below reading byte by byte instead of "int bytesRead = is.read(firstBytes, 0, maxBytesOfResponseToLog);"
    // because read() guarantees to read a byte

    int bytesRead = 0;

    int nextByte = 0;

    for (int i = 0; (i < length) && (nextByte >= 0); i++) {
      nextByte = read();
      if (nextByte >= 0) {
        buffer[offset + bytesRead++] = (byte) nextByte;
      }
    }

    if (bytesRead > 0) {
      ((PushbackInputStream) in).unread(buffer, offset, bytesRead);
    }

    return bytesRead;

  }

  public byte[] tryRead(int maxBytesToRead) throws IOException {
    validateMaxLength(maxBytesToRead);

    ByteArrayOutputStream baos = new ByteArrayOutputStream(); // as ByteArrayOutputStream to dynamically allocate internal bytes array instead of allocating possibly large buffer (if maxBytesToRead is large)

    // NOTE: below reading byte by byte instead of "int bytesRead = is.read(firstBytes, 0, maxBytesOfResponseToLog);"
    // because read() guarantees to read a byte

    int nextByte = 0;

    for (int i = 0; (i < maxBytesToRead) && (nextByte >= 0); i++) {
      nextByte = read();
      if (nextByte >= 0) {
        baos.write((byte) nextByte);
      }
    }

    byte[] buffer = baos.toByteArray();

    if (buffer.length > 0) {
      ((PushbackInputStream) in).unread(buffer, 0, buffer.length);
    }

    return buffer;

  }

  private void validateMaxLength(int length) throws IOException {
    if (length > maxPushbackBufferSize) {
      throw new IOException(
        "Trying to read more bytes than maxBytesToRead. Max bytes: " + maxPushbackBufferSize + ". Trying to read: " +
        length);
    }
  }

}

Lớp này có hai phương thức. Một để đọc vào bộ đệm hiện có (defintion tương tự như việc gọi public int read(byte b[], int off, int len)lớp InputStream). Thứ hai trả về bộ đệm mới (điều này có thể hiệu quả hơn nếu kích thước bộ đệm cần đọc là không xác định).

Bây giờ chúng ta hãy xem lớp của chúng ta đang hoạt động:

public class StreamTest2 {
  public static void main(String[] args) throws IOException {
    byte[] bytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

    InputStream originalStream = new ByteArrayInputStream(bytes);

    byte[] readBytes = getBytes(originalStream, 3);
    printBytes(readBytes); // prints: 1 2 3

    readBytes = getBytes(originalStream, 3);
    printBytes(readBytes); // prints: 4 5 6

    // now let's use our TryReadInputStream

    originalStream = new ByteArrayInputStream(bytes);

    InputStream wrappedStream = new TryReadInputStream(originalStream, 10);

    readBytes = ((TryReadInputStream) wrappedStream).tryRead(3); // NOTE: no manual call to "unread"(!) because TryReadInputStream handles this internally
    printBytes(readBytes); // prints 1 2 3

    readBytes = ((TryReadInputStream) wrappedStream).tryRead(3); 
    printBytes(readBytes); // prints 1 2 3

    readBytes = ((TryReadInputStream) wrappedStream).tryRead(3);
    printBytes(readBytes); // prints 1 2 3

    // we can also call normal read which will actually read the bytes without "writing them back"
    readBytes = getBytes(wrappedStream, 3);
    printBytes(readBytes); // prints 1 2 3

    readBytes = getBytes(wrappedStream, 3);
    printBytes(readBytes); // prints 4 5 6

    readBytes = ((TryReadInputStream) wrappedStream).tryRead(3); // now we can try read next bytes
    printBytes(readBytes); // prints 7 8 9

    readBytes = ((TryReadInputStream) wrappedStream).tryRead(3); 
    printBytes(readBytes); // prints 7 8 9


  }



}

5

Nếu bạn đang sử dụng triển khai của InputStream, bạn có thể kiểm tra kết quả của InputStream#markSupported()việc đó cho bạn biết liệu bạn có thể sử dụng phương thức mark()/ hay không reset().

Nếu bạn có thể đánh dấu luồng khi bạn đọc, thì hãy gọi reset()để quay lại để bắt đầu.

Nếu không thể, bạn sẽ phải mở lại luồng.

Một giải pháp khác là chuyển đổi InputStream thành mảng byte, sau đó lặp lại mảng nhiều lần nếu bạn cần. Bạn có thể tìm thấy một số giải pháp trong bài đăng này Chuyển đổi InputStream thành mảng byte trong Java bằng cách sử dụng lib của bên thứ ba hoặc không. Thận trọng, nếu nội dung đã đọc quá lớn, bạn có thể gặp một số rắc rối về bộ nhớ.

Cuối cùng, nếu nhu cầu của bạn là đọc hình ảnh, thì hãy sử dụng:

BufferedImage image = ImageIO.read(new URL("http://www.example.com/images/toto.jpg"));

Việc sử dụng ImageIO#read(java.net.URL)cũng cho phép bạn sử dụng bộ nhớ đệm.


1
một lời cảnh báo khi sử dụng ImageIO#read(java.net.URL): một số máy chủ web và CDN có thể từ chối các cuộc gọi trần (nghĩa là không có Tác nhân Người dùng khiến máy chủ tin rằng cuộc gọi đến từ trình duyệt web) do ImageIO#read. Trong trường hợp đó, sử dụng URLConnection.openConnection()thiết lập tác nhân người dùng cho kết nối đó + sử dụng `ImageIO.read (InputStream), hầu hết các trường hợp, sẽ thực hiện thủ thuật.
Clint Eastwood

InputStreamkhông phải là một giao diện
Brice

3

Làm thế nào về:

if (stream.markSupported() == false) {

        // lets replace the stream object
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        IOUtils.copy(stream, baos);
        stream.close();
        stream = new ByteArrayInputStream(baos.toByteArray());
        // now the stream should support 'mark' and 'reset'

    }

5
Đó là một ý tưởng khủng khiếp. Bạn đặt toàn bộ nội dung luồng vào bộ nhớ như vậy.
Niels Doucet

3

Để chia đôi InputStream, trong khi tránh tải tất cả dữ liệu trong bộ nhớ , rồi xử lý chúng một cách độc lập:

  1. Tạo một vài OutputStream, chính xác:PipedOutputStream
  2. Kết nối mỗi PipedOutputStream với một PipedInputStream, chúng PipedInputStreamđược trả về InputStream.
  3. Kết nối InputStream tìm nguồn cung ứng với vừa tạo OutputStream. Vì vậy, mọi thứ đọc nó từ tìm nguồn cung ứng InputStream, sẽ được viết bằng cả hai OutputStream. Không cần phải triển khai điều đó, bởi vì nó đã được thực hiện trong TeeInputStream(commons.io).
  4. Trong một luồng được phân tách, hãy đọc toàn bộ luồng inputStream tìm nguồn cung ứng và mặc nhiên dữ liệu đầu vào được chuyển đến inputStreams đích.

    public static final List<InputStream> splitInputStream(InputStream input) 
        throws IOException 
    { 
        Objects.requireNonNull(input);      
    
        PipedOutputStream pipedOut01 = new PipedOutputStream();
        PipedOutputStream pipedOut02 = new PipedOutputStream();
    
        List<InputStream> inputStreamList = new ArrayList<>();
        inputStreamList.add(new PipedInputStream(pipedOut01));
        inputStreamList.add(new PipedInputStream(pipedOut02));
    
        TeeOutputStream tout = new TeeOutputStream(pipedOut01, pipedOut02);
    
        TeeInputStream tin = new TeeInputStream(input, tout, true);
    
        Executors.newSingleThreadExecutor().submit(tin::readAllBytes);  
    
        return Collections.unmodifiableList(inputStreamList);
    }

Hãy lưu ý đóng inputStreams sau khi được sử dụng và đóng luồng đang chạy: TeeInputStream.readAllBytes()

Trong trường hợp, bạn cần chia nó thành nhiềuInputStream thay vì chỉ hai. Thay thế trong đoạn mã trước đó bằng lớp TeeOutputStreamđể triển khai của riêng bạn, lớp này sẽ đóng gói a List<OutputStream>và ghi đè OutputStreamgiao diện:

public final class TeeListOutputStream extends OutputStream {
    private final List<? extends OutputStream> branchList;

    public TeeListOutputStream(final List<? extends OutputStream> branchList) {
        Objects.requireNonNull(branchList);
        this.branchList = branchList;
    }

    @Override
    public synchronized void write(final int b) throws IOException {
        for (OutputStream branch : branchList) {
            branch.write(b);
        }
    }

    @Override
    public void flush() throws IOException {
        for (OutputStream branch : branchList) {
            branch.flush();
        }
    }

    @Override
    public void close() throws IOException {
        for (OutputStream branch : branchList) {
            branch.close();
        }
    }
}

Xin vui lòng, bạn có thể giải thích một chút hơn về bước 4? Tại sao chúng ta phải kích hoạt đọc thủ công? Tại sao việc đọc bất kỳ pipedInputStream KHÔNG kích hoạt việc đọc inputStream nguồn? Và tại sao chúng tôi gọi đó là không đồng bộ?
Дмитрий Кулешов

2

Chuyển dòng đầu vào thành các byte và sau đó chuyển nó vào hàm savefile nơi bạn lắp ráp dòng đầu vào giống nhau vào dòng đầu vào. Ngoài ra trong hàm gốc sử dụng byte để sử dụng cho các tác vụ khác


5
Tôi nói ý kiến ​​tồi về vấn đề này, mảng kết quả có thể rất lớn và sẽ cướp bộ nhớ của thiết bị.
Kevin Parker,

0

Trong trường hợp bất kỳ ai đang chạy trong ứng dụng Spring Boot và bạn muốn đọc phần nội dung phản hồi của a RestTemplate(đó là lý do tại sao tôi muốn đọc một luồng hai lần), có một cách rõ ràng để thực hiện việc này.

Trước hết, bạn cần sử dụng Spring StreamUtilsđể sao chép luồng vào Chuỗi:

String text = StreamUtils.copyToString(response.getBody(), Charset.defaultCharset()))

Nhưng đó không phải là tất cả. Bạn cũng cần sử dụng một nhà máy yêu cầu có thể đệm luồng cho bạn, như sau:

ClientHttpRequestFactory factory = new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory());
RestTemplate restTemplate = new RestTemplate(factory);

Hoặc, nếu bạn đang sử dụng factory bean, thì (đây là Kotlin nhưng tuy nhiên):

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
fun createRestTemplate(): RestTemplate = RestTemplateBuilder()
  .requestFactory { BufferingClientHttpRequestFactory(SimpleClientHttpRequestFactory()) }
  .additionalInterceptors(loggingInterceptor)
  .build()

Nguồn: https://objectpartners.com/2018/03/01/log-your-resttemplate-request-and-response-without-destroying-the-body/


0

Nếu bạn đang sử dụng RestTemplate để thực hiện các cuộc gọi http Chỉ cần thêm một bộ chặn. Nội dung phản hồi được lưu trong bộ nhớ cache bằng cách triển khai ClientHttpResponse. Giờ đây, inputstream có thể được lấy ra từ phản hồi nhiều lần nếu chúng ta cần

ClientHttpRequestInterceptor interceptor =  new ClientHttpRequestInterceptor() {

            @Override
            public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                    ClientHttpRequestExecution execution) throws IOException {
                ClientHttpResponse  response = execution.execute(request, body);

                  // additional work before returning response
                  return response 
            }
        };

    // Add the interceptor to RestTemplate Instance 

         restTemplate.getInterceptors().add(interceptor); 
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.