it-roy-ru.com

Как кэшировать InputStream для многократного использования

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

POIFSFileSystem fileSystem = new POIFSFileSystem(inputStream);

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

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

Правка 1:

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

Правка 2:

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

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

23
Azder

вы можете украсить 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();
    }
}

прецедент

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

17
dfa

Попробуйте 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 использовался ранее.

20
Tomasz

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

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();
 }
4
Kaba Aboubacar

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

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();
        }
    }
}
2
user2807207

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

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

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

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

1
Aaron Digulla

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

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

1
Michael Borgwardt
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
Daniel Kaplan

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

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

Этот ответ повторяет предыдущие 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
Brice

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

    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();
}
0
FuePi