Implémentation de gRPC en Java : Un Guide Pratique
Penchons-nous sur la manière d’intégrer gRPC dans un environnement Java.
gRPC, ou Google Remote Procedure Call, est une architecture RPC open source conçue par Google pour faciliter les communications à haute performance entre microservices. Cette technologie permet aux développeurs d’interconnecter des services développés dans différents langages. gRPC s’appuie sur Protobuf (Protocol Buffers), un format de sérialisation de données structurées extrêmement efficace et compressé.
Dans certains scénarios, l’API gRPC peut surpasser l’API REST en termes d’efficacité.
Mettons en place un serveur gRPC. Cela commence par la création de plusieurs fichiers .proto, qui définissent les services et les modèles de données (DTO). Pour ce serveur simple, nous utiliserons un service appelé ProfileService et son descripteur, ProfileDescriptor.
Voici à quoi ressemble le fichier de définition de ProfileService :
syntax = "proto3";
package com.deft.grpc;
import "google/protobuf/empty.proto";
import "profile_descriptor.proto";
service ProfileService {
rpc GetCurrentProfile (google.protobuf.Empty) returns (ProfileDescriptor) {}
rpc clientStream (stream ProfileDescriptor) returns (google.protobuf.Empty) {}
rpc serverStream (google.protobuf.Empty) returns (stream ProfileDescriptor) {}
rpc biDirectionalStream (stream ProfileDescriptor) returns (stream ProfileDescriptor) {}
}
gRPC propose diverses approches de communication client-serveur. Examinons-les en détail :
- Appel serveur standard : requête/réponse unique.
- Streaming du client vers le serveur.
- Streaming du serveur vers le client.
- Flux bidirectionnel : interactions simultanées dans les deux sens.
Le service ProfileService utilise le ProfileDescriptor, défini dans la section d’importation du fichier .proto :
syntax = "proto3";
package com.deft.grpc;
message ProfileDescriptor {
int64 profile_id = 1;
string name = 2;
}
- int64 : correspond au type Long en Java, pour stocker l’identifiant du profil.
- String : comme en Java, représente une chaîne de caractères.
Pour la gestion du projet, vous pouvez utiliser Gradle ou Maven. Ici, nous privilégions Maven pour sa commodité. Il est important de le noter, car la génération de code .proto diffère légèrement avec Gradle et nécessitera une configuration de build différente. Pour un serveur gRPC basique, nous n’avons besoin que d’une seule dépendance :
<dependency>
<groupId>io.github.lognet</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>4.5.4</version>
</dependency>
Ce starter prend en charge une part significative du travail nécessaire. Le projet aura la structure suivante:
Nous aurons besoin de GrpcServerApplication pour démarrer l’application Spring Boot, et de GrpcProfileService pour implémenter les méthodes définies dans le fichier .proto. Pour utiliser protoc et générer les classes à partir des fichiers .proto, nous ajoutons le plugin protobuf-maven-plugin dans le pom.xml. Voici la configuration de la section build :
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.2</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
<outputDirectory>${basedir}/target/generated-sources/grpc-java</outputDirectory>
<protocArtifact>com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.38.0:exe:${os.detected.classifier}</pluginArtifact>
<clearOutputDirectory>false</clearOutputDirectory>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
- protoSourceRoot : le chemin où les fichiers .proto sont stockés.
- outputDirectory : le dossier de destination pour les fichiers générés.
- clearOutputDirectory : indicateur pour empêcher la suppression des fichiers générés.
À ce stade, vous pouvez initialiser votre projet. Ensuite, accédez au répertoire de sortie spécifié. Les fichiers générés y seront. Vous pouvez alors procéder à l’implémentation de GrpcProfileService.
La déclaration de classe prendra la forme suivante :
@GRpcService public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase
L’annotation GRpcService identifie la classe en tant que bean grpc-service. Étant donné que nous héritons de ProfileServiceGrpc, nous pouvons remplacer les méthodes de la classe de base. La première méthode à remplacer est getCurrentProfile:
@Override
public void getCurrentProfile(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) {
System.out.println("getCurrentProfile");
responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor
.newBuilder()
.setProfileId(1)
.setName("test")
.build());
responseObserver.onCompleted();
}
Pour répondre au client, vous invoquez la méthode onNext sur le StreamObserver transmis. Après l’envoi de la réponse, vous envoyez un signal au client via onCompleted, indiquant que le traitement de la requête est terminé. Suite à une requête au serveur via getCurrentProfile, voici la réponse attendue :
{
"profile_id": "1",
"name": "test"
}
Passons au flux du serveur. Dans cette approche, le client envoie une requête au serveur, qui répond avec un flux de messages. Par exemple, le serveur pourrait envoyer cinq réponses en boucle. Une fois l’envoi terminé, il signale au client que le flux s’est terminé avec succès.
Voici l’implémentation de la méthode de streaming du serveur :
@Override
public void serverStream(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) {
for (int i = 0; i < 5; i++) {
responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor
.newBuilder()
.setProfileId(i)
.build());
}
responseObserver.onCompleted();
}
Le client recevra cinq messages, chacun contenant un ProfileId correspondant à l’index de la réponse.
{
"profile_id": "0",
"name": ""
}
{
"profile_id": "1",
"name": ""
}
…
{
"profile_id": "4",
"name": ""
}
Le flux client est similaire au flux serveur. Ici, c’est le client qui envoie un flux de messages au serveur. Le serveur peut traiter les messages au fur et à mesure de leur réception ou attendre la fin du flux pour un traitement groupé.
@Override
public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> clientStream(StreamObserver<Empty> responseObserver) {
return new StreamObserver<>() {
@Override
public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) {
log.info("ProfileDescriptor from client. Profile id: {}", profileDescriptor.getProfileId());
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onCompleted() {
responseObserver.onCompleted();
}
};
}
Dans le flux client, vous retournez un StreamObserver que le serveur utilisera pour recevoir les messages du client. La méthode onError sera invoquée en cas d’erreur dans le flux.
Par exemple, si le flux se termine de façon inattendue.
Pour mettre en œuvre un flux bidirectionnel, vous devez combiner les logiques de création de flux du serveur et du client.
@Override
public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> biDirectionalStream(
StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) {
return new StreamObserver<>() {
int pointCount = 0;
@Override
public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) {
log.info("biDirectionalStream, pointCount {}", pointCount);
responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor
.newBuilder()
.setProfileId(pointCount++)
.build());
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onCompleted() {
responseObserver.onCompleted();
}
};
}
Dans cet exemple, à chaque message reçu du client, le serveur répond avec un profil dont le pointCount est incrémenté.
Conclusion
Nous avons exploré les bases de la communication entre un client et un serveur en utilisant gRPC, incluant l’implémentation de flux serveur, flux client, et flux bidirectionnel.
Article rédigé par Sergey Golitsyn.