Começando com gRPC: Criando sua primeira aplicação utilizando Micronaut

Tony Augusto
7 min readFeb 12, 2021

--

— "Para quem só sabe usar martelo, todo problema é um prego.” Abraham Maslow

É inegável que a tecnologia evolui muito rápido e nós, desenvolvedores, devemos nos esforçar muito para conseguir nos manter atualizados e prontos para resolver problemas da melhor forma possível. Hoje tratarei sobre o RPC, mais precisamente framework gRPC e como ele pode se tornar uma opção aos serviços REST comuns.

Ferramentas utilizadas:

  • Micronaut 2.3.0;
  • Java 11;
  • Gradle 6.7.

Do que se trata nosso exemplo?

Usaremos um CRUD simples de produtos para demonstrar os conceitos mais básicos do gRPC e como aplicá-los. O intuito é demonstrar o funcionamento e estimular a pesquisa na documentação oficial para obter mais detalhes.

Passo 1: Criando seu projeto Micronaut gRPC

Você, neste exato momento, enquanto pula os tópicos caçando o código

Bom devs, vamos começar do começo. Antes de tudo precisamos criar o nosso projeto utilizando o launch do Micronaut (parecido com o amigo da vizinhança Spring Initializr). Nosso exemplo está configurado da seguinte forma:

Bem fácil, não?

Passo 2: Definindo nosso arquivo .proto

Conforme a documentação do gRPC o .proto é um arquivo de texto comum em que definimos nossas mensagens que por sua vez são pequenos registros lógicos de informações contendo pares nome-valor chamados campos. Porém, somente as mensagens não são suficientes para nós, precisamos assim definir os nossos serviços gRPC. Dado que nosso exemplo será um CRUD, definiremos um serviço para cada um deles:

syntax = "proto3";option java_multiple_files = true;
option java_outer_classname = "GrpcProductServer";

package br.com.tony.grpc.example;

service GrpcServerService {
rpc CreateProduct(CreateProductInput) returns (ProductOutput) {}
rpc DeleteProduct(ProductId) returns (EmptyResponse) {}
rpc FindProductById(ProductId) returns (ProductOutput) {}
rpc UpdateProduct(UpdateProductInput) returns (EmptyResponse) {}
}

message EmptyResponse {}

message ProductId {
int32 id = 1;
}

message UpdateProductInput {
int32 id = 1;
string name = 2;
double price = 3;
}

message CreateProductInput {
string name = 1;
double price = 2;
}

message ProductOutput {
int32 id = 1;
string name = 2;
double price = 3;
}

Como você pode ver, cada campo na definição da mensagem possui um número exclusivo. Esses números de campo são usados ​​para identificar seus campos no formato binário da mensagem e não devem ser alterados depois que seu tipo de mensagem estiver em uso. Fonte: Assigning Field Numbers

Pronto! Definimos as mensagens e passamos como parâmetro de entrada e retorno do serviço gRPC. A lista dos tipos de campos disponíveis você pode consultar no Guia da linguagem proto3.

Passo 3: Gerando as classes e implementando os serviços gRPC

Agora que definimos nosso .proto, utilizaremos o protoc para gerar as classes de acesso a dados na nossa linguagem escolhida (Java). Nas opções de build do Gradle execute a task chamada generateProto dentro de other. Após executar a task, o arquivo .proto dará origem a classes Java que serão utilizadas para atendermos ao serviço gRPC que queremos prover.

No projeto, criaremos um pacote chamado resource (ou controller) e dentro dele uma classe ProductResource, imagine que essa classe representa para o gRPC algo similar a sua classe de rotas POST, GET etc de serviços REST. Agora faremos com que essa classe estenda da classe Java gerada pelo protoc e devemos sobescrever os métodos (serviços) adicionando a nossa implementação própria. A classe inicialmente está assim:

A anotação @Singleton é utilizada pelo mecanismo de injeção de dependências do Micronaut e o @Inject indica que uma instância de ProductService será injetada para uso do ProductResource.

Você também deve ter observado, por exemplo, no método createProduct o parâmetro StreamObserver<GrpcProductServer.ProductOutput> responseObserver. Mas o que seria esse tal de StreamObserver? Trata-se de uma interface disponível no pacote io.grpc.stub, cuja documentação você pode consultar aqui, e que trás consigo a assinatura de 3 métodos, sendo:

void onNext(V value);
void onError(Throwable t);
void onCompleted();
  • void onNext(V value) recebe um valor do fluxo. Será por ele que repassaremos os valores das nossas respostas gRPC.
  • void onError(Throwable t) recebe um erro de encerramento do fluxo e o utilizaremos para retornar os erros no fluxo.
  • void onCompleted() recebe uma notificação de conclusão de fluxo bem-sucedida e invocamos após o onNext para notificar o client do sucesso do fluxo.

Esses métodos são muito importantes, portanto guarde as informações sobre eles e sempre consulte a documentação oficial para obter mais detalhes.

3.1: Implementando o serviço createProduct

Chegou o momento de implementar os nossos serviços gRPC. Neste ponto não entrarei em detalhes das camadas service, repository etc pois o intuito é focarmos no gRPC, mas não se preocupe pois todo o código está disponível aqui. O nosso serviço de criar produtos está atualmente dessa forma:

@Override
public void createProduct(CreateProductInput request, StreamObserver<ProductOutput> responseObserver) {
super.createProduct(request, responseObserver);
}

O primeiro passo para criar um produto é obter as informações enviadas pelo client e para isso nosso método tem o parâmetro request que trás consigo todas as infos da requisição. Neste exemplo, a nossa camada de service recebe ProductInputDTO e retorna ProductDTO no método de criar produtos, ficando essa parte da seguinte forma:

ProductDTO productDTO = this.productService.createProduct(
ProductInputDTO.builder()
.name(request.getName())
.price(request.getPrice())
.build());

Bom, os atributos do nosso produto recém criado já estão no ProductDTO, agora precisamos construir a resposta StreamObserver<ProductOutput> e para isso utilizaremos o builder do ProductOutput, passando para a resposta as informações contidas no nosso ProductDTO:

ProductOutput response = ProductOutput.newBuilder()
.setId(productDTO.getId().intValue())
.setName(productDTO.getName())
.setPrice(productDTO.getPrice())
.build();

Com a resposta pronta podemos finalizar o fluxo de sucesso com:

responseObserver.onNext(response);
responseObserver.onCompleted();

Com isso o nosso serviço já está funcional para criação de produtos. Entretanto, adicionei uma validação para devolver uma exceção do tipo ProductAlreadyExistsException caso um produto com o mesmo nome já esteja cadastrado no sistema, como capturá-la e enviá-la para o client? Podemos utilizar o void onError(Throwable t). Para isso, envolveremos o nosso código dentro de um try/catch:

try {
// Código de sucesso em criação de produto.
} catch (ProductAlreadyExistsException e) { responseObserver.onError(Status.ALREADY_EXISTS
.withDescription(e.getMessage()).asRuntimeException());
}

O Status fornecido dentro do responseObserver.onError vem do pacote io.grpc. Desta forma, ao enviarmos um nome duplicado o client receberá um erro mais amigável como retorno. O nosso código completo deste método ficou assim:

Testando o serviço createProduct

Agora já podemos realizar o nosso primeiro teste! Utilizarei o Insomnia para realizar as chamadas gRPC locais, mas temos outras boas opções como o BloomRPC. Caso ainda não tenha o feito, você também pode baixar o código fonte do projeto no meu GitHub. Para testar o serviço, importe o arquivo .proto e defina o IP/porta onde o serviço está executando, que no meu caso é 0.0.0.0:50051.

Criação de um produto com sucesso.

Acima temos a resposta em caso de sucesso. Abaixo veremos o que acontece em caso de produtos duplicados:

Erro ao criar produto com mesmo nome.

Feito! Dessa forma o nosso serviço de criação de produtos já está implementado! Vamos aos próximos serviços.

3.2: Implementando o serviço deleteProduct

Esse serviço é provavelmente o mais simples de implementar dadas as nossas regras de negócio atuais. Caso o client forneça um id que não seja encontrado pelo banco retornaremos um ProductNotFoundException. O método final ficará desta forma:

O único ponto de atenção é que a nossa resposta é do tipo EmptyRequest, então a construimos sem nenhum parâmetro:

EmptyResponse response = EmptyResponse.newBuilder().build();

Pronto!

Testando o serviço deleteProduct

No Insomnia, o caso de sucesso fica da seguinte forma:

Sucesso ao excluir um produto

E caso o id não exista:

Erro ao excluir um produto

3.3: Implementando o serviço findProductById

Assim como na exclusão de um produto, na busca por ID podemos retornar um ProductNotFoundException em casos onde não exista um produto para o id passado pelo client.

A construção da resposta é bem parecida com a que fizemos na criação de um produto:

ProductOutput response = ProductOutput.newBuilder()
.setId(productDTO.getId().intValue())
.setName(productDTO.getName())
.setPrice(productDTO.getPrice())
.build();

O método completo ficará da seguinte forma:

Testando o serviço findProductById

No Insomnia, o caso de sucesso fica da seguinte forma:

Sucesso ao buscar um produto

E caso o id não exista:

Sucesso ao buscar um produto

3.4: Implementando o serviço updateProduct

A atualização de um produto no nosso exemplo supõe que um objeto produto completo será enviado (similar ao PUT do REST) e devemos passar as informações de atualização para o método updateProduct:

this.productService.updateProduct(ProductDTO.builder()
.id((long) request.getId())
.name(request.getName())
.price(request.getPrice())
.build());

Na camada service foram adicionadas validações para ProductNotFound e ProductAlreadyExistsException e em caso de sucesso uma EmptyResponse será retornada. O código final do nosso serviço ficou assim:

Testando o serviço updateProduct

O caso de sucesso ficará assim:

Sucesso ao atualizar um produto.

Caso ID seja válido, mas o nome já existe no banco de dados este será o resultado:

Erro por nome duplicado

Caso ID seja inválido:

Erro produto não encontrado.

Pronto!

Com isso finalizamos todos os nossos serviços de produtos! As referências estão logo abaixo.

Código do projeto: Exemplo Micronaut e gRPC

Referências:

--

--

Tony Augusto

Desenvolvedor back-end Java e uma pessoa que acredita na propagação do conhecimento