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

StackOverflow https://stackoverflow.com/questions/924990

Вопрос

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

POIFSFileSystem fileSystem = new POIFSFileSystem(inputStream);

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

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

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

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

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

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

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

Это было полезно?

Решение

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

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

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

Другие советы

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

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.

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

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

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

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

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

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

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

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

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

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

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

  • напишите свою собственную оболочку InputStream, в которой вы создадите временный файл для отражения исходного содержимого потока.
  • сбрасывать все, прочитанное из исходного входного потока, в этот временный файл
  • когда поток будет полностью прочитан, все данные будут отражены во временном файле.
  • используйте InputStream.reset для переключения (инициализации) внутреннего потока на FileInputStream(mirrored_content_file)
  • с этого момента вы потеряете ссылку на исходный поток (можно собрать)
  • добавьте новый метод Release(), который удалит временный файл и освободит любой открытый поток.
  • вы даже можете вызвать Release() из завершить чтобы быть уверенным, что временный файл освобожден, на случай, если вы забудете вызвать Release() (в большинстве случаев вам следует избегать использования завершить, всегда вызывайте метод для освобождения ресурсов объекта).видеть Зачем вам вообще реализовывать метод Finalize()?
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 является частью Commons IO.

Этот ответ повторяет предыдущие 1|2 на основе BufferInputStream.Основные изменения заключаются в том, что он допускает бесконечное повторное использование.И заботится о закрытии исходного входного потока, чтобы освободить системные ресурсы.Ваша ОС определяет ограничение на них, и вы не хотите, чтобы в программе закончились дескрипторы файлов (Вот почему вы всегда должны «потреблять» ответы, например:с апачем EntityUtils.consumeQuietly()). РЕДАКТИРОВАТЬ Обновлен код для обработки готовых потребителей, использующих 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();
        }
    }
}

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

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

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

    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();
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top