Як клонувати InputStream?


162

У мене є InputStream, який я передаю методу, щоб зробити деяку обробку. Я буду використовувати той же InputStream в іншому методі, але після першої обробки, InputStream з'являється закритим всередині методу.

Як я можу клонувати InputStream для надсилання до методу, який його закриває? Є ще одне рішення?

EDIT: методи, що закривають InputStream, - це зовнішній метод з lib. Я не маю контролю над закриттям чи ні.

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 Jericho HTML Parser повертає String, відформатований у правильному форматі. Це все, що мені потрібно на даний момент.
Ренато Діньяні

Відповіді:


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()дзвінку, а в тому, що Потік читається до кінця. Оскільки mark()і, reset()можливо, не є найкращими методами для http-з'єднань, можливо, вам слід поглянути на підхід до масиву байтів, описаний у моїй відповіді.
Ентоні Акціолій

1
І ще одне - ви завжди можете відкрити нове підключення до тієї ж URL-адреси. Дивіться тут: stackoverflow.com/questions/5807340/…
Ентоні Акціолій

11

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

Одне рішення - прочитати всі дані з InputStream в масив байтів, а потім створити навколо цього байтового масиву ByteArrayInputStream і передати цей потік вводу у ваш метод.

Правка 1: Тобто, якщо інший метод також повинен прочитати ті самі дані. Тобто ви хочете "скинути" потік.


Я не знаю, в якій частині вам потрібна допомога. Я думаю, ти вмієш читати з потоку? Прочитайте всі дані з InputStream та запишіть їх у ByteArrayOutputStream. Зателефонуйте доByteArray () на ByteArrayOutputStream після того, як ви прочитали всі дані. Потім передайте цей байтовий масив в конструктор ByteArrayInputStream.
Кай

8

Якщо дані, прочитані з потоку, великі, я рекомендую використовувати TeeInputStream від IO Apache Commons. Таким чином ви можете по суті повторити вхід і пропустити трубу t'd як ваш клон.


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передаєте екземпляр, де ви б пройшли у вхідному потоці. З оригінальним вхідним потоком як параметром конструктора.

Слід зазначити, що цей байт працює для байтів, тому не використовуйте його, якщо висока вимога є вимогою.


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

Цей код вимагає деяких залежностей:

ПОВЕРНЕНО

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

ГРАДИНА

'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

Нижче - рішення з Котліном.

Ви можете скопіювати свій 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

Клас нижче повинен зробити трюк. Просто створіть екземпляр, зателефонуйте на метод "помножити" та вкажіть вихідний потік і кількість дублікатів, які вам потрібні.

Важливо: ви повинні споживати всі клоновані потоки одночасно в окремих потоках.

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локальний ресурс для цього.


Ця відповідь мені незрозуміла. Як ви ініціалізуєте постачальника від існуючого is?
користувач1156544

@ user1156544 Як я писав Клонування вхідного потоку може бути не дуже хорошою ідеєю, оскільки для цього потрібні глибокі знання про деталі клонування вхідного потоку. Ви не можете використовувати постачальника для створення потоку входу для наявного. Постачальник може використовувати java.io.Fileабо, java.net.URLнаприклад, для створення нового вхідного потоку кожного разу, коли він викликається.
SpaceTrucker

Я зараз бачу. Це не буде працювати з вхідним потоком, як явно запитує ОП, але з файлом або URL-адресою, якщо вони є оригінальним джерелом даних. Дякую
користувач1156544
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.