Formats de données binaires dans une API REST Spring

1. Vue d’ensemble

JSON et XML sont des formats de transfert de données très répandus en matière d’API REST, mais ce n’est pas la seule option disponible.

Il existe de nombreux autres formats avec un degré de vitesse de sérialisation et une taille de données sérialisées variables.

Dans cet article, nous explorons comment configurer un mécanisme Spring REST pour utiliser des formats de données binaires - que nous illustrons avec Kryo.

De plus, nous montrons comment prendre en charge plusieurs formats de données en ajoutant la prise en charge des tampons de protocole Google.

2. HttpMessageConverter

L’interface HttpMessageConverter est essentiellement l’API publique de Spring pour la conversion des formats de données REST.

Il existe différentes manières de spécifier les convertisseurs souhaités. Ici, nous implémentons WebMvcConfigurer et fournissons explicitement les convertisseurs que nous voulons utiliser dans la méthode configureMessageConverters remplacée:

@Configuration
@EnableWebMvc
@ComponentScan({ "org.baeldung.web" })
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
       //...
    }
}

3. Kryo

3.1. Présentation de Kryo et Maven

Kryo est un format de codage binaire qui offre une bonne vitesse de sérialisation et de désérialisation et une taille de données transférée inférieure à celle des formats texte.

Bien qu’en théorie, il puisse être utilisé pour transférer des données entre différents types de systèmes, il est principalement conçu pour fonctionner avec des composants Java.

Nous ajoutons les bibliothèques Kryo nécessaires avec la dépendance Maven suivante:

<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>4.0.0</version>
</dependency>

Pour vérifier la dernière version de kryo , vous pouvez avoir https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22com.esotericsoftware%22%20AND%20a%3A%22kryo%22 Tuna regarde ici].

3.2. Kryo au printemps REST

Pour utiliser Kryo en tant que format de transfert de données, nous créons un HttpMessageConverter personnalisé et implémentons la logique de sérialisation et de désérialisation nécessaire. Nous définissons également notre en-tête HTTP personnalisé pour Kryo:

application/x-kryo Voici un exemple de travail simplifié complet que nous utilisons à des fins de démonstration:

public class KryoHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

    public static final MediaType KRYO = new MediaType("application", "x-kryo");

    private static final ThreadLocal<Kryo> kryoThreadLocal = new ThreadLocal<Kryo>() {
        @Override
        protected Kryo initialValue() {
            Kryo kryo = new Kryo();
            kryo.register(Foo.class, 1);
            return kryo;
        }
    };

    public KryoHttpMessageConverter() {
        super(KRYO);
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return Object.class.isAssignableFrom(clazz);
    }

    @Override
    protected Object readInternal(
      Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException {
        Input input = new Input(inputMessage.getBody());
        return kryoThreadLocal.get().readClassAndObject(input);
    }

    @Override
    protected void writeInternal(
      Object object, HttpOutputMessage outputMessage) throws IOException {
        Output output = new Output(outputMessage.getBody());
        kryoThreadLocal.get().writeClassAndObject(output, object);
        output.flush();
    }

    @Override
    protected MediaType getDefaultContentType(Object object) {
        return KRYO;
    }
}

Notez que nous utilisons un ThreadLocal ici simplement parce que la création d’instances Kryo peut coûter cher, et nous souhaitons les réutiliser autant que possible.

La méthode du contrôleur est simple (remarque, aucun type de données spécifique au protocole n’est personnalisé, nous utilisons plain DTO Foo__):

@RequestMapping(method = RequestMethod.GET, value = "/foos/{id}")
@ResponseBody
public Foo findById(@PathVariable long id) {
    return fooRepository.findById(id);
}

Et un rapide test pour prouver que nous avons tout branché correctement:

RestTemplate restTemplate = new RestTemplate();
restTemplate.setMessageConverters(Arrays.asList(new KryoHttpMessageConverter()));

HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(KryoHttpMessageConverter.KRYO));
HttpEntity<String> entity = new HttpEntity<String>(headers);

ResponseEntity<Foo> response = restTemplate.exchange("http://localhost:8080/spring-rest/foos/{id}",
  HttpMethod.GET, entity, Foo.class, "1");
Foo resource = response.getBody();

assertThat(resource, notNullValue());

4. Prise en charge de plusieurs formats de données

Souvent, vous souhaitez prendre en charge plusieurs formats de données pour le même service. Les clients spécifient les formats de données souhaités dans l’en-tête HTTP Accept et le convertisseur de message correspondant est appelé pour sérialiser les données.

Habituellement, il vous suffit d’enregistrer un autre convertisseur pour que les choses fonctionnent immédiatement. Spring sélectionne automatiquement le convertisseur approprié en fonction de la valeur de l’en-tête Accept et des types de support pris en charge déclarés dans les convertisseurs.

Par exemple, pour ajouter la prise en charge de JSON et de Kryo, enregistrez les deux KryoHttpMessageConverter et MappingJackson2HttpMessageConverter :

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
    messageConverters.add(new MappingJackson2HttpMessageConverter());
    messageConverters.add(new KryoHttpMessageConverter());
    super.configureMessageConverters(messageConverters);
}

Supposons maintenant que nous voulions également ajouter Google Protocol Buffer à la liste. Pour cet exemple, nous supposons qu’il existe une classe FooProtos.Foo générée avec le compilateur protoc en fonction du fichier proto suivant:

package baeldung;
option java__package = "org.baeldung.web.dto";
option java__outer__classname = "FooProtos";
message Foo {
    required int64 id = 1;
    required string name = 2;
}

Spring est livré avec un support intégré pour Protocol Buffer. Tout ce dont nous avons besoin pour que cela fonctionne est d’inclure ProtobufHttpMessageConverter dans la liste des convertisseurs pris en charge:

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
    messageConverters.add(new MappingJackson2HttpMessageConverter());
    messageConverters.add(new KryoHttpMessageConverter());
    messageConverters.add(new ProtobufHttpMessageConverter());
}

Cependant, nous devons définir une méthode de contrôleur distincte qui renvoie les instances FooProtos.Foo (JSON et Kryo gèrent tous les deux __Foo __s, aucun changement n’est donc nécessaire dans le contrôleur pour les distinguer).

Il y a deux façons de résoudre l’ambiguïté concernant la méthode appelée. La première approche consiste à utiliser des URL différentes pour protobuf et d’autres formats. Par exemple, pour protobuf:

@RequestMapping(method = RequestMethod.GET, value = "/fooprotos/{id}")
@ResponseBody
public FooProtos.Foo findProtoById(@PathVariable long id) { ... }

et pour les autres:

@RequestMapping(method = RequestMethod.GET, value = "/foos/{id}")
@ResponseBody
public Foo findById(@PathVariable long id) { ... }

Notez que pour protobuf nous utilisons value = “/fooprotos/\ {id}” et pour les autres formats value = “/foos/\ {id}” .

  • La seconde approche - et la meilleure - consiste à utiliser la même URL, mais à spécifier explicitement le format des données produites dans le mappage de la demande pour protobuf: **

@RequestMapping(
  method = RequestMethod.GET,
  value = "/foos/{id}",
  produces = { "application/x-protobuf" })
@ResponseBody
public FooProtos.Foo findProtoById(@PathVariable long id) { ... }

Notez qu’en spécifiant le type de média dans l’attribut annotation produces , nous donnons un indice au mécanisme Spring sous-jacent quant au mappage à utiliser en fonction de la valeur de l’en-tête Accept fournie par les clients. être appelé pour l’URL “foos/\ {id}” .

La deuxième approche nous permet de fournir aux clients une API REST uniforme et cohérente pour tous les formats de données.

Enfin, si vous souhaitez approfondir l’utilisation des tampons de protocole avec une API Spring REST, consultez le lien:/spring-rest-api-with-protocol-buffers[l’article de référence].

5. Enregistrement de convertisseurs de messages supplémentaires

Il est très important de noter que vous perdez tous les convertisseurs de message par défaut lorsque vous substituez la méthode configureMessageConverters .

Seuls ceux que vous fournissez seront utilisés.

Même si parfois c’est exactement ce que vous voulez, dans de nombreux cas, vous souhaitez simplement ajouter de nouveaux convertisseurs, tout en conservant ceux par défaut, qui prennent déjà en charge les formats de données standard tels que JSON. Pour ce faire, substituez la méthode extendMessageConverters :

@Configuration
@EnableWebMvc
@ComponentScan({ "org.baeldung.web" })
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
        messageConverters.add(new ProtobufHttpMessageConverter());
        messageConverters.add(new KryoHttpMessageConverter());
    }
}

6. Conclusion

Dans ce tutoriel, nous avons étudié la facilité d’utilisation de tout format de transfert de données dans Spring MVC, à l’aide de Kryo comme exemple.

Nous avons également montré comment ajouter la prise en charge de plusieurs formats afin que différents clients puissent utiliser différents formats.

L’implémentation de ces formats de données binaires dans un tutoriel Spring REST API est bien sûr sur Github . Ceci est un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.