同じ入力ストリームを2回読み取るにはどうすればよいですか?何とかコピーすることは可能ですか?
Webから画像を取得し、ローカルに保存して、保存した画像を返す必要があります。ダウンロードしたコンテンツに新しいストリームを開始してから再度読み込むのではなく、同じストリームを使用する方が高速になると考えました。
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);
}
InputStreamの発信元によっては、リセットできない場合があります。 mark()
を使用して、reset()
とmarkSupported()
がサポートされているかどうかを確認できます。
そうであれば、InputStreamでreset()
を呼び出して最初に戻ることができます。そうでない場合は、ソースからInputStreamを再度読み取る必要があります。
InputStream
がmarkの使用をサポートしている場合、mark()
inputStreamを実行してからreset()
itを実行できます。 InputStrem
がマークをサポートしていない場合は、クラスJava.io.BufferedInputStream
を使用できます。したがって、BufferedInputStream
内にストリームを埋め込むことができます。
InputStream bufferdInputStream = new BufferedInputStream(yourInputStream);
bufferdInputStream.mark(some_value);
//read your bufferdInputStream
bufferdInputStream.reset();
//read it again
PushbackInputStreamで入力ストリームをラップできます。 PushbackInputStreamでは、すでに読み取られたnread( "write back")バイトが許可されているため、次のようにすることができます。
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は、元の入力ストリームをデリゲートとして保存します。これにより、「nread」元のデータを自動的に許可する新しいクラス定義を作成できます。このクラスの定義は次のとおりです。
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);
}
}
}
このクラスには2つのメソッドがあります。既存のバッファに読み込むためのもの(定義は、InputStreamクラスのpublic int read(byte b[], int off, int len)
の呼び出しに似ています)。新しいバッファーを返す2番目(これは、読み取るバッファーのサイズが不明な場合により効果的です)。
クラスの動作を見てみましょう。
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をバイト配列に変換し、必要な回数だけ配列を反復処理することです。この投稿でいくつかの解決策を見つけることができます JavaでInputStreamをバイト配列に変換する サードパーティライブラリを使用するかどうか。注意、読み取り内容が大きすぎる場合は、メモリの問題が発生する可能性があります。
最後に、イメージを読み取る必要がある場合は、次を使用します。
BufferedImage image = ImageIO.read(new URL("http://www.example.com/images/toto.jpg"));
ImageIO#read(Java.net.URL)
を使用すると、キャッシュを使用することもできます。
入力ストリームをバイトに変換してから、それをsavefile関数に渡し、そこで同じものを入力ストリームにアセンブルします。また、元の関数ではバイトを使用して他のタスクに使用します
どうですか:
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
を2つに分割するには、にメモリのすべてのデータをロードせずに、それらを個別に処理します。
OutputStream
を作成します。正確にはPipedOutputStream
PipedInputStream
は返されるInputStream
です。OutputStream
とソースInputStreamを接続します。したがって、ソースInputStream
からすべてを読み取ると、両方のOutputStream
に書き込まれます。 TeeInputStream
(commons.io)で既に行われているため、実装する必要はありません。分離されたスレッド内で、ソースの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()
場合には、を2つではなく、複数の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();
}
}
}
誰かがSpring Bootアプリで実行されており、RestTemplate
の応答本文を読みたい場合(これが2回ストリームを読みたい理由です)、これを行うきれいな(より)方法があります。
まず、SpringのStreamUtils
を使用してストリームを文字列にコピーする必要があります。
String text = StreamUtils.copyToString(response.getBody(), Charset.defaultCharset()))
しかし、それだけではありません。次のように、ストリームをバッファリングできるリクエストファクトリを使用する必要もあります。
ClientHttpRequestFactory factory = new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory());
RestTemplate restTemplate = new RestTemplate(factory);
または、ファクトリーBeanを使用している場合(これはKotlinですが、それでも):
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
fun createRestTemplate(): RestTemplate = RestTemplateBuilder()
.requestFactory { BufferingClientHttpRequestFactory(SimpleClientHttpRequestFactory()) }
.additionalInterceptors(loggingInterceptor)
.build()