27

I have an InputStream that I want written to a HttpServletResponse. There's this approach, which takes too long due to the use of byte[]

InputStream is = getInputStream();
int contentLength = getContentLength();

byte[] data = new byte[contentLength];
is.read(data);

//response here is the HttpServletResponse object
response.setContentLength(contentLength);
response.write(data);

I was wondering what could possibly be the best way to do it, in terms of speed and efficiency.

eugenevd
  • 810
  • 7
  • 21
Sabry Shawally
  • 603
  • 2
  • 11
  • 24

3 Answers3

53

Just write in blocks instead of copying it entirely into Java's memory first. The below basic example writes it in blocks of 10KB. This way you end up with a consistent memory usage of only 10KB instead of the complete content length. Also the enduser will start getting parts of the content much sooner.

response.setContentLength(getContentLength());
byte[] buffer = new byte[10240];

try (
    InputStream input = getInputStream();
    OutputStream output = response.getOutputStream();
) {
    for (int length = 0; (length = input.read(buffer)) > 0;) {
        output.write(buffer, 0, length);
    }
}

As creme de la creme with regard to performance, you could use NIO Channels and a directly allocated ByteBuffer. Create the following utility/helper method in some custom utility class, e.g. Utils:

public static long stream(InputStream input, OutputStream output) throws IOException {
    try (
        ReadableByteChannel inputChannel = Channels.newChannel(input);
        WritableByteChannel outputChannel = Channels.newChannel(output);
    ) {
        ByteBuffer buffer = ByteBuffer.allocateDirect(10240);
        long size = 0;

        while (inputChannel.read(buffer) != -1) {
            buffer.flip();
            size += outputChannel.write(buffer);
            buffer.clear();
        }

        return size;
    }
}

Which you then use as below:

response.setContentLength(getContentLength());
Utils.stream(getInputStream(), response.getOutputStream());
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • 2
    Of course many utility packages have this method already defined, so once you start using Guava... http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/io/ByteStreams.html#copy(java.io.InputStream, java.io.OutputStream) – Michael Brewer-Davis Apr 13 '12 at 15:27
  • +1 @BalusC Do we need to set its ContentType? If yes, what would it be? – Roy Lee Mar 19 '13 at 06:03
  • 1
    @Roylee: yes, that's recommended. Just set the content type to the type of the content :) http://www.freeformatter.com/mime-types-list.html – BalusC Mar 19 '13 at 11:03
  • i tried this,, why is that my stream will return an error like this: org.apache.catalina.connector.ClientAbortException: java.io.IOException: An established connection was aborted by the software in your host machine at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:356) at org.apache.catalina.connector.OutputBuffer.appendByteArray(OutputBuffer.java:778) at org.apache.catalina.connector.OutputBuffer.append(OutputBuffer.java:707) at org.apache.catalina.connector.OutputBuffer.writeBytes(OutputBuffer.java:391) – vvavepacket May 14 '17 at 07:05
  • how to getContentLenght() of input Stream? – Pramod Gaikwad Nov 30 '17 at 11:23
1
BufferedInputStream in = null;
BufferedOutputStream out = null;
OutputStream os;
os = new BufferedOutputStream(response.getOutputStream());
in = new BufferedInputStream(new FileInputStream(file));
out = new BufferedOutputStream(os);
byte[] buffer = new byte[1024 * 8];
int j = -1;
while ((j = in.read(buffer)) != -1) {
    out.write(buffer, 0, j);
}
Li Shen
  • 293
  • 1
  • 2
  • 6
0

I think that is very close to the best way, but I would suggest the following change. Use a fixed size buffer(Say 20K) and then do the read/write in a loop.

For the loop do something like

byte[] buffer=new byte[20*1024];
outputStream=response.getOutputStream();
while(true) {
  int readSize=is.read(buffer);
  if(readSize==-1)
    break;
  outputStream.write(buffer,0,readSize);
}

ps: Your program will not always work as is, because read don't always fill up the entire array you give it.

Luatic
  • 8,513
  • 2
  • 13
  • 34
MTilsted
  • 5,425
  • 9
  • 44
  • 76
  • what exactly do you mean by read doesn't fill up the entire array you give it? – Sabry Shawally Apr 13 '12 at 14:33
  • 1
    Read does not always fill up the input array. So you need to check the value which read return which is the number of bytes read. (See http://docs.oracle.com/javase/6/docs/api/java/io/InputStream.html#read%28byte[]%29) – MTilsted Apr 13 '12 at 15:30