Comment cloner un InputStream?

162

J'ai un InputStream que je passe à une méthode pour faire du traitement. J'utiliserai le même InputStream dans une autre méthode, mais après le premier traitement, l'InputStream semble être fermé à l'intérieur de la méthode.

Comment puis-je cloner le InputStream pour l'envoyer à la méthode qui le ferme? Il y a une autre solution?

EDIT: la méthode qui ferme l'InputStream est une méthode externe à partir d'une bibliothèque. Je n'ai pas de contrôle sur la fermeture ou non.

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";
    }
}
Renato Dinhani
la source
2
Voulez-vous "réinitialiser" le flux après le retour de la méthode? Ie, lire le flux depuis le début?
aioobe
Oui, les méthodes qui ferme le InputStream renvoie le jeu de caractères dans lequel il a été encodé. La deuxième méthode consiste à convertir le InputStream en chaîne à l'aide du jeu de caractères trouvé dans la première méthode.
Renato Dinhani
Vous devriez dans ce cas être capable de faire ce que je décris dans ma réponse.
Kaj
Je ne connais pas la meilleure façon de le résoudre, mais je résous mon problème autrement. La méthode toString de Jericho HTML Parser renvoie la chaîne au format correct. C'est tout ce dont j'ai besoin pour le moment.
Renato Dinhani

Réponses:

188

Si tout ce que vous voulez faire est de lire les mêmes informations plus d'une fois et que les données d'entrée sont suffisamment petites pour tenir en mémoire, vous pouvez copier les données de votre InputStreamvers un ByteArrayOutputStream .

Ensuite, vous pouvez obtenir le tableau d'octets associé et ouvrir autant de ByteArrayInputStream "clonés" que vous le souhaitez.

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

Mais si vous avez vraiment besoin de garder le flux d'origine ouvert pour recevoir de nouvelles données, vous devrez suivre cette close()méthode externe et l'empêcher d'être appelée d'une manière ou d'une autre.

MISE À JOUR (2019):

Depuis Java 9, les bits du milieu peuvent être remplacés par InputStream.transferTo:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
input.transferTo(baos);
InputStream firstClone = new ByteArrayInputStream(baos.toByteArray()); 
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray()); 
Anthony Accioly
la source
Je trouve une autre solution à mon problème qui n'implique pas de copier l'InputStream, mais je pense que si j'ai besoin de copier l'InputStream, c'est la meilleure solution.
Renato Dinhani
7
Cette approche consomme de la mémoire proportionnellement au contenu complet du flux d'entrée. Mieux vaut utiliser TeeInputStreamcomme décrit dans la réponse ici .
aioobe
2
IOUtils (d'Apache commons) a une méthode de copie qui ferait la lecture / écriture du tampon au milieu de votre code.
rethab
31

Vous souhaitez utiliser Apache CloseShieldInputStream:

Il s'agit d'un wrapper qui empêchera la fermeture du flux. Vous feriez quelque chose comme ça.

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();
Femi
la source
Ça a l'air bien, mais ça ne marche pas ici. Je vais éditer mon message avec le code.
Renato Dinhani
CloseShieldne fonctionne pas car votre HttpURLConnectionflux d'entrée d' origine est fermé quelque part. Votre méthode ne devrait-elle pas appeler IOUtils avec le flux protégé IOUtils.toString(csContent,charset)?
Anthony Accioly
Peut-être que ça peut être ça. Puis-je empêcher la fermeture de HttpURLConnection?
Renato Dinhani
1
@Renato. Peut-être que le problème n'est pas du tout l' close()appel, mais le fait que le Stream est lu jusqu'à la fin. Puisque mark()et reset()ne sont peut-être pas les meilleures méthodes pour les connexions http, vous devriez peut-être jeter un coup d'œil à l'approche du tableau d'octets décrite dans ma réponse.
Anthony Accioly
1
Une dernière chose, vous pouvez toujours ouvrir une nouvelle connexion vers la même URL. Voir ici: stackoverflow.com/questions/5807340/…
Anthony Accioly
11

Vous ne pouvez pas le cloner et la manière dont vous allez résoudre votre problème dépend de la source des données.

Une solution consiste à lire toutes les données de InputStream dans un tableau d'octets, puis à créer un ByteArrayInputStream autour de ce tableau d'octets et à transmettre ce flux d'entrée à votre méthode.

Edit 1: Autrement dit, si l'autre méthode doit également lire les mêmes données. Ie vous voulez "réinitialiser" le flux.

Kaj
la source
Je ne sais pas pour quelle partie vous avez besoin d'aide. Je suppose que vous savez lire à partir d'un flux? Lisez toutes les données de InputStream et écrivez les données dans ByteArrayOutputStream. Appelez toByteArray () sur ByteArrayOutputStream après avoir terminé la lecture de toutes les données. Passez ensuite ce tableau d'octets au constructeur d'un ByteArrayInputStream.
Kaj
8

Si les données lues à partir du flux sont volumineuses, je recommanderais d'utiliser un TeeInputStream d'Apache Commons IO. De cette façon, vous pouvez essentiellement répliquer l'entrée et passer un tube t'd comme votre clone.

Nathan Ryan
la source
5

Cela peut ne pas fonctionner dans toutes les situations, mais voici ce que j'ai fait: j'ai étendu la classe FilterInputStream et effectué le traitement requis des octets lorsque la bibliothèque externe lit les données.

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
    }

}

Ensuite, vous passez simplement une instance de l' StreamBytesWithExtraProcessingInputStreamendroit où vous auriez passé dans le flux d'entrée. Avec le flux d'entrée d'origine comme paramètre de constructeur.

Il convient de noter que cela fonctionne octet pour octet, donc ne l'utilisez pas si des performances élevées sont requises.

Diederik
la source
3

UPD. Vérifiez le commentaire avant. Ce n'est pas exactement ce qui a été demandé.

Si vous utilisez, apache.commonsvous pouvez copier des flux en utilisant IOUtils.

Vous pouvez utiliser le code suivant:

InputStream = IOUtils.toBufferedInputStream(toCopy);

Voici l'exemple complet adapté à votre situation:

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

Ce code nécessite certaines dépendances:

MAVEN

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

GRADLE

'commons-io:commons-io:2.4'

Voici la référence DOC pour cette méthode:

Récupère tout le contenu d'un InputStream et représente les mêmes données que le résultat InputStream. Cette méthode est utile lorsque,

Source InputStream est lent. Il a des ressources réseau associées, nous ne pouvons donc pas le garder ouvert pendant longtemps. Il a un délai d'expiration du réseau associé.

Vous pouvez en savoir plus IOUtilsici: http://commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/IOUtils.html#toBufferedInputStream(java.io.InputStream)

Andrey E
la source
7
Cela ne clone pas le flux d'entrée mais le met uniquement en mémoire tampon. Ce n'est pas la même chose; l'OP veut relire (une copie de) le même flux.
Raphael
1

Voici la solution avec Kotlin.

Vous pouvez copier votre InputStream dans ByteArray

val inputStream = ...

val byteOutputStream = ByteArrayOutputStream()
inputStream.use { input ->
    byteOutputStream.use { output ->
        input.copyTo(output)
    }
}

val byteInputStream = ByteArrayInputStream(byteOutputStream.toByteArray())

Si vous avez besoin de lire byteInputStreamplusieurs fois, appelez byteInputStream.reset()avant de relire.

https://code.luasoftware.com/tutorials/kotlin/how-to-clone-inputstream/

Desmond Lua
la source
0

La classe ci-dessous devrait faire l'affaire. Créez simplement une instance, appelez la méthode "multiplier" et fournissez le flux d'entrée source et la quantité de doublons dont vous avez besoin.

Important: vous devez consommer tous les flux clonés simultanément dans des threads séparés.

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();
                }
            }
        }
    }
}
codeur vstrom
la source
Ne répond pas à la question. Il souhaite utiliser le flux dans une méthode pour déterminer le jeu de caractères, puis le relire avec son jeu de caractères dans une deuxième méthode.
Marquis of Lorne
0

Le clonage d'un flux d'entrée peut ne pas être une bonne idée, car cela nécessite une connaissance approfondie des détails du flux d'entrée en cours de clonage. Une solution de contournement pour cela consiste à créer un nouveau flux d'entrée qui lit à nouveau à partir de la même source.

Donc, en utilisant certaines fonctionnalités de Java 8, cela ressemblerait à ceci:

public class Foo {

    private Supplier<InputStream> inputStreamSupplier;

    public void bar() {
        procesDataThisWay(inputStreamSupplier.get());
        procesDataTheOtherWay(inputStreamSupplier.get());
    }

    private void procesDataThisWay(InputStream) {
        // ...
    }

    private void procesDataTheOtherWay(InputStream) {
        // ...
    }
}

Cette méthode a pour effet positif de réutiliser le code déjà en place - la création du flux d'entrée encapsulé dans inputStreamSupplier. Et il n'est pas nécessaire de maintenir un deuxième chemin de code pour le clonage du flux.

D'un autre côté, si la lecture à partir du flux coûte cher (parce que c'est fait sur une connexion à faible bande passante), alors cette méthode doublera les coûts. Cela pourrait être contourné en utilisant un fournisseur spécifique qui stockera d'abord le contenu du flux localement et fournira une InputStreamressource pour cette ressource maintenant locale.

SpaceTrucker
la source
Cette réponse n'est pas claire pour moi. Comment initialiser le fournisseur à partir d'un existant is?
user1156544
@ user1156544 Comme je l'ai écrit, le clonage d'un flux d'entrée n'est peut-être pas une bonne idée, car cela nécessite une connaissance approfondie des détails du flux d'entrée cloné. vous ne pouvez pas utiliser le fournisseur pour créer un flux d'entrée à partir d'un flux existant. Le fournisseur peut utiliser un java.io.Fileou java.net.URLpar exemple pour créer un nouveau flux d'entrée à chaque fois qu'il est appelé.
SpaceTrucker
Je vois maintenant. Cela ne fonctionnera pas avec le flux d'entrée comme l'OP le demande explicitement, mais avec Fichier ou URL s'ils sont la source d'origine des données. Merci
user1156544