Java 读取输入流两次

Java 读取输入流两次,java,inputstream,Java,Inputstream,如何读取同一输入流两次?有可能以某种方式复制它吗 我需要从网络上获取图像,将其保存在本地,然后返回保存的图像。我只是认为使用同一个流会更快,而不是启动下载内容的新流,然后再次阅读。根据输入流的来源,您可能无法重置它。您可以使用markSupported()检查是否支持mark()和reset() 如果是,您可以在InputStream上调用reset(),返回到开头。如果没有,则需要再次从源读取InputStream。如果使用的是的实现,则可以检查该实现的结果,告诉您是否可以使用该方法/ 如果可

如何读取同一输入流两次?有可能以某种方式复制它吗


我需要从网络上获取图像,将其保存在本地,然后返回保存的图像。我只是认为使用同一个流会更快,而不是启动下载内容的新流,然后再次阅读。

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


如果是,您可以在InputStream上调用
reset()
,返回到开头。如果没有,则需要再次从源读取InputStream。

如果使用的是的实现,则可以检查该实现的结果,告诉您是否可以使用该方法/

如果可以在读取时标记流,则调用
reset()
返回开始

如果你不能,你将不得不再次打开一条小溪

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

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

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

使用还允许您使用缓存。

将inputstream转换为字节,然后将其传递给savefile函数,在这里您可以将其组装为inputstream。 另外,在原始函数“使用字节用于其他任务”中,您可以使用将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);
}

如果您的
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

您可以使用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();
  }


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

现在让我们来看看我们的课堂:

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


  }



}
那么:

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'

    }

如果有人在Spring Boot应用程序中运行,并且您想要读取
RestTemplate
(这就是我想要读取两次流的原因)的响应体,那么有一种干净的(er)方法可以做到这一点

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

String text = StreamUtils.copyToString(response.getBody(), Charset.defaultCharset()))
但这还不是全部。您还需要使用一个可以为您缓冲流的请求工厂,如下所示:

ClientHttpRequestFactory factory = new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory());
RestTemplate restTemplate = new RestTemplate(factory);
或者,如果您使用的是工厂bean,那么(这是Kotlin,但无论如何):


来源:

用于将
输入流
一分为二,同时避免加载内存中的所有数据,然后独立处理:

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

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

    如果您使用RestTemplate进行http调用,只需添加一个拦截器。 响应体由ClientHttpResponse的实现缓存。 现在,可以根据需要多次从respose检索inputstream

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

    可能使用mark和Reset我认为这是个坏主意,生成的数组可能很大,会占用设备的内存。@Paul Grime:IOUtils.tobyarray也从内部调用copy方法。正如@Ankit所说,这个解决方案对我无效,因为输入是内部读取的,不能重复使用。我知道这个注释已经过时了,但是,在第一个选项中,如果您将inputstream作为字节数组读取,这是否意味着您正在将所有数据加载到内存中?如果你正在加载像大文件这样的东西,这可能是一个大问题?@jaxkodex,是的,这是正确的。如果您作为一名开发人员更了解您正在处理的流的实际类型,那么您可以编写更合适的自定义行为。提供的答案是一个一般的抽象。可以使用IOUtils.toByteArray(InputStream)在一次调用中获取字节数组。使用
    ImageIO#read(java.net.URL)
    时需要提醒一句:一些web服务器和CDN可能会拒绝裸调用(即,没有让服务器相信调用来自web浏览器的用户代理)由
    ImageIO#read
    制作。在这种情况下,使用
    URLConnection.openConnection()
    将用户代理设置为该连接+使用`ImageIO.read(InputStream)大多数情况下都可以做到这一点。
    InputStream
    不是接口InputStream不支持'mark'-您可以在is上调用mark,但它什么都不做。同样,在IS上调用reset也会引发异常。@ayahuasca
    InputStream
    子类,如
    BufferedInputStream
    支持“标记”。缓冲输入流只能标记回缓冲区大小,因此如果源代码不合适,则无法返回到开始处。@L.Blanc抱歉,这似乎不正确。查看一下BufferedInputStream.fill(),t
    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);
    }
    
    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();
            }
        }
    }
    
    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); 
    
    ByteArrayInputStream ins = new ByteArrayInputStream("Hello".getBytes());
    System.out.println("ins.available() at begining:: " + ins.available());
    ins.mark(0);
    // Read input stream for some operations
    System.out.println("ins.available() after reading :: " + ins.available());
        ins.reset();
        System.out.println("ins.available() after resetting :: " + ins.available());
        // ins is ready for reading once again.