스트림을 두 번 읽기


127

동일한 입력 스트림을 어떻게 두 번 읽습니까? 어떻게 든 복사 할 수 있습니까?

웹에서 이미지를 가져 와서 로컬에 저장 한 다음 저장된 이미지를 반환해야합니다. 다운로드 한 콘텐츠에 대해 새 스트림을 시작한 다음 다시 읽는 대신 동일한 스트림을 사용하는 것이 더 빠를 것입니다.


1
어쩌면 마크와 리셋을 사용
하세요

답변:


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는 내부적으로 내부에서도 copy 메서드를 호출합니다.
Ankit

4
@Ankit이 말했듯 이이 솔루션은 입력이 내부적으로 읽고 재사용 할 수 없기 때문에 유효하지 않습니다.
Xtreme Biker

30
이 주석이 시간이 없다는 것을 알고 있지만 여기 첫 번째 옵션에서 입력 스트림을 바이트 배열로 읽으면 모든 데이터를 메모리에로드한다는 의미가 아닙니까? 큰 파일과 같은 것을로드하는 경우 큰 문제가 될 수 있습니까?
jaxkodex

2
IOUtils.toByteArray (InputStream)을 사용하여 한 번의 호출로 바이트 배열을 가져올 수 있습니다.
유용한

30

InputStream의 출처에 따라 재설정하지 못할 수도 있습니다. 을 사용하여 mark()reset()지원 되는지 확인할 수 있습니다 markSupported().

그렇다면 reset()InputStream을 호출 하여 처음으로 돌아갈 수 있습니다. 그렇지 않은 경우 소스에서 InputStream을 다시 읽어야합니다.


1
InputStream은 'mark'를 지원하지 않습니다. IS에서 mark를 호출 할 수는 있지만 아무것도하지 않습니다. 마찬가지로 IS에서 reset을 호출하면 예외가 발생합니다.
ayahuasca

4
@ayahuasca의 InputStreamsubsclasses 같은 BufferedInputStream지원 '표시를'수행
드미트리 보그

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()새 버퍼 크기가 marklimit및 와만 비교되는 "버퍼 증가"섹션이 MAX_BUFFER_SIZE있습니다.
eugene82

8

PushbackInputStream으로 입력 스트림을 래핑 할 수 있습니다. PushbackInputStream 은 이미 읽은 읽지 않은 ( " write back ") 바이트를 허용하므로 다음과 같이 할 수 있습니다.

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을 바이트 배열로 변환 한 다음 필요한만큼 배열을 반복하는 것입니다. 이 게시물 에서 타사 라이브러리를 사용하여 Java에서 InputStream을 바이트 배열로 변환 하거나 사용하지 않는 여러 솔루션을 찾을 수 있습니다 . 읽기 내용이 너무 크면 일부 메모리 문제가 발생할 수 있습니다.

마지막으로 이미지를 읽는 것이 필요하면 다음을 사용하십시오.

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

사용하면 ImageIO#read(java.net.URL)캐시를 사용할 수도 있습니다.


1
사용시 경고 ImageIO#read(java.net.URL): 일부 웹 서버 및 CDN은에서 만든 호출을 베어 호출을 거부 할 수 있습니다 (즉, 서버가 호출이 웹 브라우저에서 온다고 믿게 만드는 사용자 에이전트없이) ImageIO#read. 이 경우 URLConnection.openConnection()사용자 에이전트를 해당 연결로 설정 +`ImageIO.read (InputStream) 사용하여 대부분의 경우 트릭을 수행합니다.
Clint Eastwood

InputStream인터페이스가 아닙니다
Brice 2017

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. PipedInputStream 각 PipedOutputStream 파이프를 연결, 이들은 PipedInputStream반환된다 InputStream.
  3. 소싱 InputStream을 방금 만든 OutputStream. 따라서 소싱에서 읽은 모든 내용 InputStream은 둘 다에 작성됩니다 OutputStream. 이미 TeeInputStream(commons.io) 에서 수행되었으므로 구현할 필요가 없습니다 .
  4. 분리 된 스레드 내에서 전체 소싱 inputStream을 읽고 암시 적으로 입력 데이터가 대상 inputStream으로 전송됩니다.

    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 읽기가 소스 inputStream 읽기를 트리거하지 않는 이유는 무엇입니까? 그리고 왜 우리는 그 호출을 비동기 적으로 수행합니까?
Дмитрий Кулешов

2

inputstream을 바이트로 변환 한 다음이를 inputstream으로 어셈블하는 savefile 함수에 전달합니다. 또한 원래 함수에서 바이트를 사용하여 다른 작업에 사용


5
나는 이것에 대해 나쁜 생각이라고 말하고, 결과 배열은 거대 할 수 있으며 장치의 메모리를 빼앗을 것입니다.
Kevin Parker

0

누구든지 Spring Boot 앱에서 실행 중이고 응답 본문을 읽고 싶은 경우 RestTemplate (이것이 스트림을 두 번 읽고 싶은 이유입니다),이 작업을 수행하는 깔끔한 방법이 있습니다.

우선, 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.