Implementação de Serviços gRPC em Java: Um Guia Prático

O gRPC é um framework de chamada de procedimento remoto (RPC) moderno e de alto desempenho, originalmente desenvolvido pelo Google. Ele utiliza o Protocol Buffers (protobuf) como sua Interface Definition Language (IDL) para definir a estrutura dos serviços e mensagens, e o HTTP/2 como protocolo de transporte, oferecendo suporte nativo a streams bidirecionais e multiplexação de requisições. Seu uso se torna cada vez mais relevante em arquiteturas de microsserviços e ambientes cloud-native, devido à sua eficiência, interoperabilidade entre diferentes linguagens e suporte a recursos como autenticação, balanceamento de carga e observabilidade.

Conceitos Fundamentais

RPC (Remote Procedure Call): Um paradigma que permite a uma aplicação chamar uma função ou método em outro endereço de rede, como se fosse uma invocação local.

IDL (Interface Definition Language): Uma linguagem para descrever a interface de um serviço de software de forma independente da linguagem de programação. No gRPC, o Protocol Buffers é a IDL padrão.

Protocol Buffers (protobuf): Um mecanismo de serialização de dados neutro, eficiente e extensível. Ele define a estrutura dos dados (mensagens) e dos serviços (com métodos e tipos) em arquivos .proto.

Tipos de Chamada: O gRPC suporta quatro padrões de comunicação principais: chamadas unárias (simples request/response), streaming do servidor (o servidor envia uma sequência de respostas), streaming do cliente (o cliente envia uma sequência de requisições) e streaming bidirecional (ambos os lados enviam streams).

Exemplo Prático em Java

Vamos construir um serviço gRPC em Java que demonstra os diferentes tipos de chamada. O projeto utiliza Maven para gerenciamento de dependências e geração de código a partir dos arquivos .proto.

1. Definição do Serviço com Protocol Buffers

Crie o arquivo src/main/proto/greetings.proto. A definição abaixo especifica quatro métodos, cada um representando um tipo de chamada diferente.

syntax = "proto3";
package tutorial;
option java_package = "com.example.grpc.tutorial";
option java_multiple_files = true;

service Greeter {
  rpc SayHelloUnary (GreetingRequest) returns (GreetingResponse);
  rpc SayHelloServerStream (GreetingRequest) returns (stream GreetingResponse);
  rpc SayHelloClientStream (stream GreetingRequest) returns (GreetingResponse);
  rpc SayHelloBidirectional (stream GreetingRequest) returns (stream GreetingResponse);
}

message GreetingRequest {
  string person_name = 1;
}

message GreetingResponse {
  string greeting_message = 1;
}

2. Configruação do Maven para Geração de Código

Adicione o plugin protobuf-maven-plugin ao seu pom.xml. Ao executar mvn compile, o plugin gera automaticamente as classes Java correspondentes à definição do serviço e às mensagens.

<build>
  <plugins>
    <plugin>
      <groupId>org.xolstice.maven.plugins</groupId>
      <artifactId>protobuf-maven-plugin</artifactId>
      <version>0.6.1</version>
      <configuration>
        <protocArtifact>com.google.protobuf:protoc:3.21.12:exe:${os.detected.classifier}</protocArtifact>
        <pluginId>grpc-java</pluginId>
        <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.54.1:exe:${os.detected.classifier}</pluginArtifact>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>compile</goal>
            <goal>compile-custom</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

3. Implementação do Servidor

A lógica do serviço reside na classe que estende GreeterGrpc.GreeterImplBase. Cada método é sobrescrito para tratar o tipo de chamada correspondente.

package com.example.grpc.server;

import com.example.grpc.tutorial.*;
import io.grpc.stub.StreamObserver;

import java.util.ArrayList;
import java.util.List;

public class GreeterServiceImpl extends GreeterGrpc.GreeterImplBase {

    // Chamada Unária
    @Override
    public void sayHelloUnary(GreetingRequest req, StreamObserver<GreetingResponse> responseObserver) {
        GreetingResponse reply = GreetingResponse.newBuilder()
                .setGreetingMessage("Olá, " + req.getPersonName())
                .build();
        responseObserver.onNext(reply);
        responseObserver.onCompleted();
    }

    // Streaming do Servidor
    @Override
    public void sayHelloServerStream(GreetingRequest req, StreamObserver<GreetingResponse> responseObserver) {
        String name = req.getPersonName();
        for (int i = 1; i <= 5; i++) {
            GreetingResponse reply = GreetingResponse.newBuilder()
                    .setGreetingMessage("Saudação #" + i + " para " + name)
                    .build();
            responseObserver.onNext(reply);
        }
        responseObserver.onCompleted();
    }

    // Streaming do Cliente
    @Override
    public StreamObserver<GreetingRequest> sayHelloClientStream(StreamObserver<GreetingResponse> responseObserver) {
        return new StreamObserver<GreetingRequest>() {
            final List<String> receivedNames = new ArrayList<>();

            @Override
            public void onNext(GreetingRequest value) {
                receivedNames.add(value.getPersonName());
            }

            @Override
            public void onError(Throwable t) {
                System.err.println("Erro no stream do cliente: " + t.getMessage());
            }

            @Override
            public void onCompleted() {
                String allNames = String.join(", ", receivedNames);
                GreetingResponse reply = GreetingResponse.newBuilder()
                        .setGreetingMessage("Olá para todos: " + allNames)
                        .build();
                responseObserver.onNext(reply);
                responseObserver.onCompleted();
            }
        };
    }

    // Streaming Bidirecional
    @Override
    public StreamObserver<GreetingRequest> sayHelloBidirectional(StreamObserver<GreetingResponse> responseObserver) {
        return new StreamObserver<GreetingRequest>() {
            @Override
            public void onNext(GreetingRequest req) {
                // Para cada requisição recebida, envia uma resposta imediatamente
                GreetingResponse reply = GreetingResponse.newBuilder()
                        .setGreetingMessage("Eco: Olá, " + req.getPersonName())
                        .build();
                responseObserver.onNext(reply);
            }

            @Override
            public void onError(Throwable t) {
                System.err.println("Erro no stream bidirecional: " + t.getMessage());
            }

            @Override
            public void onCompleted() {
                responseObserver.onCompleted();
            }
        };
    }
}

Para iniciar o servidor:

import io.grpc.Server;
import io.grpc.ServerBuilder;

public class GrpcServer {
    public static void main(String[] args) throws Exception {
        Server server = ServerBuilder.forPort(8080)
                .addService(new GreeterServiceImpl())
                .build()
                .start();
        System.out.println("Servidor gRPC iniciado na porta 8080");
        server.awaitTermination();
    }
}

4. Implementação do Cliente

O cliente estabelece um canal com o servidor e cria um stub (cliente) para invocar os métodos do serviço.

package com.example.grpc.client;

import com.example.grpc.tutorial.*;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;

import java.util.Iterator;
import java.util.concurrent.CountDownLatch;

public class GrpcClient {
    public static void main(String[] args) throws InterruptedException {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080)
                .usePlaintext()
                .build();

        // Usando stub assíncrono para todos os exemplos
        GreeterGrpc.GreeterStub asyncStub = GreeterGrpc.newStub(channel);

        // 1. Chamada Unária
        System.out.println("Testando chamada unária...");
        GreetingRequest unaryRequest = GreetingRequest.newBuilder().setPersonName("Alice").build();
        asyncStub.sayHelloUnary(unaryRequest, new StreamObserver<GreetingResponse>() {
            @Override
            public void onNext(GreetingResponse value) {
                System.out.println("Resposta Unária: " + value.getGreetingMessage());
            }
            @Override
            public void onError(Throwable t) { t.printStackTrace(); }
            @Override
            public void onCompleted() { System.out.println("Unária concluída."); }
        });

        // 2. Streaming do Servidor
        System.out.println("\nTestando streaming do servidor...");
        GreetingRequest serverStreamReq = GreetingRequest.newBuilder().setPersonName("Bob").build();
        asyncStub.sayHelloServerStream(serverStreamReq, new StreamObserver<GreetingResponse>() {
            @Override
            public void onNext(GreetingResponse value) {
                System.out.println("Resposta do Servidor (Stream): " + value.getGreetingMessage());
            }
            @Override
            public void onError(Throwable t) { t.printStackTrace(); }
            @Override
            public void onCompleted() { System.out.println("Streaming do servidor concluído."); }
        });

        // 3. Streaming do Cliente
        System.out.println("\nTestando streaming do cliente...");
        CountDownLatch clientLatch = new CountDownLatch(1);
        StreamObserver<GreetingRequest> clientStreamObserver = asyncStub.sayHelloClientStream(new StreamObserver<GreetingResponse>() {
            @Override
            public void onNext(GreetingResponse value) {
                System.out.println("Resposta do Cliente (Stream): " + value.getGreetingMessage());
            }
            @Override
            public void onError(Throwable t) { t.printStackTrace(); }
            @Override
            public void onCompleted() {
                System.out.println("Streaming do cliente concluído.");
                clientLatch.countDown();
            }
        });
        clientStreamObserver.onNext(GreetingRequest.newBuilder().setPersonName("Carlos").build());
        clientStreamObserver.onNext(GreetingRequest.newBuilder().setPersonName("Diana").build());
        clientStreamObserver.onCompleted();
        clientLatch.await();

        // 4. Streaming Bidirecional
        System.out.println("\nTestando streaming bidirecional...");
        CountDownLatch bidiLatch = new CountDownLatch(1);
        StreamObserver<GreetingRequest> bidiObserver = asyncStub.sayHelloBidirectional(new StreamObserver<GreetingResponse>() {
            @Override
            public void onNext(GreetingResponse value) {
                System.out.println("Resposta Bidirecional: " + value.getGreetingMessage());
            }
            @Override
            public void onError(Throwable t) { t.printStackTrace(); }
            @Override
            public void onCompleted() {
                System.out.println("Streaming bidirecional concluído.");
                bidiLatch.countDown();
            }
        });
        bidiObserver.onNext(GreetingRequest.newBuilder().setPersonName("Eva").build());
        bidiObserver.onNext(GreetingRequest.newBuilder().setPersonName("Fernando").build());
        bidiObserver.onCompleted();
        bidiLatch.await();

        channel.shutdown();
    }
}

Execute primeiro a classe GrpcServer e, em seguida, a GrpcClient. O console do servidor mostrará as requisições recebidas e as respostas enviadas, enquanto o console do cliente exibirá as respostas recebidas do servidor para cada tipo de chamada gRPC.

Tags: gRPC Protocol Buffers java maven microsservicos

Publicado em 7-6 00:31