web-dev-qa-db-fra.com

Comment mettre en cache InputStream pour plusieurs utilisations

J'ai un InputStream d'un fichier et j'utilise des composants Apache poi pour le lire comme ceci:

POIFSFileSystem fileSystem = new POIFSFileSystem(inputStream);

Le problème est que je dois utiliser le même flux plusieurs fois et le POIFSFileSystem ferme le flux après utilisation.

Quel est le meilleur moyen de mettre en cache les données du flux d'entrée, puis de servir davantage de flux d'entrée à différents POIFSFileSystem?

EDIT 1:

Par cache, je voulais dire stocker pour une utilisation ultérieure, pas comme un moyen d'accélérer l'application. Est-il également préférable de simplement lire le flux d'entrée dans un tableau ou une chaîne, puis de créer des flux d'entrée pour chaque utilisation?

EDIT 2:

Désolé de rouvrir la question, mais les conditions sont quelque peu différentes lorsque vous travaillez dans une application de bureau ou Web. Tout d’abord, InputStream i provient de org.Apache.commons.fileupload.FileItem dans mon application Web Tomcat ne prend pas en charge les marquages ​​et ne peut donc pas être réinitialisé. 

Deuxièmement, j'aimerais pouvoir garder le fichier en mémoire pour un accès plus rapide et moins de problèmes lors de l'utilisation de fichiers.

23
Azder

vous pouvez décorer InputStream transmis à POIFSFileSystem avec une version qui, lorsqu'elle est appelée, s'appelle, répond avec 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();
    }
}

cas de test

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

EDIT 2

vous pouvez lire le fichier entier dans un octet [] (mode Slurp) puis le transmettre à un ByteArrayInputStream

17
dfa

Essayez BufferedInputStream, qui ajoute des fonctionnalités de marquage et de réinitialisation à un autre flux d'entrée et substituez simplement sa méthode de fermeture:

public class UnclosableBufferedInputStream extends BufferedInputStream {

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

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

Alors:

UnclosableBufferedInputStream  bis = new UnclosableBufferedInputStream (inputStream);

et utilisez bis partout où inputStream était utilisé auparavant.

20
Tomasz

Cela fonctionne correctement:

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

où getBytes est comme ceci:

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

Utilisez ci-dessous l'implémentation pour une utilisation plus personnalisée - 

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

Si le fichier n'est pas si gros, lisez-le dans un tableau byte[] et donnez à POI une ByteArrayInputStream créée à partir de ce tableau.

Si le fichier est volumineux, vous ne devriez pas vous en soucier, car le système d'exploitation fera la mise en cache pour vous le mieux possible.

[EDIT] Utilisez Apache commons-io pour lire le fichier dans un tableau d'octets de manière efficace. N'utilisez pas int read() car il lit le fichier octet par octet, ce qui correspond à very slow!

Si vous voulez le faire vous-même, utilisez un objet File pour obtenir la longueur, créez le tableau et la boucle qui lit les octets du fichier. Vous devez effectuer une boucle puisque read(byte[], int offset, int len) peut lire moins de len octets (et le fait généralement).

1
Aaron Digulla

Que voulez-vous dire exactement par "cache"? Voulez-vous que les différents POIFSFileSystem commencent au début du flux? Si tel est le cas, il est absolument inutile de mettre en cache quoi que ce soit dans votre code Java. cela sera fait par le système d'exploitation, ouvrez simplement un nouveau flux.

Ou voulez-vous continuer à lire au point où le premier POIFSFileSystem s'est arrêté? Ce n'est pas la mise en cache, et c'est très difficile à faire. Le seul moyen auquel je peux penser si vous ne pouvez pas éviter la fermeture du flux consiste à écrire un wrapper fin qui compte le nombre d'octets lus, puis à ouvrir un nouveau flux et à sauter autant d'octets. Mais cela pourrait échouer si POIFSFileSystem utilise en interne quelque chose comme un BufferedInputStream.

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

Cela marche. IOUtils fait partie de commons IO.

1
Daniel Kaplan

Voici comment j'ai implémenté, pour être utilisé en toute sécurité avec n'importe quel InputStream: 

  • écrivez votre propre wrapper InputStream dans lequel vous créez un fichier temporaire pour refléter le contenu du flux d'origine
  • dump tout ce qui est lu depuis le flux d'entrée original dans ce fichier temporaire
  • lorsque le flux a été complètement lu, vous aurez toutes les données en miroir dans le fichier temporaire
  • utilisez InputStream.reset pour basculer (initialiser) le flux interne vers un FileInputStream (mirrored_content_file)
  • à partir de maintenant, vous perdrez la référence du flux original (peut être collectée)
  • ajoutez une nouvelle méthode release () qui supprimera le fichier temporaire et libérera tout flux ouvert.
  • vous pouvez même appeler release () depuis finalize pour vous assurer que le fichier temporaire est relâché au cas où vous oublieriez d'appeler release () (la plupart du temps, vous devriez éviter d'utiliser finalize , toujours appeler une méthode pour libérer des ressources d'objet). voir Pourquoi voudriez-vous jamais implémenter finalize ()?
1
adrian.tarau

Cette réponse itère sur les précédentes 1 | 2 basé sur la BufferInputStream. Les principaux changements sont que cela permet une réutilisation infinie. Et s'occupe de fermer le flux d'entrée source d'origine pour libérer des ressources système. Votre système d'exploitation définit une limite sur ceux-ci et vous ne voulez pas que le programme manque de descripteurs de fichiers (C'est aussi pourquoi vous devriez toujours «consommer» des réponses, par exemple avec Apache EntityUtils.consumeQuietly()).EDITMise à jour du code à manipuler pour les gros consommateurs qui utilisent read(buffer, offset, length). Dans ce cas, il peut arriver que BufferedInputStream essaie de rechercher le code source, ce code protège contre cette utilisation.

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

Pour le réutiliser, fermez-le d'abord s'il ne l'était pas. 

Une limitation cependant est que si le flux est fermé avant que tout le contenu du flux original ait été lu, ce décorateur aura alors des données incomplètes. Assurez-vous donc que tout le flux est lu avant la fermeture.

1
Brice

Je viens d'ajouter ma solution ici, car cela fonctionne pour moi. C'est fondamentalement une combinaison des deux premières réponses :)

    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