Micronaut: Utilizando Protobuf para serialização e desserialização dos dados enviados ao RabbitMQ

Tony Augusto
7 min readMar 27, 2021

--

Sistemas de mensageria não são uma novidade para aqueles que estão acostumados a implementar serviços baseados na arquitetura de microsserviços. Entretanto, se você utiliza gRPC em suas comunicações, já pensou em utilizar o Protobuf para serialização/desserialização dos dados enviados ao RabbitMQ? Neste artigo demonstrarei o seu uso! Nossa stack será:

  • Micronaut 2.4.0;
  • Java 11;
  • Gradle 6.8.

Fluxo do nosso exemplo

Exemplo básico de um fluxo de compras.

Construí uma implementação desse fluxo e o código está disponível no meu GitHub, neste artigo utilizarei como exemplo a comunicação entre ShoppingCart Microsservice e Payment Microsservice.

O que é Protobuf?

Em sua documentação oficial o Protobuf é definido como “mecanismo extensível de linguagem e plataforma neutras, utilizado para serializar dados estruturados”, ou seja, ele é agnóstico à linguagem e utiliza como base arquivos de contrato em formato próprio, que possuem a extensão .proto. Em resumo, para trabalhar com o Protobuf, precisamos seguir basicamente três etapas:

  • 1º Definimos a estrutura das nossas mensagens e serviços dentro do nosso arquivo .proto;
  • 2º Utilizamos o compilador do Protobuf (protoc) para gerar classes de acesso na linguagem que você desejar, por exemplo, Java, C++ etc;
  • 3º A partir dessas classes preenchemos, recuperamos e serializamos as mensagens definidas no .proto.

Para uma exemplificação mais completa e detalhada do conteúdo acima, sugiro fortemente a consulta da documentação oficial do gRPC e do Protobuf, além deste artigo: Protobuf — Uma alternativa ao JSON e XML.

Bom, agora que sabemos que o Protobuf é utilizado para serialização/desserialização de dados estruturados, partiremos para a ideia central do artigo que é utilizá-lo nessa função em detrimento do Jackson.

Definindo o nosso arquivo .proto das mensagens trocadas entre os serviços ShoppingCart e Payment Microsservice

O primeiro passo para utilizar o Protobuf é definir a estrutura dos dados que queremos utilizar nas comunicações, portanto, vamos definir um arquivo .proto com a estrutura das mensagens que o ShoppingCart Microsservice deseja entregar ao RabbitMQ e que serão consumidas pelo Payment Microsservice:

syntax = "proto3";

option java_multiple_files = true;
option java_package = "br.com.tony.shoppingcart";

package br.com.tony.shoppingcart;

message PaymentRequest {
int64 orderId = 1;
int64 clientId = 2;
string clientName = 3;
string cardNumber = 4;
double amount = 5;
}

message PaymentResponse {
int64 orderId = 1;
int64 clientId = 2;
string status = 3;
}

Temos duas mensagens, o PaymentRequest que define uma solicitação de pagamento e o PaymentResponse que traz consigo o orderId e o clientId para identificar a qual cliente e pedido se refere aquele status recebido. Este arquivo .proto dará a duas classes, sendo PaymentRequest e PaymentResponse. Demonstrarei a implementação do serialização/desserialização somente da PaymentRequest pois os métodos sobrescritos são exatamente os mesmos, alterando somente o tipo do argumento da interface RabbitMessageSerDes<T> do Micronaut.

Sobrescrevendo a implementação padrão de serialização/desserialização do Micronaut

Por padrão o Micronaut oferece suporte a serialização/desserialização no formato JSON com o Jackson, mas na documentação do framework é informado que se registrarmos um bean do tipo RabbitMessageSerDes ele será utilizado em detrimento do padrão. Então, vamos implementar a nossa própria RabbitMessageSerDes que usa o Protobuf ao invés do Jackson.

Criando o nosso bean PaymentRequestSerializerDeserializer

Como o próprio nome da classe define, este bean será responsável por serializar/desserializar a nossa mensagem do tipo PaymentRequest. A implementação final é bem simples, veja:

Observe que a interface RabbitMessageSerDes<T> recebe um parâmetro do tipo genérico T, que no nosso caso serão as classes geradas pelo protoc a partir do nosso arquivo .proto, como estamos implementando o bean do PaymentRequest, passaremos esta classe como parâmetro. A interface RabbitMessageSerDes define três métodos que devemos implementar:

Primeiro método: T deserialize(RabbitConsumerState consumerState, Argument<T> argument) ;

A implementação desse método será responsável por desserializar as mensagens que o RabbitMQ obtém da fila, ele retorna um tipo T que será o que informarmos como parâmetro da interface RabbitMessageSerDes<T>. Como utilizaremos o Protobuf para serializar as mensagens, naturalmente precisamos utilizá-lo para desserializar. A classe PaymentRequest gerada pelo compilador protoc possui vários métodos estáticos padrão, dentre eles o que recebe um array de bytes e faz o parse. Como estamos no método de desseriazação, utilizaremos o parseFrom que recebe um array de bytes como parâmetro, conforme linha 19 do código da nossa implementação:

PaymentRequest.parseFrom(consumerState.getBody());

Perceba que o consumerState.getBody() faz parte da implementação do Micronaut para o RabbitMQ e retorna um array de bytes com o corpo da mensagem recebida, no nosso caso esse array de bytes é resultado da serialização que explicarei logo adiante, portanto, ao fazermos o parseFrom desses dados iremos obter uma PaymentRequest com todos aqueles campos que definimos na mensagem no arquivo .proto.

Importante: tenha em mente que o arquivo .proto é uma espécie de contrato entre os serviços, portanto, para a correta serialização/desserialização das informações, todos os serviços envolvidos na comunicação devem possuir a mesma versão do .proto, caso algum campo da mensagem seja diferente o parse irá retornar dados incorretos.

Segundo método: byte[] serialize(T data, MutableBasicProperties properties);

Este método é responsável por fazer a serialização dos dados da mensagem, ele recebe dados do tipo genérico T, que no nosso caso é uma PaymentRequest. Para convertermos esses dados em um array de bytes utilizaremos um método que a PaymentRequest herdou da superclasse AbstractMessageLite.

data.toByteArray();

Terceiro método: boolean supports(Argument<T> type);

Por último, mas não menos importante, conforme a documentação da interface este método “determina se o serdes suporta o tipo fornecido”. Mas como ele verifica isso? No nosso caso é através do método boolean isAssignableFrom(Class<?> cls); que faz parte do pacote java.lang e “determina se a classe ou interface representada por este objeto Class é igual ou é uma superclasse ou superinterface da classe ou interface representada pelo parâmetro Class especificado”. Pode parecer um pouco confuso, mas veja a implementação do método:

@Override
public boolean supports(Argument<PaymentRequest> type) {
return type.getType().isAssignableFrom(PaymentRequest.class);
}

Se lermos a implementação fica bem mais fácil entender. O método retorna true se o argumento type for igual ou superclasse/superinterface da classe PaymentRequest.class gerada pelo protoc. Isso serve para verificar se há uma implementação de serialização/desserialização capaz de trabalhar com a nossa PaymentRequest.

E como fica o nosso publisher e consumer RabbitMQ?

O Micronaut facilita muito a criação do publisher e consumer das filas RabbitMQ. No nosso caso, lembre-se que alteramos somente a forma de serialização/desserialização dos dados, portanto a única alteração é no tocante aos tipos recebidos. Por exemplo, veja como está o nosso PaymentProcessorQueuePublisher que faz parte do serviço ShoppingCart e envia a requisição de pagamento a ser consumida pelo Payment:

Observe que ele está recebendo um PaymentRequest como parâmetro de dado a ser enviado para o broker do RabbitMQ.

O mesmo ocorre com a implementação que consome as requisições de pagamento e está no Payment Microsservice. Observe:

Mais informações sobre o suporte do Micronaut ao RabbitMQ podem ser encontradas na documentação do Micronaut RabbitMQ.

Testando nossa implementação

Show me the code!

O código do exemplo que construí está disponível no meu GitHub. No readme do repositório há um passo a passo de como executá-lo localmente e não tratarei disso aqui pois ficaria redundante. A partir de agora irei supor que você está com o exemplo do repositório em execução na sua máquina ou que simplesmente você está testando no seu próprio usecase.

Enviando uma requisição

Utilize um client gRPC como o Insomnia, importe o arquivo order-service.proto que está no diretório shopping-cart-service/src/main/proto, configure a porta gRPC como 0.0.0.0:50051 e selecione o método order:

Seguindo nosso fluxograma, ao receber uma requisição o ShoppingCart Microsservice coleta as informações pertinentes e envia ao nosso Payment Microsservice uma PaymentRequest. Portanto, vamos observar o fluxo acompanhando os logs que serão impressos nos terminais de execução desses dois serviços.

Log no terminal do ShoppingCart Microsservice ao enviar uma PaymentRequest ao Payment Microsservice:

ShoppingCart Microsservice

Marquei em amarelo o log relacionado ao envio da PaymentRequest para facilitar a visualização. Observe que a requisição possui todas as informações que definimos no nosso .proto, por se tratar de um caso de estudos estou imprimindo dados sensíveis no log, mas saiba que todos são fictícios e o cardNumber, apesar de não estar impresso, está na requisição.

Log no terminal do Payment Microsservice ao receber, processar e enviar uma reposta sobre uma PaymentRequest:

Payment Microsservice

O Payment Microsservice fica “escutando” a fila de solicitações de pagamento e simula um processamento ao receber uma mensagem. Na penúltima linha o log informa que uma PaymentResponse está sendo enviada para o broker para ser lida pelo nosso ShoppingCart Microsservice.

Log no terminal do ShoppingCart Microsservice ao receber uma PaymentResponse do Payment Microsservice:

ShoppingCart Microsservice

--

--

Tony Augusto

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