Прочитать поток дважды


128

Как прочитать один и тот же входной поток дважды? Можно ли как-нибудь скопировать?

Мне нужно получить изображение из Интернета, сохранить его локально, а затем вернуть сохраненное изображение. Я просто подумал, что было бы быстрее использовать тот же поток вместо того, чтобы запускать новый поток для загруженного контента, а затем читать его снова.


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 01

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 не поддерживает метку - вы можете вызвать метку в IS, но она ничего не делает. Точно так же вызов сброса на IS вызовет исключение.
аяхуаска

4
Подклассы InputStream@ayahuasca вроде BufferedInputStreamподдерживают 'mark'
Дмитрий Богданович

10

если вы InputStreamподдерживаете использование mark, вы можете использовать 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 02

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, используя сторонние библиотеки или нет. Осторожно, если прочитанное содержимое слишком велико, могут возникнуть проблемы с памятью.

Наконец, если вам нужно прочитать изображение, используйте:

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) в большинстве случаев поможет.
Клинт Иствуд

InputStreamэто не интерфейс
Брайс

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для вашей собственной реализации, которая инкапсулирует 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

Преобразуйте входной поток в байты, а затем передайте его функции savefile, где вы собираете то же самое во входной поток. Также в исходной функции используйте байты для других задач.


5
Я говорю, что это плохая идея, результирующий массив может быть огромным и лишит устройство памяти.
Кевин Паркер,

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); 
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.