API REST Spring avec tampons de protocole

API Spring REST avec tampons de protocole

1. Vue d'ensemble

Protocol Buffers est un mécanisme neutre de langage et de plate-forme pour la sérialisation et la désérialisation des données structurées, qui est proclamé par Google, son créateur, beaucoup plus rapide, plus petit et plus simple que d'autres types de charges utiles, tels que XML et JSON.

Ce didacticiel vous guide lors de la configuration d'une API REST pour tirer parti de cette structure de message binaire.

2. Tampons de protocole

Cette section fournit des informations de base sur les tampons de protocole et leur application dans l'écosystème Java.

2.1. Introduction aux tampons de protocole

Afin d'utiliser les tampons de protocole, nous devons définir des structures de message dans les fichiers.proto. Chaque fichier est une description des données pouvant être transférées d'un noeud à un autre ou stockées dans des sources de données. Voici un exemple de fichiers.proto, qui est nomméexample.proto et se trouve dans le répertoiresrc/main/resources. Ce fichier sera utilisé dans ce tutoriel ultérieurement:

syntax = "proto3";
package example;
option java_package = "com.example.protobuf";
option java_outer_classname = "exampleTraining";

message Course {
    int32 id = 1;
    string course_name = 2;
    repeated Student student = 3;
}
message Student {
    int32 id = 1;
    string first_name = 2;
    string last_name = 3;
    string email = 4;
    repeated PhoneNumber phone = 5;
    message PhoneNumber {
        string number = 1;
        PhoneType type = 2;
    }
    enum PhoneType {
        MOBILE = 0;
        LANDLINE = 1;
    }
}

Dans ce tutoriel,we use version 3 of both protocol buffer compiler and protocol buffer language, le fichier.proto doit donc commencer par la déclarationsyntax = “proto3”. Si un compilateur version 2 est en cours d'utilisation, cette déclaration serait omise. Vient ensuite la déclarationpackage, qui est l'espace de noms de cette structure de message pour éviter les conflits de noms avec d'autres projets.

Les deux déclarations suivantes sont utilisées pour Java uniquement: l'optionjava_package spécifie le package dans lequel nos classes générées doivent vivre, et l'optionjava_outer_classname indique le nom de la classe englobant tous les types définis dans ce.proto fichier.

La sous-section 2.3 ci-dessous décrit les éléments restants et explique comment ceux-ci sont compilés en code Java.

2.2. Tampons de protocole avec Java

Une fois la structure du message définie, nous avons besoin d’un compilateur pour convertir ce contenu indépendant du langage en code Java. Vous pouvez suivre les instructions dans lesProtocol Buffers repository afin d'obtenir une version de compilateur appropriée. Vous pouvez également télécharger un compilateur binaire pré-construit à partir du référentiel central Maven en recherchant l'artefactcom.google.protobuf:protoc, puis en choisissant une version appropriée pour votre plate-forme.

Ensuite, copiez le compilateur dans le répertoiresrc/main de votre projet et exécutez la commande suivante dans la ligne de commande:

protoc --java_out=java resources/example.proto

Cela devrait générer un fichier source pour la classeexampleTraining dans le packagecom.example.protobuf, comme spécifié dans les déclarationsoption du fichierexample.proto.

En plus du compilateur, l'exécution du tampon de protocole est requise. Ceci peut être réalisé en ajoutant la dépendance suivante au fichier Maven POM:


    com.google.protobuf
    protobuf-java
    3.0.0-beta-3

Nous pouvons utiliser une autre version du runtime, à condition qu'elle soit la même que la version du compilateur. Pour la dernière version, veuillez consulterthis link.

2.3. Compilation d'une description de message

En utilisant un compilateur, les messages d'un fichier.proto sont compilés en classes Java imbriquées statiques. Dans l'exemple ci-dessus, les messagesCourse etStudent sont convertis en classes JavaCourse etStudent, respectivement. En même temps, les champs des messages sont compilés dans des getters et des setters de style JavaBeans à l'intérieur de ces types générés. Le marqueur, composé d'un signe égal et d'un nombre, à la fin de chaque déclaration de champ est la balise unique utilisée pour coder le champ associé sous la forme binaire.

Nous allons parcourir les champs typés des messages pour voir comment ceux-ci sont convertis en méthodes d'accès.

Commençons par le messageCourse. Il comporte deux champs simples, dontid etcourse_name. Leurs types de tampon de protocole,int32 etstring, sont traduits en types Javaint etString. Voici leurs getters associés après compilation (les implémentations étant laissées de côté par souci de concision):

public int getId();
public java.lang.String getCourseName();

Notez que les noms des champs dactylographiés doivent être en majuscules (les mots individuels sont séparés par des caractères de soulignement) pour maintenir la coopération avec les autres langues. Le compilateur convertira ces noms en cas de chameau conformément aux conventions Java.

Le dernier champ du messageCourse,student, est du type complexeStudent, qui sera décrit ci-dessous. Ce champ est précédé du mot-clérepeated, ce qui signifie qu'il peut être répété un nombre illimité de fois. Le compilateur génère certaines méthodes associées au champstudent comme suit (sans implémentations):

public java.util.List getStudentList();
public int getStudentCount();
public com.example.protobuf.exampleTraining.Student getStudent(int index);

Nous allons maintenant passer au messageStudent, qui est utilisé comme type complexe du champstudent du messageCourse. Ses champs simples, y comprisid,first_name,last_name etemail sont utilisés pour créer des méthodes d'accès Java:

public int getId();
public java.lang.String getFirstName();
public java.lang.String getLastName();
public java.lang.String.getEmail();

Le dernier champ,phone, est du type complexePhoneNumber. Similaire au champstudent du messageCourse, ce champ est répétitif et a plusieurs méthodes associées:

public java.util.List getPhoneList();
public int getPhoneCount();
public com.example.protobuf.exampleTraining.Student.PhoneNumber getPhone(int index);

Le messagePhoneNumber est compilé dans le type imbriquéexampleTraining.Student.PhoneNumber, avec deux getters correspondant aux champs du message:

public java.lang.String getNumber();
public com.example.protobuf.exampleTraining.Student.PhoneType getType();

PhoneType, le type complexe du champtype du messagePhoneNumber, est un type d'énumération, qui sera transformé en un type Javaenum imbriqué dans leexampleTraining.Student) classe de s:

public enum PhoneType implements com.google.protobuf.ProtocolMessageEnum {
    MOBILE(0),
    LANDLINE(1),
    UNRECOGNIZED(-1),
    ;
    // Other declarations
}

3. Protobuf dans l'API REST Spring

Cette section vous guidera tout au long de la configuration d’un service REST à l’aide de Spring Boot.

3.1. Déclaration Bean

Commençons par la définition de nos@SpringBootApplication principaux:

@SpringBootApplication
public class Application {
    @Bean
    ProtobufHttpMessageConverter protobufHttpMessageConverter() {
        return new ProtobufHttpMessageConverter();
    }

    @Bean
    public CourseRepository createTestCourses() {
        Map courses = new HashMap<>();
        Course course1 = Course.newBuilder()
          .setId(1)
          .setCourseName("REST with Spring")
          .addAllStudent(createTestStudents())
          .build();
        Course course2 = Course.newBuilder()
          .setId(2)
          .setCourseName("Learn Spring Security")
          .addAllStudent(new ArrayList())
          .build();
        courses.put(course1.getId(), course1);
        courses.put(course2.getId(), course2);
        return new CourseRepository(courses);
    }

    // Other declarations
}

Le beanProtobufHttpMessageConverter est utilisé pour convertir les réponses renvoyées par les méthodes annotées de@RequestMapping en messages de tampon de protocole.

L'autre bean,CourseRepository, contient des données de test pour notre API.

Ce qui est important ici, c'est que nous fonctionnons avecProtocol Buffer specific data – not with standard POJOs.

Voici l'implémentation simple desCourseRepository:

public class CourseRepository {
    Map courses;

    public CourseRepository (Map courses) {
        this.courses = courses;
    }

    public Course getCourse(int id) {
        return courses.get(id);
    }
}

3.2. Configuration du contrôleur

Nous pouvons définir la classe@Controller pour une URL de test comme suit:

@RestController
public class CourseController {
    @Autowired
    CourseRepository courseRepo;

    @RequestMapping("/courses/{id}")
    Course customer(@PathVariable Integer id) {
        return courseRepo.getCourse(id);
    }
}

Et encore une fois, l’important ici est que le DTO de cours que nous renvoyons de la couche contrôleur n’est pas un POJO standard. Cela va être le déclencheur pour qu'il soit converti en messages de tampon de protocole avant d'être transféré vers le client.

4. Clients REST et tests

Maintenant que nous avons examiné la mise en œuvre simple de l'API - illustrons maintenantdeserialization of protocol buffer messages on the client side - en utilisant deux méthodes.

Le premier tire parti de l'APIRestTemplate avec un beanProtobufHttpMessageConverter préconfiguré pour convertir automatiquement les messages.

Le second utiliseprotobuf-java-format pour transformer manuellement les réponses de tampon de protocole en documents JSON.

Pour commencer, nous devons configurer le contexte d'un test d'intégration et demander à Spring Boot de trouver les informations de configuration dans la classeApplication en déclarant une classe de test comme suit:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebIntegrationTest
public class ApplicationTest {
    // Other declarations
}

Tous les extraits de code de cette section seront placés dans la classeApplicationTest.

4.1. Réponse attendue

La première étape pour accéder à un service REST consiste à déterminer l'URL de la demande:

private static final String COURSE1_URL = "http://localhost:8080/courses/1";

CeCOURSE1_URL sera utilisé pour obtenir le premier cours double de test du service REST que nous avons créé auparavant. Après l'envoi d'une demande GET à l'URL ci-dessus, la réponse correspondante est vérifiée à l'aide des assertions suivantes:

private void assertResponse(String response) {
    assertThat(response, containsString("id"));
    assertThat(response, containsString("course_name"));
    assertThat(response, containsString("REST with Spring"));
    assertThat(response, containsString("student"));
    assertThat(response, containsString("first_name"));
    assertThat(response, containsString("last_name"));
    assertThat(response, containsString("email"));
    assertThat(response, containsString("[email protected]"));
    assertThat(response, containsString("[email protected]"));
    assertThat(response, containsString("[email protected]"));
    assertThat(response, containsString("phone"));
    assertThat(response, containsString("number"));
    assertThat(response, containsString("type"));
}

Nous utiliserons cette méthode d'assistance dans les deux cas de test décrits dans les sous-sections suivantes.

4.2. Test avecRestTemplate

Voici comment nous créons un client, envoyons une requête GET à la destination désignée, recevons la réponse sous la forme de messages de tampon de protocole et la vérifions à l'aide de l'APIRestTemplate:

@Autowired
private RestTemplate restTemplate;

@Test
public void whenUsingRestTemplate_thenSucceed() {
    ResponseEntity course = restTemplate.getForEntity(COURSE1_URL, Course.class);
    assertResponse(course.toString());
}

Pour que ce cas de test fonctionne, nous avons besoin d'un bean de typeRestTemplate pour être enregistré dans une classe de configuration:

@Bean
RestTemplate restTemplate(ProtobufHttpMessageConverter hmc) {
    return new RestTemplate(Arrays.asList(hmc));
}

Un autre bean de typeProtobufHttpMessageConverter est également nécessaire pour transformer automatiquement les messages de tampon de protocole reçus. Ce haricot est le même que celui défini dans la sous-section 3.1. Comme le client et le serveur partagent le même contexte d'application dans ce didacticiel, nous pouvons déclarer le beanRestTemplate dans la classeApplication et réutiliser le beanProtobufHttpMessageConverter.

4.3. Test avecHttpClient

La première étape pour utiliser l'APIHttpClient et convertir manuellement les messages de tampon de protocole consiste à ajouter les deux dépendances suivantes au fichier Maven POM:


    com.googlecode.protobuf-java-format
    protobuf-java-format
    1.4


    org.apache.httpcomponents
    httpclient
    4.5.2

Pour les dernières versions de ces dépendances, veuillez consulter les artefactsprotobuf-java-format ethttpclient dans le référentiel central Maven.

Passons à la création d'un client, exécutons une requête GET et convertissons la réponse associée en une instanceInputStream à l'aide de l'URL donnée:

private InputStream executeHttpRequest(String url) throws IOException {
    CloseableHttpClient httpClient = HttpClients.createDefault();
    HttpGet request = new HttpGet(url);
    HttpResponse httpResponse = httpClient.execute(request);
    return httpResponse.getEntity().getContent();
}

Maintenant, nous allons convertir les messages de tampon de protocole sous la forme d'un objetInputStream en un document JSON:

private String convertProtobufMessageStreamToJsonString(InputStream protobufStream) throws IOException {
    JsonFormat jsonFormat = new JsonFormat();
    Course course = Course.parseFrom(protobufStream);
    return jsonFormat.printToString(course);
}

Et voici comment un scénario de test utilise les méthodes d'assistance privée déclarées ci-dessus et valide la réponse:

@Test
public void whenUsingHttpClient_thenSucceed() throws IOException {
    InputStream responseStream = executeHttpRequest(COURSE1_URL);
    String jsonOutput = convertProtobufMessageStreamToJsonString(responseStream);
    assertResponse(jsonOutput);
}

4.4. Réponse en JSON

Afin de clarifier les choses, les réponses JSON que nous avons reçues aux tests décrits dans les sous-sections précédentes sont présentées ici:

id: 1
course_name: "REST with Spring"
student {
    id: 1
    first_name: "John"
    last_name: "Doe"
    email: "[email protected]"
    phone {
        number: "123456"
    }
}
student {
    id: 2
    first_name: "Richard"
    last_name: "Roe"
    email: "[email protected]"
    phone {
        number: "234567"
        type: LANDLINE
    }
}
student {
    id: 3
    first_name: "Jane"
    last_name: "Doe"
    email: "[email protected]"
    phone {
        number: "345678"
    }
    phone {
        number: "456789"
        type: LANDLINE
    }
}

5. Conclusion

Ce didacticiel a rapidement présenté les tampons de protocole et illustré la configuration d’une API REST utilisant le format avec Spring. Nous sommes ensuite passés au support client et au mécanisme de sérialisation-désérialisation.

L'implémentation de tous les exemples et extraits de code peut être trouvée dansa GitHub project.