Como clonar um InputStream?

162

Eu tenho um InputStream que passo para um método para fazer algum processamento. Usarei o mesmo InputStream em outro método, mas após o primeiro processamento, o InputStream parece ser fechado dentro do método.

Como posso clonar o InputStream para enviar para o método que o fecha? Existe outra solução?

EDIT: o método que fecha o InputStream é um método externo de uma lib. Eu não tenho controle sobre o fechamento ou não.

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
fonte
2
Deseja "redefinir" o fluxo após o retorno do método? Ou seja, leia o fluxo desde o início?
Aioobe 7/11
Sim, os métodos que fecham o InputStream retornam o conjunto de caracteres que foi codificado. O segundo método é converter o InputStream em uma String usando o conjunto de caracteres encontrado no primeiro método.
Renato Dinhani 7/11/11
Nesse caso, você deve poder fazer o que estou descrevendo na minha resposta.
Kaj
Não sei a melhor maneira de resolvê-lo, mas caso contrário, resolvo o meu problema. O método toString do Jericho HTML Parser retorna a String formatada no formato correto. É tudo o que preciso no momento.
Renato Dinhani

Respostas:

188

Se tudo o que você deseja fazer é ler as mesmas informações mais de uma vez e os dados de entrada forem pequenos o suficiente para caber na memória, você poderá copiar os dados InputStreampara um ByteArrayOutputStream .

Em seguida, você pode obter a matriz de bytes associada e abrir quantos ByteArrayInputStream s "clonados" desejar.

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

Mas se você realmente precisar manter o fluxo original aberto para receber novos dados, precisará rastrear esse close()método externo e impedir que seja chamado de alguma forma.

ATUALIZAÇÃO (2019):

Desde o Java 9, os bits do meio podem ser substituídos por InputStream.transferTo:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
input.transferTo(baos);
InputStream firstClone = new ByteArrayInputStream(baos.toByteArray()); 
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray()); 
Anthony Accioly
fonte
Eu encontro outra solução para o meu problema, que não envolve copiar o InputStream, mas acho que se precisar copiar o InputStream, essa é a melhor solução.
Renato Dinhani
7
Essa abordagem consome memória proporcional ao conteúdo completo do fluxo de entrada. Melhor usar TeeInputStreamcomo descrito na resposta aqui .
precisa saber é
2
O IOUtils (do apache commons) possui um método de cópia que faria o buffer de leitura / gravação no meio do seu código.
rethab
31

Você quer usar o Apache CloseShieldInputStream :

Este é um invólucro que impedirá que o fluxo seja fechado. Você faria algo assim.

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
fonte
Parece bom, mas não funciona aqui. Vou editar minha postagem com o código.
Renato Dinhani 7/11
CloseShieldnão está funcionando porque seu HttpURLConnectionfluxo de entrada original está sendo fechado em algum lugar. Seu método não deveria chamar IOUtils com o fluxo protegido IOUtils.toString(csContent,charset)?
Anthony Accioly
Talvez possa ser isso. Posso impedir que a HttpURLConnection seja fechada?
Renato Dinhani 7/11
1
@Renato. Talvez o problema não seja a close()ligação, mas o fato de o Stream estar sendo lido até o fim. Como mark()e reset()talvez não sejam os melhores métodos para conexões http, talvez você deva dar uma olhada na abordagem de matriz de bytes descrita em minha resposta.
Anthony Accioly
1
Mais uma coisa, você sempre pode abrir uma nova conexão com a mesma URL. Veja aqui: stackoverflow.com/questions/5807340/…
Anthony Accioly
11

Você não pode cloná-lo, e como você resolverá seu problema depende de qual é a fonte dos dados.

Uma solução é ler todos os dados do InputStream em uma matriz de bytes e criar um ByteArrayInputStream em torno dessa matriz de bytes e passar esse fluxo de entrada para o seu método.

Edit 1: Ou seja, se o outro método também precisar ler os mesmos dados. Ou seja, você deseja "redefinir" o fluxo.

Kaj
fonte
Não sei em que parte você precisa de ajuda. Eu acho que você sabe ler de um fluxo? Leia todos os dados do InputStream e grave-os no ByteArrayOutputStream. Chame toByteArray () no ByteArrayOutputStream depois de concluir a leitura de todos os dados. Em seguida, passe essa matriz de bytes para o construtor de um ByteArrayInputStream.
Kaj
8

Se os dados lidos no fluxo forem grandes, eu recomendaria o uso de um TeeInputStream do Apache Commons IO. Dessa forma, você pode essencialmente replicar a entrada e passar um canal t'd como seu clone.

Nathan Ryan
fonte
5

Isso pode não funcionar em todas as situações, mas aqui está o que eu fiz: Estendi a classe FilterInputStream e faço o processamento necessário dos bytes quando a lib externa lê os dados.

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
    }

}

Então você simplesmente passa uma instância de StreamBytesWithExtraProcessingInputStreamonde você teria passado no fluxo de entrada. Com o fluxo de entrada original como parâmetro do construtor.

Deve-se notar que isso funciona byte por byte, portanto, não o use se um alto desempenho for um requisito.

Diederik
fonte
3

UPD. Verifique o comentário antes. Não é exatamente o que foi pedido.

Se você estiver usando, apache.commonspoderá copiar fluxos usando IOUtils.

Você pode usar o seguinte código:

InputStream = IOUtils.toBufferedInputStream(toCopy);

Aqui está o exemplo completo adequado à sua situação:

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

Este código requer algumas dependências:

MAVEN

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

GRADLE

'commons-io:commons-io:2.4'

Aqui está a referência DOC para este método:

Busca todo o conteúdo de um InputStream e representa os mesmos dados que o resultado InputStream. Este método é útil onde,

O InputStream de origem está lento. Como possui recursos de rede associados, não podemos mantê-lo aberto por muito tempo. Tem tempo limite de rede associado.

Você pode encontrar mais informações IOUtilsaqui: http://commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/IOUtils.html#toBufferedInputStream(java.io.InputStream)

Andrey E
fonte
7
Isso não clona o fluxo de entrada, mas apenas o armazena em buffer. Isso não é o mesmo; o OP deseja reler (uma cópia) do mesmo fluxo.
Raphael
1

Abaixo está a solução com Kotlin.

Você pode copiar seu InputStream no ByteArray

val inputStream = ...

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

val byteInputStream = ByteArrayInputStream(byteOutputStream.toByteArray())

Se você precisar ler byteInputStreamvárias vezes, ligue parabyteInputStream.reset() antes de ler novamente.

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

Desmond Lua
fonte
0

A turma abaixo deve fazer o truque. Apenas crie uma instância, chame o método "multiplicar" e forneça o fluxo de entrada de origem e a quantidade de duplicatas necessária.

Importante: você deve consumir todos os fluxos clonados simultaneamente em threads separados.

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();
                }
            }
        }
    }
}
codificador vstrom
fonte
Não responde a pergunta. Ele deseja usar o fluxo em um método para determinar o conjunto de caracteres e, em seguida, relê-lo junto com o conjunto de caracteres em um segundo método.
Marquês de Lorne
0

A clonagem de um fluxo de entrada pode não ser uma boa ideia, pois isso requer conhecimento profundo sobre os detalhes do fluxo de entrada que está sendo clonado. Uma solução alternativa para isso é criar um novo fluxo de entrada que leia novamente a mesma fonte.

Portanto, usando alguns recursos do Java 8, seria assim:

public class Foo {

    private Supplier<InputStream> inputStreamSupplier;

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

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

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

Este método tem o efeito positivo de reutilizar o código que já está em vigor - a criação do fluxo de entrada encapsulado em inputStreamSupplier. E não há necessidade de manter um segundo caminho de código para a clonagem do fluxo.

Por outro lado, se a leitura do fluxo for cara (porque é feita em uma conexão de baixa largura de banda), esse método dobrará os custos. Isso pode ser contornado usando um fornecedor específico que armazenará o conteúdo do fluxo localmente primeiro e fornecerá um InputStreamrecurso local agora.

SpaceTrucker
fonte
Esta resposta não está clara para mim. Como você inicializa o fornecedor a partir de um existente is?
usar o seguinte comando
@ user1156544 Como escrevi Clonando, um fluxo de entrada pode não ser uma boa idéia, pois isso requer conhecimento profundo sobre os detalhes do fluxo de entrada que está sendo clonado. você não pode usar o fornecedor para criar um fluxo de entrada de um existente. O fornecedor pode usar um java.io.Fileou java.net.URLpor exemplo para criar um novo fluxo de entrada cada vez que é chamado.
SpaceTrucker
Eu vejo agora. Isso não funcionará com o inputstream, conforme solicitado pelo OP, mas com o File ou URL, se eles forem a fonte original dos dados. Graças
user1156544