Akışı iki kez okuyun


127

Aynı girdi akışını iki kez nasıl okursunuz? Bir şekilde kopyalamak mümkün mü?

Web'den bir resim almam, yerel olarak kaydetmem ve ardından kaydedilen resmi iade etmem gerekiyor. İndirilen içeriğe yeni bir akış başlatmak ve ardından tekrar okumak yerine aynı akışı kullanmanın daha hızlı olacağını düşündüm.


1
Belki işaretle ve sıfırla
Vyacheslav Shylkin

Yanıtlar:


114

Sen kullanabilirsiniz org.apache.commons.io.IOUtils.copybir bayt dizisine InputStream içeriğini kopyalamak için, ve sonra tekrar tekrar ByteArrayInputStream kullanarak bayt diziden okuyun. Örneğin:

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
Mark tüm türler için desteklenmediğinden, bunun tek geçerli çözüm olduğunu düşünüyorum.
Warpzit

3
@Paul Grime: IOUtils.toByeArray dahili olarak copy yöntemini içeriden de çağırır.
Ankit

4
@Ankit'in dediği gibi, girdi dahili olarak okunduğu ve tekrar kullanılamayacağı için bu çözüm benim için geçerli değil.
Xtreme Biker

30
Bu yorumun zaman aşımına uğradığını biliyorum, ama burada ilk seçenekte, girdi akışını bir bayt dizisi olarak okursanız, bu tüm verileri belleğe yüklediğiniz anlamına gelmez mi? büyük dosyalar gibi bir şey yüklüyorsanız bu büyük bir sorun olabilir?
jaxkodex

2
Bir çağrıda bayt dizisini almak için IOUtils.toByteArray (InputStream) kullanılabilir.
faydalı

30

InputStream'in nereden geldiğine bağlı olarak, onu sıfırlayamayabilirsiniz. Sen olmadığını kontrol edebilirsiniz mark()ve reset()kullanma desteklenmektedir markSupported().

Eğer öyleyse, reset()başlangıca dönmek için InputStream'de arama yapabilirsiniz . Değilse, InputStream'i kaynaktan tekrar okumanız gerekir.


1
InputStream, 'mark'ı desteklemez - bir IS'de mark çağırabilirsiniz, ancak hiçbir şey yapmaz. Aynı şekilde, bir IS'de sıfırlama çağrısı bir istisna atacaktır.
ayahuasca

4
@ayahuasca InputStreamabonelikleri BufferedInputStream' mark'ı destekliyor gibi
Dmitry Bogdanovich

10

InputStreamDesteğiniz mark kullanarak ise , o zaman mark()inputStream'inizi ve sonra reset()onu yapabilirsiniz. Eğer InputStremişaretiniz desteklemiyorsa sınıfı kullanabilir java.io.BufferedInputStream, böylece akışınızı buna BufferedInputStreambenzer bir

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

1
Arabelleğe alınmış bir giriş akışı yalnızca arabellek boyutuna geri dönebilir, bu nedenle kaynak sığmazsa, en başa geri dönemezsiniz.
L. Blanc

@ L.Blanc üzgünüm ama bu doğru görünmüyor. Bir göz atın BufferedInputStream.fill(), yeni arabellek boyutunun yalnızca marklimitve ile karşılaştırıldığı "büyüme tamponu" bölümü var MAX_BUFFER_SIZE.
eugene82

8

PushbackInputStream ile giriş akışını sarmalayabilirsiniz. PushbackInputStream, halihazırda okunmuş olan okunmamış (" geri yazma ") baytlara izin verir , böylece şunları yapabilirsiniz:

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


}

Lütfen PushbackInputStream'in dahili bayt arabelleğini depoladığını ve böylece bellekte "geri yazılan" baytları tutan bir arabellek oluşturduğunu unutmayın.

Bu yaklaşımı bilerek daha da ileri gidebilir ve onu FilterInputStream ile birleştirebiliriz. FilterInputStream, orijinal giriş akışını temsilci olarak depolar. Bu, orijinal verilerin otomatik olarak " okunmamış " olmasına izin veren yeni sınıf tanımının oluşturulmasına izin verir . Bu sınıfın tanımı şöyledir:

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

}

Bu sınıfın iki yöntemi vardır. Mevcut arabelleğe okumak için bir tane (tanım, public int read(byte b[], int off, int len)InputStream sınıfının çağrılmasına benzer ). İkincisi, yeni arabellek döndürür (okunacak arabelleğin boyutu bilinmiyorsa bu daha etkili olabilir).

Şimdi sınıfımızı iş başında görelim:

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

Bir uygulamasını kullanıyorsanız, / yöntemini kullanıp kullanamayacağınızı söyleyen InputStreamsonucunu kontrol InputStream#markSupported()edebilirsiniz .mark()reset()

Akışı okurken işaretleyebiliyorsanız, reset()başlamak için geri dönmek için arayın .

Yapamazsanız tekrar bir akış açmanız gerekecek.

Başka bir çözüm, InputStream'i bayt dizisine dönüştürmek ve ardından dizi üzerinde ihtiyaç duyduğunuz kadar yineleme yapmaktır. Bu yazıda, InputStream'i 3. parti kitaplıklar kullanarak veya kullanmadan Java'da bayt dizisine dönüştürün . Dikkat, okunan içerik çok büyükse, bazı bellek sorunları yaşayabilirsiniz.

Son olarak, ihtiyacınız olan resmi okumaksa, şunu kullanın:

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

Kullanmak ImageIO#read(java.net.URL)ayrıca önbelleği kullanmanıza izin verir.


1
kullanırken bir uyarı kelimesi ImageIO#read(java.net.URL): bazı web sunucuları ve CDN'ler çıplak çağrıları reddedebilir (örneğin, sunucuyu çağrının bir web tarayıcısından geldiğine inandıran bir Kullanıcı Aracısı olmadan)ImageIO#read . Bu durumda, URLConnection.openConnection()kullanıcı aracısını bu bağlantıya + `ImageIO.read (InputStream) kullanarak ayarlamak, çoğu zaman işe yarayacaktır.
Clint Eastwood

InputStreambir arayüz değil
Brice

3

Peki ya:

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
Bu berbat bir fikir. Tüm yayın içeriğini bu şekilde hafızaya koyarsınız.
Niels Doucet'in

3

İkiye bölmek için InputStream,Tüm verileri belleğe yüklemekten kaçınırken ve ardından bunları bağımsız olarak işlemek için:

  1. OutputStreamTam olarak birkaç tane oluşturun :PipedOutputStream
  2. Her PipedOutputStream'i bir PipedInputStream ile bağlayın, bunlar PipedInputStream döndürülür InputStream.
  3. Yeni oluşturulan kaynak girdisi akışını bağlayın OutputStream. Yani, her şey onu kaynaktan okur, her InputStreamikisinde de yazılırOutputStream . Bunu uygulamaya gerek yok çünkü zaten TeeInputStream(commons.io) 'da yapıldı.
  4. Ayrılmış bir iş parçacığı içinde tüm kaynak girdisi akışını okuyun ve dolaylı olarak girdi verileri hedef inputStreams'e aktarılır.

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

Kullanıldıktan sonra inputStreams'i ve çalışan iş parçacığını kapatmayı unutmayın: TeeInputStream.readAllBytes()

Durumda, onuInputStream sadece ikiye bölmek yerine birden fazla bölmeniz gerekir . Önceki kod parçasında, TeeOutputStreamkendi uygulamanız için sınıfı değiştirin; bu, a'yı List<OutputStream>kapsayacak ve OutputStreamarabirimi geçersiz kılacaktır :

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

Lütfen 4. adımı biraz daha açıklar mısınız? Okumayı neden manuel olarak tetiklemeliyiz? PipedInputStream öğesinin okunması neden inputStream kaynağının okumasını tetiklemiyor? Ve neden eşzamansız çağrıyı yapıyoruz?
Дмитрий Кулешов

2

Girdi akışını baytlara dönüştürün ve ardından aynısını girdi akışına bir araya getirdiğiniz kayıt dosyası işlevine geçirin. Ayrıca orijinal işlevde diğer görevlerde kullanmak için bayt kullanın


5
Bu konuda kötü bir fikir diyorum, ortaya çıkan dizi çok büyük olabilir ve cihaz hafızasını soyacaktır.
Kevin Parker

0

Spring Boot uygulamasında çalışan birinin olması ve bir RestTemplate (bu yüzden bir akışı iki kez okumak istiyorum), bunu yapmanın temiz (er) bir yolu var.

Her şeyden önce, StreamUtilsakışı bir String'e kopyalamak için Spring'i kullanmanız gerekir :

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

Ama hepsi bu değil. Ayrıca, akışı sizin için arabelleğe alabilecek bir istek fabrikası kullanmanız gerekir, örneğin:

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

Veya fabrika fasulyesini kullanıyorsanız (bu Kotlin ama yine de):

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

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


0

Http aramaları yapmak için RestTemplate kullanıyorsanız Bir durdurucu eklemeniz yeterlidir. Yanıt gövdesi, ClientHttpResponse uygulamasıyla önbelleğe alınır. Artık girdi akışı, ihtiyaç duyduğumuz kadar çok kez yanıtlamadan alınabilir

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); 
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.