web-dev-qa-db-fra.com

Obtenir InputStream avec RestTemplate

J'utilise une classe d'URL pour lire un InputStream. Est-il possible d'utiliser RestTemplate pour cela?

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

Comment puis-je obtenir InputStream avec RestTemplate au lieu d'utiliser URL?

24
user1950349

Vous ne devriez pas obtenir le InputStream directement. RestTemplate est destiné à encapsuler le traitement du contenu de la réponse (et de la requête). Sa force est de gérer tous les IO et de vous remettre un objet Java prêt à l'emploi.

Vous devrez enregistrer les objets appropriés HttpMessageConverter . Ceux-ci auront accès à la InputStream de la réponse, via un HttpInputMessage object.

Comme Abdull le suggère , Spring est livré avec une implémentation HttpMessageConverter pour Resource qui enveloppe elle-même une InputStream, ResourceHttpMessageConverter . Il ne prend pas en charge tous les types Resource, mais comme vous devriez quand même programmer des interfaces, vous devriez simplement utiliser la superinterface Resource.

L’implémentation actuelle (4.3.5) renverra une ByteArrayResource avec le contenu du flux de réponses copié dans une nouvelle ByteArrayInputStream à laquelle vous pourrez accéder. 

Vous n'êtes pas obligé de fermer le flux. La RestTemplate s'en occupe pour vous. (Cela est regrettable si vous essayez d'utiliser un InputStreamResource , un autre type pris en charge par ResourceHttpMessageConverter, car il enveloppe la réponse InputStream de la réponse sous-jacente mais est fermé avant de pouvoir être exposé à votre code client.)

1

Les réponses précédentes ne sont pas fausses, mais elles n'entrent pas dans la profondeur que j'aime voir. Il existe des cas où traiter InputStream de bas niveau est non seulement souhaitable, mais nécessaire, l'exemple le plus courant étant la transmission en continu d'un fichier volumineux de la source (un serveur Web) à la destination (une base de données). Si vous essayez d'utiliser un ByteArrayInputStream, vous serez, sans surprise, accueilli par OutOfMemoryError. Oui, vous pouvez utiliser votre propre code de client HTTP, mais vous devrez gérer les codes de réponse, les convertisseurs de réponse, etc. erronés. Si vous utilisez déjà Spring, il est naturel de choisir RestTemplate.

Au moment de cette écriture, spring-web:5.0.2.RELEASE a une ResourceHttpMessageConverter qui a un boolean supportsReadStreaming, qui si défini, et le type de réponse est InputStreamResource, retourne InputStreamResource; sinon, il retourne une ByteArrayResource. Il est donc clair que vous n'êtes pas le seul à avoir demandé une assistance en streaming.

Cependant, il existe un problème: RestTemplate ferme la réponse peu de temps après l'exécution de HttpMessageConverter. Ainsi, même si vous avez demandé InputStreamResource et l'avez obtenu, ce n'est pas bon, car le flux de réponse a été fermé. Je pense que c'est un défaut de conception qu'ils ont négligé; cela aurait dû dépendre du type de réponse. Donc, malheureusement, pour lire, vous devez consommer pleinement la réponse; vous ne pouvez pas le faire circuler si vous utilisez RestTemplate.

L'écriture n'est pas un problème cependant. Si vous souhaitez diffuser une InputStream, ResourceHttpMessageConverter le fera pour vous. Sous le capot, il utilise org.springframework.util.StreamUtils pour écrire 4 096 octets à la fois, de InputStream à OutputStream.

Certains des HttpMessageConverter prennent en charge tous les types de supports. Ainsi, selon vos besoins, vous devrez peut-être supprimer ceux par défaut de RestTemplate et définir ceux dont vous avez besoin, en tenant compte de leur ordre relatif.

Dernier point mais non le moindre, les implémentations de ClientHttpRequestFactory ont un boolean bufferRequestBody que vous pouvez et devez définir sur false si vous téléchargez un flux volumineux. Sinon, vous savez, OutOfMemoryError. Au moment de la rédaction de ce document, SimpleClientHttpRequestFactory (client JDK) et HttpComponentsClientHttpRequestFactory (client HTTP Apache) prennent en charge cette fonctionnalité, mais pas OkHttp3ClientHttpRequestFactory. Encore une fois, la supervision de la conception.

Éditer: Ticket archivé SPR-16885 .

23
Abhijit Sarkar

Spring a un org.springframework.http.converter.ResourceHttpMessageConverter. Il convertit la classe org.springframework.core.io.Resource ..__ de Spring. Cette classe Resource encapsule une InputStream, que vous pouvez obtenir via someResource.getInputStream().

En réunissant tous ces éléments, vous pouvez obtenir une variable InputStream via RestTemplate prête à l'emploi en spécifiant Resource.class comme type de réponse de votre invocation RestTemplate.

Voici un exemple utilisant l’une des méthodes RestTemplate 's exchange(..):

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
22
Abdull

Merci à Abhijit Sarkar pour sa réponse.

J'avais besoin de télécharger un flux JSON lourd et de le diviser en petits morceaux de données gérables en streaming . Le JSON est composé d'objets qui ont de grandes propriétés: de telles grandes propriétés peuvent être sérialisées dans un fichier, et ainsi supprimées du fichier JSON incontrôlé. objet.

Un autre cas d'utilisation consiste à télécharger un flux JSON objet par objet, à le traiter comme un algorithme map/réduire et à produire une sortie unique sans avoir à charger tout le flux en mémoire.

Encore un autre cas d’utilisation consiste à lire un gros fichier JSON et à ne sélectionner que quelques objets en fonction d’une condition, tout en respectant l’objet Plain Old Java Objects.

Voici un exemple: nous aimerions diffuser un très gros fichier JSON qui est un tableau et nous ne récupérerons que le premier objet du tableau.

Étant donné ce gros fichier sur un serveur, disponible à l’adresse 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...
]

Chaque ligne de ce tableau JSON peut être analysée comme cet objet:

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

Vous avez besoin de cette classe pour que le code d’analyse soit réutilisable:

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

Et cette classe à analyser:

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

}

Ensuite, voici le code à utiliser pour diffuser en continu la réponse HTTP, analyser le tableau JSON et renvoyer uniquement le premier objet 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);
1
Sebien

Vous pouvez passer votre propre extracteur de réponse. Voici un exemple où j’écris json sur disque en streaming -

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

En variante, vous pouvez utiliser la réponse sous forme d'octets et ensuite convertir en flux

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

L'extracteur est

public class BinaryFileExtractor implements ResponseExtractor<byte[]> {

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