Implementando gRPC em Java: Um Guia Detalhado
Vamos aprofundar na implementação do gRPC utilizando a linguagem Java.
O que é gRPC? gRPC, ou Google Remote Procedure Call, é uma estrutura de código aberto para RPC (Chamada de Procedimento Remoto) desenvolvida pelo Google. Seu propósito é facilitar a comunicação de alta performance entre microsserviços. Uma das principais vantagens do gRPC é a capacidade de permitir que serviços desenvolvidos em diferentes linguagens possam interagir entre si. Ele utiliza o formato Protobuf (Protocol Buffers) para a serialização de dados estruturados. Protobuf é um formato de mensagens binárias altamente eficiente e compacto, o que contribui para o desempenho do gRPC.
Em certos cenários, o uso da API gRPC pode oferecer melhor performance quando comparada com a API REST.
Nosso objetivo é criar um servidor gRPC. Para começar, precisamos definir arquivos .proto que descrevem nossos serviços e modelos (DTOs). Neste exemplo, utilizaremos o ProfileService
e o ProfileDescriptor
.
A definição do ProfileService
é a seguinte:
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) {} }
O gRPC suporta diversas modalidades de comunicação entre cliente e servidor. Vamos analisar cada uma delas:
- Chamada normal do servidor (requisição/resposta).
- Transmissão do cliente para o servidor (client streaming).
- Transmissão do servidor para o cliente (server streaming).
- Fluxo bidirecional (bidirectional streaming).
O serviço ProfileService
utiliza o ProfileDescriptor
, que está definido na seção de importação:
syntax = "proto3"; package com.deft.grpc; message ProfileDescriptor { int64 profile_id = 1; string name = 2; }
int64
corresponde ao tipoLong
em Java. Utilizaremos para o identificador do perfil.String
, assim como em Java, representa uma sequência de caracteres.
Para a construção do projeto, você pode usar o Gradle ou o Maven. Neste guia, utilizaremos o Maven. É importante mencionar que a configuração para geração dos arquivos a partir de .proto é diferente no Gradle. Para um servidor gRPC simples, precisamos apenas da seguinte dependência:
<dependency> <groupId>io.github.lognet</groupId> <artifactId>grpc-spring-boot-starter</artifactId> <version>4.5.4</version> </dependency>
Essa dependência simplifica bastante o processo de configuração do gRPC.
A estrutura do projeto será a seguinte:
Precisamos de um GrpcServerApplication
para inicializar o aplicativo Spring Boot, e um GrpcProfileService
, que implementará os métodos definidos no serviço .proto. Para utilizar o protoc e gerar as classes a partir dos arquivos .proto, adicionaremos o plugin protobuf-maven-plugin
ao arquivo pom.xml
. A seção de build deve ser configurada da seguinte forma:
<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
: Define o diretório onde os arquivos .proto estão localizados.outputDirectory
: Define o diretório onde os arquivos gerados serão colocados.clearOutputDirectory
: Um indicador para não apagar os arquivos gerados.
Neste ponto, já podemos construir o projeto. Após a compilação, podemos encontrar os arquivos gerados no diretório especificado. Agora, podemos implementar o GrpcProfileService
.
A declaração da classe será a seguinte:
@GRpcService public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase
A anotação @GRpcService
marca a classe como um bean do gRPC.
Como nossa classe herda de ProfileServiceGrpc.ProfileServiceImplBase
, podemos sobrescrever os métodos da classe pai. O primeiro método que iremos sobrescrever é o 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(); }
Para responder ao cliente, devemos chamar o método onNext
no StreamObserver
que foi passado como argumento. Após enviar a resposta, devemos enviar um sinal para o cliente indicando que o servidor terminou de processar a requisição através de onCompleted
. Ao enviar uma requisição para o servidor para o método getCurrentProfile
, a resposta será:
{ "profile_id": "1", "name": "test" }
Agora, vamos analisar o server stream. Nesta forma de comunicação, o cliente envia uma solicitação ao servidor, e o servidor responde com um fluxo de mensagens. Por exemplo, o servidor pode enviar cinco mensagens em um loop. Quando o envio é finalizado, o servidor envia uma mensagem para o cliente informando que o fluxo foi concluído com sucesso.
O método sobrescrito para server stream será assim:
@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(); }
Dessa forma, o cliente receberá cinco mensagens com um profileId
igual ao número da resposta.
{ "profile_id": "0", "name": "" } { "profile_id": "1", "name": "" } … { "profile_id": "4", "name": "" }
O client stream é similar ao server stream. A diferença é que, neste caso, o cliente envia um fluxo de mensagens e o servidor as processa. O servidor pode processar as mensagens de forma imediata ou aguardar todas as requisições do cliente antes de realizar o processamento.
@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(); } }; }
No client stream, precisamos retornar um StreamObserver
para o cliente, por onde o servidor receberá as mensagens. O método onError
será chamado se ocorrer algum erro no stream. Por exemplo, se o stream for finalizado de forma incorreta.
Para implementar um fluxo bidirecional, é necessário combinar os conceitos de server stream e client stream.
@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(); } }; }
Neste exemplo, em resposta a cada mensagem do cliente, o servidor retornará um perfil com um pointCount
incrementado.
Conclusão
Neste guia, abordamos as formas básicas de comunicação entre um cliente e um servidor utilizando gRPC: server stream, client stream e fluxo bidirecional.
Artigo escrito por Sergey Golitsyn