44

I am using URL class to read an InputStream from it. Is there any way I can use RestTemplate for this?

InputStream input = new URL(url).openStream();
JsonReader reader = new JsonReader(new InputStreamReader(input, StandardCharsets.UTF_8.displayName())); 

How can I get InputStream with RestTemplate instead of using URL?

user1950349
  • 4,738
  • 19
  • 67
  • 119

9 Answers9

58

The previous answers are not wrong, but they don't go into the depth that I like to see. There are cases when dealing with low level InputStream is not only desirable, but necessary, the most common example being streaming a large file from source (some web server) to destination (a database). If you try to use a ByteArrayInputStream, you will be, not so surprisingly, greeted with OutOfMemoryError. Yes, you can roll your own HTTP client code, but you'll have to deal with erroneous response codes, response converters etc. If you are already using Spring, looking to RestTemplate is a natural choice.

As of this writing, spring-web:5.0.2.RELEASE has a ResourceHttpMessageConverter that has a boolean supportsReadStreaming, which if set, and the response type is InputStreamResource, returns InputStreamResource; otherwise it returns a ByteArrayResource. So clearly, you're not the only one that asked for streaming support.

However, there is a problem: RestTemplate closes the response soon after the HttpMessageConverter runs. Thus, even if you asked for InputStreamResource, and got it, it's no good, because the response stream has been closed. I think this is a design flaw that they overlooked; it should've been dependent on the response type. So unfortunately, for reading, you must consume the response fully; you can't pass it around if using RestTemplate.

Writing is no problem though. If you want to stream an InputStream, ResourceHttpMessageConverter will do it for you. Under the hood, it uses org.springframework.util.StreamUtils to write 4096 bytes at a time from the InputStream to the OutputStream.

Some of the HttpMessageConverter support all media types, so depending on your requirement, you may have to remove the default ones from RestTemplate, and set the ones you need, being mindful of their relative ordering.

Last but not the least, implementations of ClientHttpRequestFactory has a boolean bufferRequestBody that you can, and should, set to false if you are uploading a large stream. Otherwise, you know, OutOfMemoryError. As of this writing, SimpleClientHttpRequestFactory (JDK client) and HttpComponentsClientHttpRequestFactory (Apache HTTP client) support this feature, but not OkHttp3ClientHttpRequestFactory. Again, design oversight.

Edit: Filed ticket SPR-16885.

Abhijit Sarkar
  • 21,927
  • 20
  • 110
  • 219
  • 3
    @Kieveli I’m not sure what you mean by no example. I provided references to practical use cases like streaming. Spoon feeding code isn’t the goal of this answer. – Abhijit Sarkar May 08 '18 at 18:13
  • Not sure it's a design flaw. I think it's just the result of the design decision to have RestTemplate manage the resources within the call. I hit this same issue when I tried to use an InputStream I retrieved in JdbcTemplate. It had closed the result set and thus the input stream to the CLOB field. But great explanation. – David Bradley Aug 28 '19 at 18:07
  • `bufferRequestBody` should be considered only when sending file via post or put. If we talk about reading InputStream from http response, it can be ignored – valijon Jun 28 '20 at 05:39
48

Spring has a org.springframework.http.converter.ResourceHttpMessageConverter. It converts Spring's org.springframework.core.io.Resource class. That Resource class encapsulates a InputStream, which you can obtain via someResource.getInputStream().

Putting this all together, you can actually get an InputStream via RestTemplate out-of-the-box by specifying Resource.class as your RestTemplate invocation's response type.

Here is an example using one of RestTemplate's exchange(..) methods:

import org.springframework.web.client.RestTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.core.io.Resource;

ResponseEntity<Resource> responseEntity = restTemplate.exchange( someUrlString, HttpMethod.GET, someHttpEntity, Resource.class );

InputStream responseInputStream;
try {
    responseInputStream = responseEntity.getBody().getInputStream();
}
catch (IOException e) {
    throw new RuntimeException(e);
}

// use responseInputStream
Abdull
  • 26,371
  • 26
  • 130
  • 172
  • 4
    `responseEntity.getBody().getInputStream();` is incorrect. there is no getInputStream method. – brain storm Sep 21 '17 at 17:18
  • 1
    @brainstorm, versions older and newer than `spring-core-3.1.1.RELEASE` have a [`org.springframework.core.io.Resource`](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/core/io/Resource.html) which extends `org.springframework.core.io.InputStreamSource` which provides `getInputStream()` – Abdull Sep 22 '17 at 10:39
  • 30
    The underlying InputStream will be a be ByteArrayStream. This means the response body will be loaded into memory. – Prof Mo Apr 04 '19 at 05:30
  • 1
    It doesn't work for me. It obviously load everything into memory. The above response is therefore not correct. – Jurass Jan 11 '22 at 12:13
24

You should not get the InputStream directly. RestTemplate is meant to encapsulate processing the response (and request) content. Its strength is handling all the IO and handing you a ready-to-go Java object.

One of RestTemplate's original authors, Brian Clozel, has stated:

RestTemplate is not meant to stream the response body; its contract doesn't allow it, and it's been around for so long that changing such a basic part of its behavior cannot be done without disrupting many applications.

You'll need to register appropriate HttpMessageConverter objects. Those will have access to the response's InputStream, through an HttpInputMessage object.

As Abdull suggests, Spring does come with an HttpMessageConverter implementation for Resource which itself wraps an InputStream, ResourceHttpMessageConverter. It doesn't support all Resource types, but since you should be programming to interfaces anyway, you should just use the superinterface Resource.

The current implementation (4.3.5), will return a ByteArrayResource with the content of the response stream copied to a new ByteArrayInputStream which you can access.

You don't have to close the stream. The RestTemplate takes care of that for you. (This is unfortunate if you try to use a InputStreamResource, another type supported by the ResourceHttpMessageConverter, because it wraps the underlying response's InputStream but is closed before it can be exposed to your client code.)

Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
  • 6
    "You should not get the InputStream directly. RestTemplate is meant to encapsulate processing the response (and request) content. Its strength is handling all the IO and handing you a ready-to-go Java object." Absolutely wrong. In some cases serialized object is much larger than the heap can contain, and due to that you **must** stream its serialization rather than convert it into string or what ever serialized medium you use and only then write it out into your buffer into output stream. – Dragas Jul 16 '20 at 07:34
  • 2
    @Dragas You're not wrong about `InputStream` in general. However, `RestTemplate` was not designed to support that streaming use case. You can see that [here](https://github.com/spring-projects/spring-framework/issues/21424#issuecomment-453472592) from one of its original authors' (Brian Clozel) comments: _`RestTemplate` is not meant to stream the response body; its contract doesn't allow it, and it's been around for so long that changing such a basic part of its behavior cannot be done without disrupting many applications._ – Sotirios Delimanolis Jul 24 '20 at 13:24
8

I encountered the same issue and solved it by extending RestTemplate and closing the connection only after the stream is read.

you can see the code here: https://github.com/ItamarBenjamin/stream-rest-template

ItamarBe
  • 482
  • 1
  • 5
  • 12
  • Can you please add license to your repository on GitHub? I wanted to use it in my project, but I cannot according to this: https://opensource.stackexchange.com/questions/1720/what-can-i-assume-if-a-publicly-published-project-has-no-license – afrish Apr 06 '21 at 11:27
  • 1
    sure @afrish. as i've never done that, can you let me know what exactly do i need to add there? when i uploaded it my intention was that anybody who needs it would use it. glad to see you found it helpful. – ItamarBe Apr 07 '21 at 09:18
  • I think you can follow this https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/adding-a-license-to-a-repository and choose MIT or Apache license there – afrish Apr 07 '21 at 11:40
6

Very simple, yet efficient solution would be using ResponseExtractor. It's especially useful when you want to operate on very large InputStream and your RAM is limited.

Here is how you should be implementing it:

public void consumerInputStreamWithoutBuffering(String url, Consumer<InputStream> streamConsumer) throws IOException {

    final ResponseExtractor responseExtractor =
            (ClientHttpResponse clientHttpResponse) -> {
                streamConsumer.accept(clientHttpResponse.getBody());
                return null;
            };

    restTemplate.execute(url, HttpMethod.GET, null, responseExtractor);
}

And then, invoke the method anywhere you need:

Consumer<InputStream> doWhileDownloading = inputStream -> {
                //Use inputStream for your business logic...
};

consumerInputStreamWithoutBuffering("https://localhost.com/download", doWhileDownloading);

Please, be aware of the following common pitfall:

public InputStream getInputStreamFromResponse(String url) throws IOException {

    final ResponseExtractor<InputStream> responseExtractor =
            clientHttpResponse -> clientHttpResponse.getBody();

    return restTemplate.execute(url, HttpMethod.GET, null, responseExtractor);
}

Here InputStream will be closed before you can access it

valijon
  • 1,304
  • 2
  • 20
  • 35
5

I solve it by doing that. I hope it will help you all.

    @GetMapping("largeFile")
    public ResponseEntity<InputStreamResource> downloadLargeFile(
            @RequestParam("fileName") String fileName
    ) throws IOException {

        RestTemplate restTemplate = new RestTemplate();

        // Optional Accept header
        RequestCallback requestCallback = request -> request.getHeaders()
                .setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));

        // Streams the response instead of loading it all in memory
        ResponseExtractor<InputStreamResource> responseExtractor = response -> {
            // Here I write the response to a file but do what you like
            Path path = Paths.get("tmp/" + fileName);
            Files.copy(response.getBody(), path, StandardCopyOption.REPLACE_EXISTING);
            return new InputStreamResource(new FileInputStream(String.format("tmp/%s", fileName)));
        };

        InputStreamResource response = restTemplate.execute(
            String.format("http://%s:%s/file/largeFileRestTemplate?fileName=%s", host, "9091", fileName),
            HttpMethod.GET,
            requestCallback,
            responseExtractor
        );

        return ResponseEntity
            .ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=%s", fileName))
            .body(response);
    }
Spring
  • 831
  • 1
  • 12
  • 42
  • 1
    when request body is handled by a message converter it will be ByteArrayInputStream at the end and it will load in-memory, your example does'nt solve core problem, you are not loading in-momory while creating request that is good part. – DV Singh Sep 10 '22 at 21:34
  • @DVSingh, thank for sharing. I don’t say what your commented is wrong. I’ve used and well test on the code I posted. It’s actually solved the problem. – Spring Sep 11 '22 at 11:54
  • 4
    it's loaded to into the memory, but it doesn't mean the whole capacity. it's loaded piece by piece of bytes through the stream. – Spring Sep 12 '22 at 02:14
4

Thanks to Abhijit Sarkar's answer for leading the way.

I needed to download a heavy JSON stream and break it into small streamable manageable pieces of data. The JSON is composed of objects that have big properties: such big properties can be serialized to a file, and thus removed from the unmarshalled JSON object.

Another use case is to download a JSON stream object by object, process it like a map/reduce algorythm and produce a single output without having to load the whole stream in memory.

Yet another use case is to read a big JSON file and only pick a few objects based on a condition, while unmarshalling to Plain Old Java Objects.

Here is an example: we'd like to stream a very huge JSON file that is an array, and we'd like to retrieve only the first object in the array.

Given this big file on a server, available at http://example.org/testings.json :

[
    { "property1": "value1", "property2": "value2", "property3": "value3" },
    { "property1": "value1", "property2": "value2", "property3": "value3" },
    ... 1446481 objects => a file of 104 MB => take quite long to download...
]

Each row of this JSON array can be parsed as this object:

@lombok.Data
public class Testing {
    String property1;
    String property2;
    String property3;
}

You need this class make the parsing code reusable:

import com.fasterxml.jackson.core.JsonParser;
import java.io.IOException;
@FunctionalInterface
public interface JsonStreamer<R> {
    /**
     * Parse the given JSON stream, process it, and optionally return an object.<br>
     * The returned object can represent a downsized parsed version of the stream, or the result of a map/reduce processing, or null...
     *
     * @param jsonParser the parser to use while streaming JSON for processing
     * @return the optional result of the process (can be {@link Void} if processing returns nothing)
     * @throws IOException on streaming problem (you are also strongly encouraged to throw HttpMessageNotReadableException on parsing error)
     */
    R stream(JsonParser jsonParser) throws IOException;
}

And this class to parse:

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import lombok.AllArgsConstructor;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;

import java.io.IOException;
import java.util.Collections;
import java.util.List;

@AllArgsConstructor
public class StreamingHttpMessageConverter<R> implements HttpMessageConverter<R> {

    private final JsonFactory factory;
    private final JsonStreamer<R> jsonStreamer;

    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        return MediaType.APPLICATION_JSON.isCompatibleWith(mediaType);
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return false; // We only support reading from an InputStream
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return Collections.singletonList(MediaType.APPLICATION_JSON);
    }

    @Override
    public R read(Class<? extends R> clazz, HttpInputMessage inputMessage) throws IOException {
        try (InputStream inputStream = inputMessage.getBody();
             JsonParser parser = factory.createParser(inputStream)) {
            return jsonStreamer.stream(parser);
        }
    }

    @Override
    public void write(R result, MediaType contentType, HttpOutputMessage outputMessage) {
        throw new UnsupportedOperationException();
    }

}

Then, here is the code to use to stream the HTTP response, parse the JSON array and return only the first unmarshalled object:

// You should @Autowire these:
JsonFactory jsonFactory = new JsonFactory();
ObjectMapper objectMapper = new ObjectMapper();
RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();

// If detectRequestFactory true (default): HttpComponentsClientHttpRequestFactory will be used and it will consume the entire HTTP response, even if we close the stream early
// If detectRequestFactory false: SimpleClientHttpRequestFactory will be used and it will close the connection as soon as we ask it to

RestTemplate restTemplate = restTemplateBuilder.detectRequestFactory(false).messageConverters(
    new StreamingHttpMessageConverter<>(jsonFactory, jsonParser -> {

        // While you use a low-level JsonParser to not load everything in memory at once,
        // you can still profit from smaller object mapping with the ObjectMapper
        if (!jsonParser.isClosed() && jsonParser.nextToken() == JsonToken.START_ARRAY) {
            if (!jsonParser.isClosed() && jsonParser.nextToken() == JsonToken.START_OBJECT) {
                return objectMapper.readValue(jsonParser, Testing.class);
            }
        }
        return null;

    })
).build();

final Testing firstTesting = restTemplate.getForObject("http://example.org/testings.json", Testing.class);
log.debug("First testing object: {}", firstTesting);
Sebien
  • 821
  • 8
  • 11
  • 1
    Are you sure this code is able to process the whole file if you wanted to? The response stream is closed as soon as you exit the `try` block, so it appears that you will get an exception. – Abhijit Sarkar Jun 12 '18 at 09:32
  • That's why the whole parsing is done before the end of the try block. When creating a new StreamingHttpMessageConverter<>(jsonFactory, jsonStreamer), the JsonStreamer lambda passed as second parameter will be used in the read(...) method: the whole JsonStreamer.stream(...) method/lambda will be called inside the try, while the stream is open. And yes, as soon as we end the try/read(), the stream is closed (when detectRequestFactory=false). – Sebien Jun 12 '18 at 11:23
  • I see; I missed the fact that you’re returning `R`, so obviously you’ll have to consume the stream fully and deserialize into `R` – Abhijit Sarkar Jun 12 '18 at 12:18
  • How to iterate through all the objects – user1862354 Feb 25 '21 at 20:12
4

You can pass in your own response extractor. Here is an example where I write out the json to disk in a streaming fashion -

        RestTemplate restTemplate = new RestTemplateBuilder().basicAuthentication("user", "their_password" ).build();

        int responseSize = restTemplate.execute(uri,
            HttpMethod.POST,
            (ClientHttpRequest requestCallback) -> {
                requestCallback.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                requestCallback.getBody().write(body.getBytes());
            },
            responseExtractor -> {
                FileOutputStream fos  = new FileOutputStream(new File("out.json"));
                return StreamUtils.copy(responseExtractor.getBody(), fos);
            }
    )
neesh
  • 5,167
  • 6
  • 29
  • 32
  • 1
    This actually works quite well. If you're trying to stream a response from one service to another, you can copy this stream to your HttpServletResponse, or copy it to a file like you mentioned and then work with it as a local file – rpgFANATIC Jul 14 '20 at 20:52
-1

As a variant you can consume response as bytes and than convert to stream

byte data[] = restTemplate.execute(link, HttpMethod.GET, null, new BinaryFileExtractor());
return new ByteArrayInputStream(data);

Extractor is

public class BinaryFileExtractor implements ResponseExtractor<byte[]> {

  @Override
  public byte[] extractData(ClientHttpResponse response) throws IOException {
    return ByteStreams.toByteArray(response.getBody());
  }
}
inigo skimmer
  • 908
  • 6
  • 12
  • 4
    That is exactly what they are trying not to do. If you read it in as a byte[] you are loading the whole thing in memory. i.e. not a stream. If you then stream it through after loading the whole thing in RAM you have the worst of both options. – BrianC Jan 29 '19 at 22:56