Implementering av gRPC i Java: En Dybdegående Guide
La oss undersøke hvordan man kan implementere gRPC i Java-utviklingsmiljøet.
gRPC (Google Remote Procedure Call): gRPC er en RPC-arkitektur med åpen kildekode, utviklet av Google. Den er designet for å muliggjøre høyhastighetskommunikasjon mellom mikrotjenester. gRPC gir utviklere muligheten til å integrere tjenester som er skrevet i ulike programmeringsspråk. Kommunikasjonen i gRPC foregår ved hjelp av meldingsformatet Protobuf (Protocol Buffers), et meget effektivt format for serialisering av strukturert data, som sikrer minimal datastørrelse.
I visse situasjoner kan et gRPC API være mer effektivt enn et tradisjonelt REST API.
Vi skal nå se på hvordan man lager en enkel gRPC-server. Det første steget innebærer å skrive flere .proto-filer, som beskriver tjenestene og datamodellene (DTO-er). For å illustrere dette, vil vi bruke tjenesten `ProfileService` og modellen `ProfileDescriptor`.
Her er hvordan `ProfileService` ser ut:
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 tilbyr ulike kommunikasjonsmodeller mellom klient og server. Disse kan deles inn i:
- Standard server-anrop – forespørsel og svar.
- Streaming fra klient til server.
- Streaming fra server til klient.
- Toveis streaming.
`ProfileService`-tjenesten bruker `ProfileDescriptor`, som er definert i import-delen:
syntax = "proto3"; package com.deft.grpc; message ProfileDescriptor { int64 profile_id = 1; string name = 2; }
- `int64` tilsvarer `long` i Java, og brukes til å representere profil-IDen.
- `string` er en strengvariabel, tilsvarende `String` i Java.
Man kan bruke enten Gradle eller Maven for å bygge prosjektet. Vi vil bruke Maven i dette eksemplet, da det er mer praktisk i dette tilfellet. Dette er viktig å merke seg, da genereringen av .proto-filer og konfigurasjonen av byggefilen vil variere noe for Gradle. For å sette opp en enkel gRPC-server, trenger vi en avhengighet:
<dependency> <groupId>io.github.lognet</groupId> <artifactId>grpc-spring-boot-starter</artifactId> <version>4.5.4</version> </dependency>
Denne starteren forenkler prosessen betydelig, og gjør mye av det nødvendige arbeidet for oss.
Strukturen til prosjektet vi skal lage vil være som følger:
Vi trenger `GrpcServerApplication` for å starte Spring Boot-applikasjonen, og `GrpcProfileService` for å implementere metodene definert i .proto-tjenesten. For å generere klasser fra .proto-filene ved hjelp av `protoc`, må vi legge til `protobuf-maven-plugin` i `pom.xml`. Bygge-delen av filen vil se slik ut:
<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` spesifiserer mappen hvor .proto-filene er plassert.
- `outputDirectory` angir mappen der de genererte filene skal plasseres.
- `clearOutputDirectory` indikerer om genererte filer skal slettes eller ikke.
På dette punktet kan man bygge prosjektet. Etter det, gå til mappen angitt i `outputDirectory`. Der vil de genererte filene være. Nå kan man begynne å implementere `GrpcProfileService` trinnvis.
Klassedeklarasjonen vil se slik ut:
@GRpcService public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase
`@GRpcService`-annotasjonen markerer klassen som en gRPC-tjenestekomponent.
Siden vi arver tjenesten vår fra `ProfileServiceGrpc.ProfileServiceImplBase`, kan vi overstyre metodene til den overordnede klassen. Den første metoden vi overstyrer er `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(); }
For å returnere et svar til klienten, må man kalle `onNext`-metoden på `StreamObserver`-objektet som er sendt inn som parameter. Etter at svaret er sendt, sendes en melding til klienten om at serveren er ferdig med å jobbe ved å kalle `onCompleted()`. Ved en forespørsel til `getCurrentProfile`-serveren vil svaret se slik ut:
{ "profile_id": "1", "name": "test" }
La oss så se på server-streaming. Med denne tilnærmingen sender klienten en forespørsel til serveren, og serveren svarer med en strøm av meldinger. For eksempel sender serveren fem forespørsler i en løkke. Når sendingen er fullført, sender serveren en melding til klienten om at strømmen er fullført.
Den overstyrte `serverStream`-metoden ser slik ut:
@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(); }
Dermed vil klienten motta fem meldinger med en `ProfileId` som tilsvarer løpenummeret:
{ "profile_id": "0", "name": "" } { "profile_id": "1", "name": "" } … { "profile_id": "4", "name": "" }
Klient-streaming er veldig likt server-streaming. I dette tilfellet sender klienten en strøm av meldinger til serveren, som behandler dem. Serveren kan behandle meldinger umiddelbart, eller vente til alle forespørsler fra klienten er mottatt før de behandles.
@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(); } }; }
I klient-streaming må man returnere en `StreamObserver` til klienten, som serveren vil motta meldinger på. `onError`-metoden vil bli kalt hvis det oppstår en feil i strømmen, for eksempel om den avsluttes feil.
For å implementere en toveis strøm, må man kombinere opprettelsen av en strøm fra serveren og klienten.
@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(); } }; }
I dette eksemplet, som respons på klientens melding, vil serveren returnere en profil med en økende `pointCount`.
Oppsummering
Vi har gjennomgått de fundamentale meldingstilnærmingene mellom en klient og en server ved hjelp av gRPC, og vi har implementert server-streaming, klient-streaming og toveis-streaming.
Artikkelen er skrevet av Sergey Golitsyn