Как кэшировать InputStream для многократного использования - PullRequest
24 голосов
/ 29 мая 2009

У меня есть InputStream файла, и я использую компоненты Apache Poi для чтения из него следующим образом:

POIFSFileSystem fileSystem = new POIFSFileSystem(inputStream);

Проблема в том, что мне нужно использовать один и тот же поток несколько раз, а POIFSFileSystem закрывает поток после использования.

Каков наилучший способ кэширования данных из входного потока и последующего предоставления большего количества входных потоков в другую POIFSFileSystem?

РЕДАКТИРОВАТЬ 1:

Под кешем я подразумевал хранилище для последующего использования, а не для ускорения работы приложения. Также лучше ли просто прочитать входной поток в массив или строку, а затем создать входные потоки для каждого использования?

РЕДАКТИРОВАТЬ 2:

Извините, что вновь открыл вопрос, но условия несколько иные при работе внутри рабочего стола и веб-приложения. Прежде всего, InputStream, который я получаю из org.apache.commons.fileupload.FileItem в моем веб-приложении tomcat, не поддерживает маркировку, поэтому не может быть сброшен.

Во-вторых, я хотел бы иметь возможность хранить файл в памяти для более быстрого доступа и уменьшения проблем при работе с файлами.

Ответы [ 10 ]

20 голосов
/ 20 августа 2009

Попробуйте BufferedInputStream, который добавляет функциональность пометки и сброса к другому входному потоку, и просто переопределяет его метод close:

public class UnclosableBufferedInputStream extends BufferedInputStream {

    public UnclosableBufferedInputStream(InputStream in) {
        super(in);
        super.mark(Integer.MAX_VALUE);
    }

    @Override
    public void close() throws IOException {
        super.reset();
    }
}

Итак:

UnclosableBufferedInputStream  bis = new UnclosableBufferedInputStream (inputStream);

и используйте bis везде, где ранее использовался inputStream.

17 голосов
/ 29 мая 2009

вы можете украсить InputStream, передаваемый в POIFSFileSystem , с версией, которая при вызове close () отвечает ответом reset ():

class ResetOnCloseInputStream extends InputStream {

    private final InputStream decorated;

    public ResetOnCloseInputStream(InputStream anInputStream) {
        if (!anInputStream.markSupported()) {
            throw new IllegalArgumentException("marking not supported");
        }

        anInputStream.mark( 1 << 24); // magic constant: BEWARE
        decorated = anInputStream;
    }

    @Override
    public void close() throws IOException {
        decorated.reset();
    }

    @Override
    public int read() throws IOException {
        return decorated.read();
    }
}

TestCase

static void closeAfterInputStreamIsConsumed(InputStream is)
        throws IOException {
    int r;

    while ((r = is.read()) != -1) {
        System.out.println(r);
    }

    is.close();
    System.out.println("=========");

}

public static void main(String[] args) throws IOException {
    InputStream is = new ByteArrayInputStream("sample".getBytes());
    ResetOnCloseInputStream decoratedIs = new ResetOnCloseInputStream(is);
    closeAfterInputStreamIsConsumed(decoratedIs);
    closeAfterInputStreamIsConsumed(decoratedIs);
    closeAfterInputStreamIsConsumed(is);
}

РЕДАКТИРОВАТЬ 2

вы можете прочитать весь файл в байте [] (режим slurp), а затем передать его в ByteArrayInputStream

4 голосов
/ 15 февраля 2011

Это работает правильно:

byte[] bytes = getBytes(inputStream);
POIFSFileSystem fileSystem = new POIFSFileSystem(new ByteArrayInputStream(bytes));

где getBytes выглядит так:

private static byte[] getBytes(InputStream is) throws IOException {
    byte[] buffer = new byte[8192];
ByteArrayOutputStream baos = new ByteArrayOutputStream(2048);
int n;
baos.reset();

while ((n = is.read(buffer, 0, buffer.length)) != -1) {
      baos.write(buffer, 0, n);
    }

   return baos.toByteArray();
 }
2 голосов
/ 23 сентября 2013

Используйте приведенную ниже реализацию для более индивидуального использования -

public class ReusableBufferedInputStream extends BufferedInputStream
{

    private int totalUse;
    private int used;

    public ReusableBufferedInputStream(InputStream in, Integer totalUse)
    {
        super(in);
        if (totalUse > 1)
        {
            super.mark(Integer.MAX_VALUE);
            this.totalUse = totalUse;
            this.used = 1;
        }
        else
        {
            this.totalUse = 1;
            this.used = 1;
        }
    }

    @Override
    public void close() throws IOException
    {
        if (used < totalUse)
        {
            super.reset();
            ++used;
        }
        else
        {
            super.close();
        }
    }
}
1 голос
/ 30 ноября 2017

Этот ответ повторяет предыдущие 1 | 2 на основе BufferInputStream. Основные изменения в том, что это позволяет бесконечное повторное использование. И заботится о закрытии исходного входного потока для освобождения системных ресурсов. Ваша ОС определяет их ограничение, и вы не хотите, чтобы в программе не было файловых дескрипторов (). Именно поэтому вам всегда следует «потреблять» ответы, например, с помощью apache EntityUtils.consumeQuietly()). EDIT Обновлен код для обработки для готовых потребителей, которые используют read(buffer, offset, length), в этом случае может случиться, что BufferedInputStream изо всех сил пытается посмотреть на источник, этот код защищает от такого использования.

public class CachingInputStream extends BufferedInputStream {    
    public CachingInputStream(InputStream source) {
        super(new PostCloseProtection(source));
        super.mark(Integer.MAX_VALUE);
    }

    @Override
    public synchronized void close() throws IOException {
        if (!((PostCloseProtection) in).decoratedClosed) {
            in.close();
        }
        super.reset();
    }

    private static class PostCloseProtection extends InputStream {
        private volatile boolean decoratedClosed = false;
        private final InputStream source;

        public PostCloseProtection(InputStream source) {
            this.source = source;
        }

        @Override
        public int read() throws IOException {
            return decoratedClosed ? -1 : source.read();
        }

        @Override
        public int read(byte[] b) throws IOException {
            return decoratedClosed ? -1 : source.read(b);
        }

        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            return decoratedClosed ? -1 : source.read(b, off, len);
        }

        @Override
        public long skip(long n) throws IOException {
            return decoratedClosed ? 0 : source.skip(n);
        }

        @Override
        public int available() throws IOException {
            return source.available();
        }

        @Override
        public void close() throws IOException {
            decoratedClosed = true;
            source.close();
        }

        @Override
        public void mark(int readLimit) {
            source.mark(readLimit);
        }

        @Override
        public void reset() throws IOException {
            source.reset();
        }

        @Override
        public boolean markSupported() {
            return source.markSupported();
        }
    }
}

Чтобы повторно использовать его, просто сначала закройте его, если это не так.

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

1 голос
/ 18 декабря 2010
public static void main(String[] args) throws IOException {
    BufferedInputStream inputStream = new BufferedInputStream(IOUtils.toInputStream("Foobar"));
    inputStream.mark(Integer.MAX_VALUE);
    System.out.println(IOUtils.toString(inputStream));
    inputStream.reset();
    System.out.println(IOUtils.toString(inputStream));
}

Это работает. IOUtils является частью общего ввода-вывода.

1 голос
/ 29 мая 2009

Это, как я бы реализовал, для безопасного использования с любым InputStream:

  • написать свою собственную оболочку InputStream, в которой вы создадите временный файл для зеркального отображения исходного содержимого потока
  • сбросить все данные из исходного потока ввода во временный файл
  • когда поток будет полностью прочитан, все данные будут отражены во временном файле
  • используйте InputStream.reset для переключения (инициализации) внутреннего потока в FileInputStream (mirrored_content_file)
  • отныне вы потеряете ссылку на исходный поток (можно собрать)
  • добавьте новый метод release (), который удалит временный файл и освободит любой открытый поток.
  • вы даже можете вызвать release () из finalize , чтобы убедиться, что временный файл выпущен, если вы забудете вызвать release () (большую часть времени вам следует избегать использования finalize , всегда вызывайте метод для освобождения ресурсов объекта). см. Зачем вам реализовывать finalize ()?
1 голос
/ 29 мая 2009

Что именно вы имеете в виду под "кешем"? Хотите, чтобы другая POIFSFileSystem запускалась в начале потока? Если это так, нет абсолютно никакого смысла кэшировать что-либо в вашем коде Java; это будет сделано ОС, просто откройте новый поток.

Или вы хотите продолжить чтение с того места, где остановилась первая POIFSFileSystem? Это не кеширование, и это очень сложно сделать. Единственный способ, которым я могу думать, если вы не можете избежать закрытия потока, это написать тонкую оболочку, которая считает количество прочитанных байтов, а затем открыть новый поток и пропустить столько байтов. Но это может привести к сбою, когда POIFSFileSystem внутренне использует что-то вроде BufferedInputStream.

1 голос
/ 29 мая 2009

Если файл не такой большой, считайте его в массив byte[] и присвойте POI ByteArrayInputStream, созданный из этого массива.

Если файл большой, то вам все равно, так как ОС сделает кэширование за вас так хорошо, как только сможет.

[EDIT] Используйте Apache commons-io для эффективного чтения файла в байтовый массив. Не используйте int read(), поскольку он читает файл побайтно, что очень медленно!

Если вы хотите сделать это самостоятельно, используйте объект File, чтобы получить длину, создайте массив и цикл, который считывает байты из файла. Вы должны выполнить цикл, поскольку read(byte[], int offset, int len) может читать меньше len байтов (и обычно делает).

0 голосов
/ 17 июня 2013

Я просто добавляю свое решение здесь, так как это работает для меня. По сути, это сочетание двух лучших ответов:)

    private String convertStreamToString(InputStream is) {
    Writer w = new StringWriter();
    char[] buf = new char[1024];
    Reader r;
    is.mark(1 << 24);
    try {
        r = new BufferedReader(new InputStreamReader(is, "UTF-8"));
        int n;
        while ((n=r.read(buf)) != -1) {
            w.write(buf, 0, n);
        }
        is.reset();
    } catch(UnsupportedEncodingException e) {
        Logger.debug(this.getClass(), "Cannot convert stream to string.", e);
    } catch(IOException e) {
        Logger.debug(this.getClass(), "Cannot convert stream to string.", e);
    }
    return w.toString();
}
...