Explorând Implementarea gRPC în Java
Să ne aprofundăm în modul de implementare a gRPC utilizând limbajul Java. gRPC, sau Google Remote Procedure Call, reprezintă o arhitectură open-source RPC, creată de Google pentru a facilita comunicații rapide între microservicii. Această tehnologie le permite dezvoltatorilor să integreze servicii scrise în diverse limbaje de programare. gRPC utilizează formatul de mesagerie Protobuf (Protocol Buffers), cunoscut pentru eficiența sa ridicată și compactitatea în serializarea datelor structurate.
În anumite scenarii, API-ul gRPC poate oferi performanțe superioare comparativ cu API-ul REST.
Pentru a exemplifica, vom dezvolta un server gRPC. Inițial, trebuie să creăm fișiere .proto care să descrie serviciile și modelele de date (DTO). În scopul unui server simplu, vom utiliza ProfileService și ProfileDescriptor.
Definiția ProfileService este următoarea:
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 oferă diverse moduri de comunicare client-server, pe care le vom detalia:
- Apel simplu de tip server – cerere și răspuns.
- Streaming de date de la client la server.
- Streaming de date de la server la client.
- Flux bidirecțional de date.
Serviciul ProfileService folosește ProfileDescriptor, definit în secțiunea de import:
syntax = "proto3";
package com.deft.grpc;
message ProfileDescriptor {
int64 profile_id = 1;
string name = 2;
}
int64corespunde tipuluiLongdin Java, reprezentând ID-ul profilului.stringeste similar cu tipulStringdin Java, reprezentând un șir de caractere.
Pentru construirea proiectului, se pot utiliza Gradle sau Maven. În acest exemplu vom utiliza Maven, deoarece configurația pentru generarea claselor din fișierele .proto va fi ușor diferită în cazul Gradle. Pentru un server gRPC simplu, este necesară o singură dependență:
<dependency>
<groupId>io.github.lognet</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>4.5.4</version>
</dependency>
Acest starter simplifică considerabil procesul de configurare.
Structura proiectului va fi următoarea:
GrpcServerApplication |
Aplicația Spring Boot. |
GrpcProfileService |
Implementează metode din serviciul .proto. |
Pentru a utiliza protoc și a genera clase din fișierele .proto, este necesar să adăugăm plugin-ul protobuf-maven-plugin în fișierul pom.xml. Secțiunea de construire (build) va arăta astfel:
<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: specifică directorul unde se află fișierele .proto.outputDirectory: definește directorul unde vor fi generate fișierele Java.clearOutputDirectory: indică dacă fișierele generate trebuie sau nu șterse înainte de a fi generate din nou.
După ce proiectul este construit, fișierele generate vor fi disponibile în directorul specificat în outputDirectory. Acum putem implementa clasa GrpcProfileService.
Declarația clasei va fi:
@GRpcService public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase
Adnotarea @GRpcService marchează clasa ca bean gRPC.
Deoarece clasa noastră extinde ProfileServiceGrpc.ProfileServiceImplBase, putem suprascrie metodele clasei părinte. Prima metodă pe care o vom implementa este 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();
}
Pentru a trimite un răspuns către client, se apelează metoda onNext a obiectului StreamObserver primit ca parametru. După trimiterea răspunsului, se semnalează clientului terminarea operației prin metoda onCompleted. La trimiterea unei cereri către metoda getCurrentProfile, răspunsul va fi:
{
"profile_id": "1",
"name": "test"
}
În continuare, vom analiza fluxul de date de la server la client. În acest mod de comunicare, clientul trimite o cerere, iar serverul răspunde cu un flux continuu de mesaje. Exemplul va trimite cinci mesaje într-o buclă. După trimiterea tuturor mesajelor, serverul semnalează clientului finalizarea fluxului.
Metoda suprascrisă serverStream va fi:
@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();
}
Astfel, clientul va primi cinci mesaje ProfileDescriptor, fiecare cu profile_id diferit.
{
"profile_id": "0",
"name": ""
}
{
"profile_id": "1",
"name": ""
}
…
{
"profile_id": "4",
"name": ""
}
Fluxul client este similar cu cel de server, dar de data aceasta clientul trimite un flux de mesaje, iar serverul le procesează. Serverul poate procesa mesajele imediat sau poate aștepta recepționarea tuturor mesajelor înainte de a le procesa.
@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();
}
};
}
În cazul fluxului client, trebuie returnat un obiect StreamObserver, prin intermediul căruia serverul va primi mesajele. Metoda onError va fi apelată în caz de eroare în flux.
Pentru implementarea unui flux bidirecțional, trebuie să combinăm crearea unui flux atât de la server cât și de la 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();
}
};
}
În acest exemplu, ca răspuns la mesajul clientului, serverul va returna un profil cu un pointCount incrementat.
Concluzii
Am analizat opțiunile de bază pentru comunicarea între un client și un server utilizând gRPC: flux de server implementat, flux de client și flux bidirecțional.
Articol scris de Serghei Golitsyn.