关于java:两次读取流

Read stream twice

如何读取相同的inputstream两次?是否可以复制?

我需要从Web获取一个图像,在本地保存它,然后返回保存的图像。我只是想使用同一个流会更快,而不是对下载的内容启动一个新的流,然后再阅读它。


您可以使用org.apache.commons.io.IOUtils.copy将输入流的内容复制到字节数组,然后使用byte array inputstream从字节数组中重复读取。例如。:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}


根据输入流的来源,您可能无法重置它。您可以使用markSupported()检查是否支持mark()reset()

如果是,可以在输入流上调用reset()返回到开始。如果不是,则需要再次从源读取输入流。


如果你的InputStream支持使用mark,那么你可以先输入mark(),然后输入reset()。如果您的InputStrem不支持mark,那么您可以使用类java.io.BufferedInputStream,这样您就可以将流嵌入这样的BufferedInputStream中。

1
2
3
4
5
    InputStream bufferdInputStream = new BufferedInputStream(yourInputStream);
    bufferdInputStream.mark(some_value);
    //read your bufferdInputStream
    bufferdInputStream.reset();
    //read it again


可以用pushbackinputstream包装输入流。pushbackinputstream允许读取已读取的未读("写回")字节,因此可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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将原始输入流存储为委托。这允许创建新的类定义,允许自动"未读"原始数据。此类的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public class TryReadInputStream extends FilterInputStream {
  private final int maxPushbackBufferSize;

  /**
  * Creates a <wyn>FilterInputStream</wyn>
  * by assigning the  argument <wyn>in</wyn>
  * to the field <wyn>this.in</wyn> so as
  * to remember it for later use.
  *
  * @param in the underlying input stream, or <wyn>null</wyn> 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 <wyn>length</wyn> 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 <wyn>buffer</wyn>
   * @aram length how many bytes to read from the stream to buff. Length needs to be less than
   *        <wyn>maxPushbackBufferSize</wyn> 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);
    }
  }

}

这个类有两个方法。一个用于读取现有缓冲区(定义类似于调用inputstream类的public int read(byte b[], int off, int len))。第二个返回新缓冲区(如果要读取的缓冲区大小未知,这可能更有效)。

现在让我们来看看我们班的实际情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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


  }



}

如果您使用的是InputStream的实现,那么您可以检查InputStream#markSupported()的结果,告诉您是否可以使用mark()/reset()方法。

如果您可以在阅读时标记流,那么调用reset()返回开始。

如果你不能,你就得再打开一条小溪。

另一种解决方案是将inputstream转换为字节数组,然后根据需要在数组中迭代多次。在这个转换后的输入流中,您可以使用第三方LIBS或JAVA中的多个解决方案找到字节数组中的几个解决方案。注意,如果读取内容太大,可能会遇到一些内存问题。

最后,如果您需要读取图像,请使用:

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

使用ImageIO#read(java.net.URL)还允许您使用缓存。


怎么样:

1
2
3
4
5
6
7
8
9
10
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'

    }


将inputstream转换为字节,然后将其传递给savefile函数,在该函数中,您将其组装为inputstream。同样在原始函数中,使用字节来执行其他任务


InputStream分为两部分,同时避免将所有数据加载到内存中,然后独立处理:

  • 创建一对OutputStream,精确地说:PipedOutputStream
  • 用pipedinputstream连接每个pipedoutsptream,这些PipedInputStream是返回的InputStream
  • 将源输入流与刚刚创建的OutputStream连接起来。因此,从源代码InputStream中读取的所有内容都将写入两个OutputStream。不需要实现这一点,因为它已经在TeeInputStream中完成了(commons.io)。
  • 在一个单独的线程中,读取整个源输入流,并隐式地将输入数据传输到目标输入流。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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);
    }
  • 注意消耗完后关闭输入流,关闭运行的线程:TeeInputStream.readAllBytes()

    在这种情况下,您需要将其拆分为多个InputStream,而不只是两个。在前面的代码片段中,为您自己的实现替换类TeeOutputStream,它将封装List并重写OutputStream接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    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();
            }
        }
    }

    如果有人正在运行一个Spring引导应用程序,而你想要读一个RestTemplate的响应主体(这就是为什么我要读两次流),那么有一种干净(er)的方法可以做到这一点。

    首先,您需要使用Spring的StreamUtils将流复制到字符串:

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

    但这不是全部。您还需要使用一个可以为您缓冲流的请求工厂,例如:

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

    或者,如果您正在使用工厂bean,那么(这是Kotlin,但是仍然是):

    1
    2
    3
    4
    5
    6
    @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/