Прочитайте потік двічі


127

Як ви читаєте один і той же вхідний потік двічі? Чи можливо це якось скопіювати?

Мені потрібно отримати зображення з Інтернету, зберегти його локально, а потім повернути збережене зображення. Мені просто здалося, що швидше використовувати той самий потік, а не запускати новий потік до завантаженого вмісту, а потім читати його ще раз.


1
Можливо, використовуйте позначку та скиньте
В’ячеслав Шилкін

Відповіді:


113

Ви можете 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 внутрішньо також викликає метод копіювання зсередини.
Анкіт

4
Як говорить @Ankit, це рішення для мене недійсне, оскільки вхід читається внутрішньо і не може бути використаний повторно.
Xtreme Biker

30
Я знаю, що цей коментар закінчився, але тут, у першому варіанті, якщо ви читаєте вхідний потік як байтовий масив, чи не означає це, що ви завантажуєте всі дані в пам'ять? що може бути великою проблемою, якщо ви завантажуєте щось на зразок великих файлів?
jaxkodex

2
Можна отримати IOUtils.toByteArray (InputStream), щоб отримати байтовий масив за один виклик.
корисно

30

Залежно від того, звідки надходить InputStream, можливо, ви не зможете його скинути. Ви можете перевірити , якщо mark()і reset()підтримуються з допомогою markSupported().

Якщо це так, ви можете зателефонувати reset()на InputStream, щоб повернутися до початку. Якщо ні, вам потрібно ще раз прочитати InputStream з джерела.


1
InputStream не підтримує 'mark' - ви можете називати позначку на ІС, але вона нічого не робить. Так само виклик скидання на ІС призведе до виключення.
ayahuasca

4
Підкласи InputStream@ayahuasca на зразок BufferedInputStreamпідтримує "позначку"
Дмитро Богданович

10

якщо ваша InputStreamпідтримка використовує позначку, то ви можете mark()ввести свою стрічку і потім 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 вибачте, але це не здається правильним. Погляньте BufferedInputStream.fill(), є розділ "буфер росту", де новий розмір буфера порівнюється лише з 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, використовуючи сторонні lib чи ні. Обережно, якщо вміст, який читають, занадто великий, у вас можуть виникнути проблеми з пам'яттю.

Нарешті, якщо вам потрібно прочитати зображення, то використовуйте:

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
Це жахлива ідея. Ви вміщуєте весь вміст потоку в таку пам'ять.
Нільс Дусет

3

Для розділення на InputStreamдва, уникаючи завантаження всіх даних у пам'ять , а потім обробляти їх самостійно:

  1. Створіть пару OutputStreamточно:PipedOutputStream
  2. З'єднайте кожен PipedOutputStream з PipedInputStream, вони PipedInputStreamповертаються InputStream.
  3. З'єднайте джерело InputStream із щойно створеним OutputStream. Отже, все, що читається з джерел InputStream, було б написано в обох OutputStream. Це не потрібно реалізовувати, тому що це робиться вже в TeeInputStream(commons.io).
  4. Всередині відокремленого потоку зчитується весь джерело джерелаStream, і неявно вхідні дані передаються цільовим 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 НЕ викликає зчитування джерела inputStream? І чому ми робимо цей виклик асинхронно?
Дмитрій Кулешов

2

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


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

0

Якщо хтось працює у програмі Spring Boot, і ви хочете прочитати відповідне тіло RestTemplate(саме тому я хочу прочитати потік двічі), існує чистий (ер) спосіб зробити це.

Перш за все, вам потрібно скористатися Spring's, StreamUtilsщоб скопіювати потік у String:

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

Але це ще не все. Вам також потрібно використовувати фабрику запитів, яка може забудувати потік для вас, наприклад:

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

Або, якщо ви використовуєте заводський квасоля, то (це Котлін, але все-таки):

@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. Тепер вхідний потік можна отримати з відповіді стільки разів, скільки нам потрібно

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.