อ่านสตรีมสองครั้ง


127

คุณอ่านอินพุตสตรีมเดียวกันสองครั้งได้อย่างไร สามารถคัดลอกได้หรือไม่?

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


1
อาจใช้เครื่องหมายและรีเซ็ต
Vyacheslav Shylkin

คำตอบ:


114

คุณสามารถใช้org.apache.commons.io.IOUtils.copyเพื่อคัดลอกเนื้อหาของ InputStream ไปยังอาร์เรย์ไบต์จากนั้นอ่านซ้ำ ๆ จากอาร์เรย์ไบต์โดยใช้ ByteArrayInputStream เช่น:

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
ฉันคิดว่านี่เป็นทางออกเดียวที่ถูกต้องเนื่องจากไม่รองรับเครื่องหมายสำหรับทุกประเภท
Warpzit

3
@ Paul Grime: IOUtils.toByeArray ภายในเรียกวิธีการคัดลอกจากภายในเช่นกัน
Ankit

4
ตามที่ @Ankit กล่าวว่าโซลูชันนี้ไม่ถูกต้องสำหรับฉันเนื่องจากอินพุตถูกอ่านภายในและไม่สามารถนำกลับมาใช้ใหม่ได้
Xtreme Biker

30
ฉันรู้ว่าความคิดเห็นนี้หมดเวลาแล้ว แต่ในตัวเลือกแรกที่นี่ถ้าคุณอ่านอินพุตสตรีมเป็นอาร์เรย์ไบต์หมายความว่าคุณกำลังโหลดข้อมูลทั้งหมดลงในหน่วยความจำใช่หรือไม่ ซึ่งอาจเป็นปัญหาใหญ่หากคุณกำลังโหลดไฟล์ขนาดใหญ่
jaxkodex

2
หนึ่งสามารถใช้ IOUtils.toByteArray (InputStream) เพื่อรับไบต์อาร์เรย์ในการโทรครั้งเดียว
มีประโยชน์

30

ขึ้นอยู่กับว่า InputStream มาจากที่ใดคุณอาจไม่สามารถรีเซ็ตได้ คุณสามารถตรวจสอบmark()และreset()ได้รับการสนับสนุนการใช้markSupported()ได้รับการสนับสนุนการใช้

หากเป็นเช่นนั้นคุณสามารถเรียกreset()InputStream เพื่อกลับไปที่จุดเริ่มต้นได้ ถ้าไม่คุณต้องอ่าน InputStream จากแหล่งที่มาอีกครั้ง


1
InputStream ไม่รองรับ 'mark' - คุณสามารถเรียกใช้เครื่องหมายบน IS ได้ แต่ไม่ทำอะไรเลย ในทำนองเดียวกันการเรียกรีเซ็ตบน IS จะทำให้เกิดข้อยกเว้น
ayahuasca

4
คลาสย่อยของInputStream@ayahuasca เช่นBufferedInputStreamรองรับ 'mark'
Dmitry Bogdanovich

10

ถ้าคุณInputStreamสนับสนุนการใช้เครื่องหมายแล้วคุณสามารถmark()inputStream แล้วของคุณreset()มัน หากคุณInputStremไม่สนับสนุนเครื่องหมายคุณสามารถใช้ชั้นเรียนjava.io.BufferedInputStreamได้ดังนั้นคุณสามารถฝังสตรีมของคุณไว้ในBufferedInputStreamลักษณะนี้ได้

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

1
สตรีมอินพุตที่บัฟเฟอร์สามารถทำเครื่องหมายกลับเป็นขนาดบัฟเฟอร์เท่านั้นดังนั้นหากแหล่งที่มาไม่พอดีคุณจะกลับไปที่จุดเริ่มต้นไม่ได้ทั้งหมด
L. Blanc

@ L.Blanc ขออภัย แต่ดูเหมือนจะไม่ถูกต้อง ลองดูที่BufferedInputStream.fill()มีส่วน "เติบโต buffer" ที่ขนาดของบัฟเฟอร์ใหม่จะเทียบเฉพาะกับและmarklimit MAX_BUFFER_SIZE
eugene82

8

คุณสามารถตัดกระแสข้อมูลเข้าด้วย PushbackInputStream PushbackInputStream อนุญาตให้ยังไม่อ่าน (" เขียนกลับ ") ไบต์ที่อ่านไปแล้วดังนั้นคุณสามารถทำได้ดังนี้:

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


}

โปรดทราบว่า PushbackInputStream เก็บบัฟเฟอร์ภายในของไบต์ดังนั้นจึงสร้างบัฟเฟอร์ในหน่วยความจำซึ่งเก็บไบต์ไว้ "เขียนกลับ"

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

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

}

คลาสนี้มีสองวิธี หนึ่งสำหรับการอ่านลงในบัฟเฟอร์ที่มีอยู่ (นิยามนั้นคล้ายคลึงกับการโทรpublic int read(byte b[], int off, int len)คลาส InputStream) ประการที่สองซึ่งส่งคืนบัฟเฟอร์ใหม่ (ซึ่งอาจมีประสิทธิภาพมากขึ้นหากไม่ทราบขนาดของบัฟเฟอร์ที่จะอ่าน)

ตอนนี้เรามาดูการใช้งานคลาสของเรา:

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

หากคุณใช้การนำไปใช้งานInputStreamคุณสามารถตรวจสอบผลลัพธ์InputStream#markSupported()ที่บอกคุณได้ว่าคุณสามารถใช้วิธีการmark()/reset() /

หากคุณสามารถทำเครื่องหมายสตรีมเมื่อคุณอ่านแล้วreset()ให้โทรกลับเพื่อเริ่มต้น

หากทำไม่ได้คุณจะต้องเปิดสตรีมอีกครั้ง

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

สุดท้ายหากคุณต้องการอ่านภาพให้ใช้:

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

การใช้ImageIO#read(java.net.URL)ยังช่วยให้คุณสามารถใช้แคช


1
คำเตือนเมื่อใช้ImageIO#read(java.net.URL): เว็บเซิร์ฟเวอร์และ CDNs อาจปฏิเสธสายเปลือย (เช่นโดยไม่ต้องมีตัวแทนของผู้ใช้ที่ทำให้เซิร์ฟเวอร์เชื่อว่าโทรมาจากเว็บเบราเซอร์) ImageIO#readที่ทำโดย ในกรณีนั้นการใช้URLConnection.openConnection()การตั้งค่าตัวแทนผู้ใช้เป็นการเชื่อมต่อนั้น + โดยใช้ ImageIO.read (InputStream) ส่วนใหญ่จะทำเคล็ดลับ
Clint Eastwood

InputStreamไม่ใช่อินเทอร์เฟซ
Brice

3

เกี่ยวกับ:

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
นั่นเป็นความคิดที่แย่มาก คุณใส่เนื้อหาสตรีมทั้งหมดไว้ในหน่วยความจำเช่นนั้น
Niels Doucet

3

สำหรับการแยกเป็นInputStreamสองส่วนในขณะที่หลีกเลี่ยงการโหลดข้อมูลทั้งหมดในหน่วยความจำแล้วประมวลผลอย่างอิสระ:

  1. สร้างสองสามOutputStreamอย่างแม่นยำ:PipedOutputStream
  2. เชื่อมต่อแต่ละ PipedOutputStream กับ PipedInputStream สิ่งเหล่านี้PipedInputStreamคือสิ่งที่ส่งคืนInputStreamจะถูกส่งกลับ
  3. เชื่อมต่อกับ InputStream OutputStreamจัดหาเพิ่งสร้าง ดังนั้นทุกอย่างที่อ่านจากการจัดหาInputStreamจะเขียนเป็นทั้งสองOutputStreamอย่าง ไม่จำเป็นต้องใช้สิ่งนั้นเพราะมันเสร็จแล้วในTeeInputStream(commons.io)
  4. ภายในเธรดที่แยกจากกันอ่าน inputStream การจัดหาทั้งหมดและโดยปริยายข้อมูลอินพุตจะถูกโอนไปยัง inputStreams เป้าหมาย

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

โปรดทราบว่าให้ปิด inputStreams หลังจากถูกใช้งานและปิดเธรดที่รัน: TeeInputStream.readAllBytes()

ในกรณีนี้คุณต้องแบ่งออกเป็นหลาย ๆInputStreamแทนที่จะเป็นเพียงสองอย่าง แทนที่ในส่วนก่อนหน้าของโค้ดคลาสTeeOutputStreamสำหรับการใช้งานของคุณเองซึ่งจะห่อหุ้ม a List<OutputStream>และแทนที่OutputStreamอินเทอร์เฟซ:

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

โปรดอธิบายเพิ่มเติมเกี่ยวกับขั้นตอนที่ 4 ได้ไหม ทำไมเราต้องเรียกการอ่านด้วยตนเอง? เหตุใดการอ่าน pipedInputStream ใด ๆ จึงไม่ทริกเกอร์การอ่านอินพุตสตรีมต้นทาง และทำไมเราถึงเรียกแบบไม่ตรงกัน?
ДмитрийКулешов

2

แปลงอินพุตสตรีมเป็นไบต์จากนั้นส่งต่อไปยังฟังก์ชัน savefile ที่คุณประกอบเข้าด้วยกันในอินพุตสตรีม นอกจากนี้ในฟังก์ชันดั้งเดิมยังใช้ไบต์เพื่อใช้สำหรับงานอื่น ๆ


5
ฉันพูดความคิดที่ไม่ดีในเรื่องนี้อาร์เรย์ผลลัพธ์อาจมีขนาดใหญ่และจะปล้นอุปกรณ์หน่วยความจำ
Kevin Parker

0

ในกรณีที่ใครก็ตามกำลังใช้งานแอป Spring Boot และคุณต้องการอ่านเนื้อหาการตอบสนองของไฟล์ RestTemplate (ซึ่งเป็นเหตุผลว่าทำไมฉันถึงต้องการอ่านสตรีมสองครั้ง) มีวิธีที่สะอาด (er) ในการทำเช่นนี้

ก่อนอื่นคุณต้องใช้ Spring StreamUtilsเพื่อคัดลอกสตรีมไปยัง String:

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

แต่นั่นไม่ใช่ทั้งหมด คุณต้องใช้โรงงานที่ร้องขอที่สามารถบัฟเฟอร์สตรีมให้คุณได้เช่น:

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

หรือถ้าคุณใช้ถั่วโรงงาน (นี่คือ Kotlin แต่อย่างไรก็ตาม):

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

ที่มา: https://objectpartners.com/2018/03/01/log-your-resttemplate-request-and-response-without-destroying-the-body/


0

หากคุณใช้ RestTemplate เพื่อโทร http เพียงเพิ่มตัวดักฟัง เนื้อหาการตอบกลับถูกแคชโดยการนำ ClientHttpResponse ไปใช้ ตอนนี้ inputstream สามารถดึงข้อมูลจาก respose กี่ครั้งก็ได้ตามที่เราต้องการ

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); 
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.