Как клонировать InputStream?

162

У меня есть InputStream, который я передаю методу для некоторой обработки. Я буду использовать тот же InputStream в другом методе, но после первой обработки InputStream оказывается закрытым внутри метода.

Как я могу клонировать InputStream для отправки в метод, который его закрывает? Есть другое решение?

РЕДАКТИРОВАТЬ: методы, которые закрывают InputStream является внешним методом из библиотеки. Я не могу контролировать закрытие или нет.

private String getContent(HttpURLConnection con) {
    InputStream content = null;
    String charset = "";
    try {
        content = con.getInputStream();
        CloseShieldInputStream csContent = new CloseShieldInputStream(content);
        charset = getCharset(csContent);            
        return  IOUtils.toString(content,charset);
    } catch (Exception e) {
        System.out.println("Error downloading page: " + e);
        return null;
    }
}

private String getCharset(InputStream content) {
    try {
        Source parser = new Source(content);
        return parser.getEncoding();
    } catch (Exception e) {
        System.out.println("Error determining charset: " + e);
        return "UTF-8";
    }
}
Ренато Динхани
источник
2
Вы хотите «сбросить» поток после возврата метода? Т.е. читать поток с самого начала?
aioobe
Да, методы, закрывающие InputStream, возвращают кодировку, в которой он был закодирован. Второй метод заключается в преобразовании InputStream в String с использованием кодировки, найденной в первом методе.
Ренато Динхани
В этом случае вы должны быть в состоянии сделать то, что я описываю в моем ответе.
Кай
Я не знаю лучший способ ее решить, но я решаю свою проблему иначе. Метод toString анализатора HTML Jericho возвращает строку, отформатированную в правильном формате. Это все, что мне нужно в данный момент.
Ренато Динхани

Ответы:

188

Если все, что вы хотите сделать, это прочитать одну и ту же информацию более одного раза, а входные данные достаточно малы, чтобы поместиться в память, вы можете скопировать данные из вашего объектаInputStream в ByteArrayOutputStream .

Тогда вы можете получить ассоциативный массив байт и открыт как многие «клонированный» ByteArrayInputStream s , как вам нравится.

ByteArrayOutputStream baos = new ByteArrayOutputStream();

// Fake code simulating the copy
// You can generally do better with nio if you need...
// And please, unlike me, do something about the Exceptions :D
byte[] buffer = new byte[1024];
int len;
while ((len = input.read(buffer)) > -1 ) {
    baos.write(buffer, 0, len);
}
baos.flush();

// Open new InputStreams using the recorded bytes
// Can be repeated as many times as you wish
InputStream is1 = new ByteArrayInputStream(baos.toByteArray()); 
InputStream is2 = new ByteArrayInputStream(baos.toByteArray()); 

Но если вам действительно нужно оставить исходный поток открытым для получения новых данных, вам нужно будет отследить этот внешний close()метод и не допустить его вызова каким-либо образом.

ОБНОВЛЕНИЕ (2019):

Начиная с Java 9, средние биты можно заменить на InputStream.transferTo:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
input.transferTo(baos);
InputStream firstClone = new ByteArrayInputStream(baos.toByteArray()); 
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray()); 
Энтони Аккиоли
источник
Я нашел другое решение моей проблемы, которое не включает в себя копирование InputStream, но я думаю, что если мне нужно скопировать InputStream, это лучшее решение.
Ренато Динхани
7
Этот подход потребляет память, пропорциональную полному содержанию входного потока. Лучше использовать, TeeInputStreamкак описано в ответе здесь .
aioobe
2
IOUtils (из Apache Commons) имеет метод копирования, который будет делать буфер чтения / записи в середине вашего кода.
реабилитация
31

Вы хотите использовать Apache CloseShieldInputStream:

Это оболочка, которая предотвращает закрытие потока. Вы бы сделали что-то вроде этого.

InputStream is = null;

is = getStream(); //obtain the stream 
CloseShieldInputStream csis = new CloseShieldInputStream(is);

// call the bad function that does things it shouldn't
badFunction(csis);

// happiness follows: do something with the original input stream
is.read();
Фей
источник
Выглядит хорошо, но здесь не работает. Я буду редактировать свой пост с кодом.
Ренато Динхани
CloseShieldне работает, потому что ваш исходный HttpURLConnectionвходной поток где-то закрыт. Разве ваш метод не должен вызывать IOUtils с защищенным потоком IOUtils.toString(csContent,charset)?
Энтони Аккиоли
Может быть, это может быть. Я могу предотвратить закрытие HttpURLConnection?
Ренато Динхани
1
@Renato. Возможно, проблема вовсе не в close()вызове, а в том, что Stream читается до конца. Поскольку mark()и, reset()возможно, не самые лучшие методы для http-соединений, возможно, вам стоит взглянуть на подход байтового массива, описанный в моем ответе.
Энтони Аккиоли
1
Еще одна вещь, вы всегда можете открыть новое соединение с тем же URL. Смотрите здесь: stackoverflow.com/questions/5807340/…
Энтони Акциоли
11

Вы не можете клонировать его, и то, как вы собираетесь решить свою проблему, зависит от источника данных.

Одним из решений является чтение всех данных из InputStream в байтовый массив, а затем создание ByteArrayInputStream вокруг этого байтового массива и передача этого входного потока в ваш метод.

Редактировать 1: То есть, если другой метод также должен прочитать те же данные. Т.е. вы хотите «сбросить» поток.

Кая
источник
Я не знаю, с какой частью вам нужна помощь. Я думаю, вы знаете, как читать из потока? Считайте все данные из InputStream и запишите данные в ByteArrayOutputStream. Вызовите toByteArray () для ByteArrayOutputStream после того, как вы завершите чтение всех данных. Затем передайте этот байтовый массив в конструктор ByteArrayInputStream.
Кай
8

Если данные, считываемые из потока, большие, я бы рекомендовал использовать TeeInputStream из Apache Commons IO. Таким образом, вы можете по существу реплицировать ввод и передать в качестве своего клона канал.

Натан Райан
источник
5

Это может работать не во всех ситуациях, но вот что я сделал: я расширил класс FilterInputStream и выполняю необходимую обработку байтов, когда внешняя библиотека читает данные.

public class StreamBytesWithExtraProcessingInputStream extends FilterInputStream {

    protected StreamBytesWithExtraProcessingInputStream(InputStream in) {
        super(in);
    }

    @Override
    public int read() throws IOException {
        int readByte = super.read();
        processByte(readByte);
        return readByte;
    }

    @Override
    public int read(byte[] buffer, int offset, int count) throws IOException {
        int readBytes = super.read(buffer, offset, count);
        processBytes(buffer, offset, readBytes);
        return readBytes;
    }

    private void processBytes(byte[] buffer, int offset, int readBytes) {
       for (int i = 0; i < readBytes; i++) {
           processByte(buffer[i + offset]);
       }
    }

    private void processByte(int readByte) {
       // TODO do processing here
    }

}

Затем вы просто передаете экземпляр того, StreamBytesWithExtraProcessingInputStreamгде вы бы прошли во входном потоке. С исходным входным потоком в качестве параметра конструктора.

Следует отметить, что это работает байт за байтом, поэтому не используйте его, если требуется высокая производительность.

Diederik
источник
3

UPD. Проверьте комментарий раньше. Это не совсем то, что спросили.

Если вы используете, apache.commonsвы можете копировать потоки с помощью IOUtils.

Вы можете использовать следующий код:

InputStream = IOUtils.toBufferedInputStream(toCopy);

Вот полный пример, подходящий для вашей ситуации:

public void cloneStream() throws IOException{
    InputStream toCopy=IOUtils.toInputStream("aaa");
    InputStream dest= null;
    dest=IOUtils.toBufferedInputStream(toCopy);
    toCopy.close();
    String result = new String(IOUtils.toByteArray(dest));
    System.out.println(result);
}

Этот код требует некоторых зависимостей:

MAVEN

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
</dependency>

Gradle

'commons-io:commons-io:2.4'

Вот ссылка DOC для этого метода:

Извлекает все содержимое InputStream и представляет те же данные, что и результат InputStream. Этот метод полезен там, где

Источник InputStream медленный. С ним связаны сетевые ресурсы, поэтому мы не можем держать его открытым в течение длительного времени. Это связано с тайм-аутом сети.

Вы можете найти больше об этом IOUtilsздесь: http://commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/IOUtils.html#toBufferedInputStream(java.io.InputStream)

Андрей Е
источник
7
Это не клонирует входной поток, а только буферизует его. Это не то же самое; ОП хочет перечитать (копию) того же потока.
Рафаэль
1

Ниже решение с Kotlin.

Вы можете скопировать свой InputStream в ByteArray

val inputStream = ...

val byteOutputStream = ByteArrayOutputStream()
inputStream.use { input ->
    byteOutputStream.use { output ->
        input.copyTo(output)
    }
}

val byteInputStream = ByteArrayInputStream(byteOutputStream.toByteArray())

Если вам нужно прочитать byteInputStreamнесколько раз, позвоните, byteInputStream.reset()прежде чем читать снова.

https://code.luasoftware.com/tutorials/kotlin/how-to-clone-inputstream/

Десмонд Луа
источник
0

Класс ниже должен сделать свое дело. Просто создайте экземпляр, вызовите метод «multiply» и предоставьте исходный поток ввода и необходимое количество дубликатов.

Важно: вы должны использовать все клонированные потоки одновременно в отдельных потоках.

package foo.bar;

import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class InputStreamMultiplier {
    protected static final int BUFFER_SIZE = 1024;
    private ExecutorService executorService = Executors.newCachedThreadPool();

    public InputStream[] multiply(final InputStream source, int count) throws IOException {
        PipedInputStream[] ins = new PipedInputStream[count];
        final PipedOutputStream[] outs = new PipedOutputStream[count];

        for (int i = 0; i < count; i++)
        {
            ins[i] = new PipedInputStream();
            outs[i] = new PipedOutputStream(ins[i]);
        }

        executorService.execute(new Runnable() {
            public void run() {
                try {
                    copy(source, outs);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

        return ins;
    }

    protected void copy(final InputStream source, final PipedOutputStream[] outs) throws IOException {
        byte[] buffer = new byte[BUFFER_SIZE];
        int n = 0;
        try {
            while (-1 != (n = source.read(buffer))) {
                //write each chunk to all output streams
                for (PipedOutputStream out : outs) {
                    out.write(buffer, 0, n);
                }
            }
        } finally {
            //close all output streams
            for (PipedOutputStream out : outs) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
встром кодер
источник
Не отвечает на вопрос. Он хочет использовать поток в одном методе, чтобы определить кодировку, а затем перечитать его вместе с его кодировкой во втором методе.
маркиз Лорн
0

Клонирование входного потока не может быть хорошей идеей, потому что это требует глубоких знаний о деталях клонируемого входного потока. Обходным путем для этого является создание нового входного потока, который снова читает из того же источника.

Таким образом, при использовании некоторых функций Java 8 это будет выглядеть так:

public class Foo {

    private Supplier<InputStream> inputStreamSupplier;

    public void bar() {
        procesDataThisWay(inputStreamSupplier.get());
        procesDataTheOtherWay(inputStreamSupplier.get());
    }

    private void procesDataThisWay(InputStream) {
        // ...
    }

    private void procesDataTheOtherWay(InputStream) {
        // ...
    }
}

Этот метод имеет положительный эффект от повторного использования уже существующего кода - создание входного потока, инкапсулированного в inputStreamSupplier. И нет необходимости поддерживать второй путь кода для клонирования потока.

С другой стороны, если чтение из потока является дорогостоящим (поскольку оно выполняется по соединению с низкой пропускной способностью), тогда этот метод удвоит затраты. Это можно обойти, используя конкретного поставщика, который сначала будет хранить потоковое содержимое локально и предоставит InputStreamдля этого теперь локальный ресурс.

SpaceTrucker
источник
Этот ответ мне не понятен. Как вы инициализируете поставщика из существующего is?
user1156544
@ user1156544 Как я уже писал, клонирование входного потока может быть не очень хорошей идеей, потому что это требует глубоких знаний о деталях клонируемого входного потока. Вы не можете использовать поставщика для создания входного потока из существующего. Поставщик может использовать java.io.Fileили, java.net.URLнапример, для создания нового входного потока при каждом его вызове.
SpaceTrucker
Я вижу сейчас. Это не будет работать с inputtream, как явно указывает OP, но с File или URL, если они являются исходным источником данных. Спасибо
user1156544