使用RestTemplate获取InputStream

时间:2016-04-02 23:23:19

标签: java inputstream resttemplate

我正在使用URL类从中读取InputStream。我有什么方法可以使用RestTemplate吗?

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

如何使用InputStream而不是RestTemplate获取URL

9 个答案:

答案 0 :(得分:24)

之前的答案并没有错,但它们没有深入到我喜欢的深度。有些情况下处理低级别InputStream不仅是可取的,而且是必要的,最常见的例子是将大型文件从源(某些Web服务器)流式传输到目标(数据库)。如果您尝试使用ByteArrayInputStream,那么您将会OutOfMemoryError向您致意。是的,您可以滚动自己的HTTP客户端代码,但是您必须处理错误的响应代码,响应转换器等。如果您已经在使用Spring,那么期待RestTemplate是一个很自然的选择。

在撰写本文时,spring-web:5.0.2.RELEASE的{​​{1}}有一个ResourceHttpMessageConverter,如果已设置,且响应类型为boolean supportsReadStreaming,则返回InputStreamResource ;否则返回InputStreamResource。很明显,你并不是唯一一个要求流媒体支持的人。

但是,有一个问题:ByteArrayResourceRestTemplate运行后很快关闭响应。因此,即使您要求HttpMessageConverter并获得它,也没有用,因为响应流已经关闭。我认为这是一个他们忽略的设计缺陷;它应该依赖于响应类型。所以不幸的是,对于阅读,你必须完全消费反应;如果使用InputStreamResource,则无法传递它。

尽管写作没有问题。如果您要传输RestTemplateInputStream会为您执行此操作。在引擎盖下,它使用ResourceHttpMessageConverter一次写入4096个字节,从org.springframework.util.StreamUtilsInputStream

部分OutputStream支持所有媒体类型,因此根据您的要求,您可能必须从HttpMessageConverter中移除默认类型,并设置您需要的内容,同时注意其相对排序

最后但并非最不重要的是,RestTemplate的实施具有ClientHttpRequestFactory,如果您要上传大型流,则可以且应该设置为boolean bufferRequestBody。否则,你知道,false。在撰写本文时,OutOfMemoryError(JDK客户端)和SimpleClientHttpRequestFactory(Apache HTTP客户端)支持此功能,但不支持HttpComponentsClientHttpRequestFactory。再次,设计监督。

修改: 提交的票证SPR-16885

答案 1 :(得分:23)

Spring有一个 `http://jsfiddle.net/bandhavya/xxkgxLr9/35/` 。它转换了Spring的org.springframework.http.converter.ResourceHttpMessageConverter类。 该org.springframework.core.io.Resource类封装了Resource,您可以通过InputStream获取。{/ p>

通过将someResource.getInputStream()指定为InputStream调用的响应类型,您可以通过RestTemplate开箱即用Resource.class。 / p>

以下是使用RestTemplate的{​​{1}}方法之一的示例:

RestTemplate

答案 2 :(得分:4)

我遇到了同样的问题,并通过扩展RestTemplate并仅在读取流之后才关闭连接来解决它。

您可以在此处查看代码:https://github.com/ItamarBenjamin/stream-rest-template

答案 3 :(得分:3)

你不应该直接获得InputStreamRestTemplate旨在封装处理响应(和请求)内容。它的优势在于处理所有IO并为您提供一个随时可用的Java对象。

您需要注册相应的HttpMessageConverter个对象。这些人可以通过HttpInputMessage对象访问回复InputStream

As Abdull suggests,Spring确实附带了HttpMessageConverter Resource实现,该实现本身包含InputStreamResourceHttpMessageConverter。它不支持所有Resource类型,但由于您应该对接口进行编程,因此您应该只使用超级接口Resource

当前实施(4.3.5)将返回ByteArrayResource,其中响应流的内容已复制到您可以访问的新ByteArrayInputStream

您不必关闭流。 RestTemplate会为您处理。 (如果您尝试使用ResourceHttpMessageConverter支持的另一种InputStreamResource,这是很不幸的,因为它包装了基础响应的InputStream,但在它暴露给您的客户端代码之前就已关闭。)

答案 4 :(得分:3)

感谢Abhijit Sarkar领导的回答。

我需要下载一个繁重的JSON流并将其分解为可流动的小型可管理数据。 JSON由具有大属性的对象组成:这些大属性可以序列化为文件,从而从解组的JSON对象中删除。

另一个用例是按对象下载JSON流对象,像map / reduce algorythm一样处理它,并生成单个输出,而不必将整个流加载到内存中。

另一个用例是读取一个大的JSON文件,只根据条件选择一些对象,同时解组为Plain Old Java Objects。

这是一个例子:我们想要传输一个非常庞大的JSON文件,它是一个数组,我们只想检索数组中的第一个对象。

鉴于服务器上的这个大文件,可在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...
]

此JSON数组的每一行都可以解析为此对象:

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

您需要此类使解析代码可重用:

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

这个要解析的类:

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

}

然后,这是用于流式传输HTTP响应的代码,解析JSON数组并仅返回第一个unmarshalled对象:

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

答案 5 :(得分:0)

您可以传入自己的响应提取器。这是我以流方式将json写到磁盘的示例-

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

答案 6 :(得分:0)

非常简单而有效的解决方案是使用ResponseExtractor。当您要在非常大的InputStream上运行并且RAM受限制时,此功能特别有用。

这是您应如何实施:

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

然后,在需要的任何地方调用该方法:

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

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

请注意以下常见陷阱

public InputStream getInputStreamFromResponse(String url) throws IOException {

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

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

这里InputStream将被关闭,然后您才能访问

答案 7 :(得分:0)

我这样做解决了。 希望对您有帮助。

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

答案 8 :(得分:-1)

作为变体,您可以将响应作为字节消耗,而不是转换为流

public class BinaryFileExtractor implements ResponseExtractor<byte[]> {

  @Override
  public byte[] extractData(ClientHttpResponse response) throws IOException {
    return ByteStreams.toByteArray(response.getBody());
  }
}

提取器是

{{1}}