Stack moderna com Kotlin, Micronaut e gRPC

Quando se fala em microsserviços, muitos pensam que o ideal é adotar Java, Spring e Rest. Mas será que essa é a única opção ágil e assertiva? Nesse artigo vou mostrar como montar uma stack moderna com Kotlin, Micronaut e gRPC.

Levantou a curiosidade? Então vamos lá!

Contexto

“Quem olha para fora sonha, quem olha para dentro desperta” – Carl Jung

Nos dias de hoje, sempre que pensamos em desenvolvimento Back-End, a “sagrada trindade” dos microsserviços – Java, Spring e Rest – surgem praticamente inquestionáveis em nossas mentes e muitas vezes tratadas como uma solução óbvia e irrefutável. 

A ideia deste artigo surgiu após a realização de muito estudo e Pesquisa & Desenvolvimento (P&D) sobre como podemos modernizar a implementação de microsserviços, explorando tecnologias mais modernas e, por que não, mais poderosas.

O artigo está dividido assim:

  1. Kotlin
  2. Micronaut
  3. gRPC
  4. Mão na massa!

Vamos nessa?

1 – Kotlin

Logo do Kotlin

A linguagem Kotlin foi criada pela JetBrains e lançada em 2016, possuindo influências de JavaScript, Groovy, Scala e C#. Sim, ela é um verdadeiro Megazord.

O código compilado em Kotlin é executado na JVM (Java Virtual Machine), portanto 100% interoperável com Java. Isso significa que você poderá utilizar bibliotecas e frameworks Java tradicionais, como Spring ou Hibernate, e até mesmo gerenciadores de dependências, como Maven e Gradle, tranquilamente num projeto Kotlin.

Com a mudança adotada pelo Google, trocando Java por Kotlin no padrão de desenvolvimento Android, a linguagem acabou se popularizando no mundo Front-End. Apesar disso, Kotlin pode ser utilizado com muita facilidade também no Back-End.

A linguagem Kotlin pode ser resumida como pouco verbosa e de sintaxe simples e concisa. Como um dos pontos mais positivos, se destaca a preocupação extra com Null Safety, sendo impossível definir uma variável com valor nulo (adeus nullPointerException), a não ser que seja especificado na declaração, o que até gera um certo peso de consciência – ou não. 

Outro ponto forte do Kotlin são as Coroutines, que permitem a escrita de códigos assíncronos com menos complexidade.

A facilidade de aprendizado da linguagem Kotlin é um ponto a ser considerado em sua adoção, principalmente para desenvolvedores que já programam em Java. 

Inicialmente, a codificação se assemelha muito mais com Java, mas com tempo e experiência podemos utilizar mais dos recursos da linguagem e criarmos códigos mais idiomáticos ao Kotlin.

Curiosidade: Atualmente a Kotlin está em quarto lugar na pesquisa de linguagens mais amadas da Stackoverflow.

2 – Micronaut

Logo do Micronaut

Lançado em 2018 pela Object Computing Inc. (OCi), o Micronaut é um framework criado com foco no desenvolvimento de microsserviços e, assim como Kotlin, é baseado na JVM.

O Micronaut surgiu com uma proposta de minimizar o consumo de memória da aplicação, reduzir o tempo de startup e de ser Cloud Native. Graças a injeção de dependências e anotações sem utilização de Reflection, o Micronaut oferece suporte a execução nativa (Graal Vm).

Assim como no Spring, Micronaut entrega ao desenvolvedor uma variedade de bibliotecas para reduzir tempo e trabalho quando se trata de integração com outras tecnologias, como por exemplo, sistemas de banco de dados, soluções de cache, Kafka, Rest, entre outros.

Em comparação com Quarkus, que apresenta a mesma proposta, o Micronaut se sobressai por possuir uma documentação um pouco mais elaborada, contendo vários guias com exemplos de implementações. 

Até mesmo a curva de aprendizado será mais suave, se você estiver chegando do universo Spring, pois o Micronaut conta com abstrações que já seguem o padrão do seu concorrente, no melhor estilo “copia, mas não faz igual”.

Curiosidade: O Micronaut foi eleito ao nível de “Experimente” no radar de tecnologia da Thoughtworks em 2019.

3 – gRPC

Logo do gRPC

O gRPC é uma implementação Open Source de RPC (Remote Call Procedures) e foi desenvolvido pelo Google em 2015 com foco em comunicação de alta performance entre microsserviços. Utiliza HTTP/2 como meio de transporte de dados e Protocol Buffers como Interface Definition Language (IDL).

Ficou confuso? Calma, não fecha esse artigo ainda não que eu te explico!

HTTP/2

No HTTP/2, as conexões entre cliente-servidor são “multiplexadas”, ou seja, são bidirecionais, com um servidor recebendo/respondendo muitas solicitações ao mesmo tempo. 

Sim, apesar de parecer que não, numa chamada Rest, o servidor só pode receber/responder uma requisição por vez.

IDL (Interface Definition Language)

Como a própria tradução direta indica, a IDL nada mais é do que uma linguagem de descrição de interface. 

Uma IDL é puramente declarativa, agnóstica de linguagem e fornece as informações necessárias para a implementação de integração de um cliente com um servidor. As IDLs são comuns na comunicação de sistemas RPCs.

Protocol Buffers

Protocol Buffers (Protobuf) é a IDL criada e utilizada pelo Google para serialização de dados durante a comunicação entre microsserviços que utilizam gRPC. 

Em comparação com a comunicação via JSON ou XML, o Protobuf possui um tamanho menor, é mais rápido e eficiente. O Protobuf utiliza arquivos “.proto” que, ao serem compilados, geram uma série de implementações automaticamente (stubs).

Certo, mas o que tudo isso significa?

Significa que, ao utilizar gRPC, você terá em mãos uma solução Contract First, que a torna multi plataforma com a mesma interface de contratos. 

Além disso, será possível contar com comunicação bidirecional assíncrona e com suporte a streaming de dados. Tudo isso pensado para ser entregue com baixo Overhead, é pegar ou largar!!

Curiosidade: A letra “g” em gRPC é utilizada também como inicial do nome de cada uma das versões de liberadas pelo Google. Confira!

Agora, para não restar nenhuma dúvida vou te mostrar um exemplo de implementação passo a passo, sem mistério.

4 – Mão na massa

Como exemplo de implementação, vamos criar um projeto que utilize as três tecnologias citadas.

Criaremos dois microsserviços que se comunicam utilizando gRPC. 

O primeiro microsserviço será o servidor e deverá conter um método de cadastro fake que recebe os dados de um usuário. O segundo microsserviço funcionará como cliente e irá realizar a requisição de cadastro no primeiro microsserviço. 

Para explorarmos melhor as possibilidades da comunicação gRPC, vamos implementar dois serviços com tipos de métodos diferentes: 

  • um serviço do tipo unário, onde uma única chamada é realizada pelo cliente e o servidor devolve uma única resposta.
  • um serviço do tipo streaming, onde ambos (cliente e servidor) enviam uma sequência de mensagens utilizando um stream de leitura/escrita.

Criando o 1º microsserviço do exemplo

Para começar, vamos criar o primeiro microsserviço através da página do Micronaut para geração de projetos. Acesse a página do Micronaut.

Vamos selecionar: gRPC Application, Kotlin e Gradle Kotlin para gerenciar dependências.

Página do Micronaut com algumas opções selecionadas. Versão: 2.5.0, Linguagem: Kotlin, Build: Gradle Kotlin e Test Framework: Junit.

Em seguida, abra sua IDE de preferência – desde que seja o IntelliJ 😄 – e faça a importação do projeto.

Vamos analisar o projeto gerado, começando por onde parte da magia é configurada: build.gradle.kts.

plugins {
   id("org.jetbrains.kotlin.jvm") version "1.4.32"
   id("org.jetbrains.kotlin.kapt") version "1.4.32"
   id("com.github.johnrengelman.shadow") version "6.1.0"
   id("io.micronaut.application") version "1.5.0"
   id("org.jetbrains.kotlin.plugin.allopen") version "1.4.32"
   id("com.google.protobuf") version "0.8.15"
}

Note que o plugin protobuf, desenvolvido pelo Google, foi adicionado no arquivo.

Mais ao final do build.gradle.kts, podemos ver as configurações que serão utilizadas pelo plugin ao compilarmos o projeto.

Durante a compilação, o plugin do protobuf irá gerar classes a partir dos arquivos localizados no diretório “/src/main/proto“. Estas classes, chamadas “stubs“, poderão ser estendidas e utilizadas no seu código. Respira fundo, pois vamos ver tudo isso de perto logo mais.

Na configuração entregue pelo Micronaut Launch, os stubs serão gerados em Java. Para mantermos a idiomaticidade em Kotlin e tirarmos proveito da linguagem (Hello, coroutines!), vamos alterar algumas configurações para que o plugin faça a geração de “stubs” em Kotlin, deixando assim:

 
sourceSets {
   main {
       java {
           srcDirs("build/generated/source/proto/main/grpc")
           srcDirs("build/generated/source/proto/main/grpckt")
           srcDirs("build/generated/source/proto/main/java")
       }
   }
}
protobuf {
   protoc {
       artifact = "com.google.protobuf:protoc:3.14.0"
   }
   plugins {
       id("grpc") {
           artifact = "io.grpc:protoc-gen-grpc-java:1.33.1"
       }
       id("grpckt") {
           artifact = "io.grpc:protoc-gen-grpc-kotlin:0.2.0:jdk7@jar"
       }
   }
   generateProtoTasks {
       ofSourceSet("main").forEach {
           it.plugins {
               // Apply the "grpc" plugin whose spec is defined above, without options.
               id("grpc")
               id("grpckt")
           }
       }
   }
}

Além disso, vamos adicionar a dependência da biblioteca Kotlin que implementa gRPC e provê suporte à utilização de stubs e também a dependência da biblioteca de coroutines.

A lista completa de dependência deverá ficar assim:

 dependencies {
   implementation("io.micronaut:micronaut-runtime")
   implementation("io.micronaut.grpc:micronaut-grpc-runtime")
   implementation("io.micronaut.kotlin:micronaut-kotlin-runtime")
   implementation("javax.annotation:javax.annotation-api")
   implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
   implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}")
   implementation("io.micronaut:micronaut-validation")
   implementation("io.grpc:grpc-kotlin-stub:0.1.1")
   implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2")
   runtimeOnly("ch.qos.logback:logback-classic")
   runtimeOnly("com.fasterxml.jackson.module:jackson-module-kotlin")
   testImplementation("io.micronaut:micronaut-http-client")
}

Com o projeto configurado, vamos estruturar o nosso arquivo “.proto”, onde toda a magia gRPC toma forma.

O Micronaut Launch já criou nosso projeto com um arquivo “demoServer.proto” (o nome do arquivo segue o nome do projeto), localizado em: “/src/main/proto“.

Ele vem configurado assim:

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.example";
option java_outer_classname = "DemoServer";
option objc_class_prefix = "HLW";

package com.example;

service DemoServerService {
rpc send (DemoServerRequest) returns (DemoServerReply) {}
}

message DemoServerRequest {
string name = 1;
}

message DemoServerReply {
string message = 1;
}

As primeiras linhas iniciadas com option indicam algumas configurações básicas:

  • java_multiple_files: quando marcado com true, serão gerados arquivos separados para a estrutura de serviços e objetos definidos logo abaixo.
  • java_package: define a estrutura de pacotes para os arquivos gerados.
  • java_outer_classname: basicamente, define o nome base para as classes geradas.
  • objc_class_prefix: prefixo das classes Objective-C que serão geradas.

No “.proto” em questão, estamos definindo um serviço que irá possuir um método “send”, que receberá um objeto “DemoServerRequest” e retornará um objeto “DemoServerReply”.

Logo em seguida, definimos a composição dos objetos. Especificamos os campos, os tipos e a ordem.

Pronto. Esse é o nosso contrato. Ao compilarmos o projeto, o plugin protobuf irá gerar os stubs necessários para que possamos implementá-los. 

Quer ficar por dentro dos melhores conteúdos sobre tecnologia, inovação e carreira? Então assine hoje mesmo a Newsletters da Zup!

Mas antes, segura a ansiedade. Vamos adicionar um pouco de tempero nesta receita!

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.example";
option java_outer_classname = "DemoServer";
option objc_class_prefix = "HLW";

package com.example;

service DemoServerService {
rpc saveUser (SaveUserRequest) returns (UserResponse) {} rpc saveUserStream (stream SaveUserRequest) returns (stream UserResponse) {}

}

message SaveUserRequest {
string name = 1;
string lastName = 2;
string document = 3;

}

message UserResponse {
int32 id = 1;
string name = 2;
string lastName = 3;
}

Certo. Especificamos dois métodos para “salvar” o usuário informado: um unário (saveUser) e outro método bidirecional (saveUserStream).

Ambos recebem o mesmo objeto de entrada (SaveUserRequest), que contém na respectiva ordem: ‘name’, ‘lastName’ e ‘document’, todos do tipo string

O objeto de resposta (UserResponse) contém: ‘id’, como tipo integer, ‘name’, ‘lastName’ e ‘document’, como string.

Agora sim, podemos compilar o projeto usando o gradle wrapper que veio de “brinde” no projeto. Basta executar no console, o comando:

 ./gradlew clean build 

Note que uma pasta build foi criada na raíz do projeto e no diretório “generated/source/proto/main/grpckt” e foi gerado o stub “DemoServerGrpcKt.kt”. 

Conforme dito anteriormente, ele é baseado no “demoServer.proto” que criamos.

Caso não esteja sendo exibido os stubs no IntelliJ, você pode optar por executar os processos de “clean” e de “build” dentro da própria IDE, através do plugin gradle que já vem instalado. No canto direito é possível gerenciar a aplicação gradle facilmente.

Abaixo, é possível visualizar na imagem toda a estrutura de pastas criadas durante a compilação.

Avançando na mão na massa com Kotlin, Micronaut e gRPC

Tudo pronto para começarmos a implementar o serviço de verdade.

Vamos criar uma classe chamada “DemoServerEndpoint”, que implementa a classe abstrata “DemoServerServiceCoroutineImplBase”, contida no stub gerado.

package com.example

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import javax.inject.Singleton

@Singleton
class DemoServerEndpoint : DemoServerServiceGrpcKt.DemoServerServiceCoroutineImplBase() {
  override suspend fun saveUser(request: SaveUserRequest): UserResponse {
      return UserResponse.newBuilder()
          .setId(1)
          .setName(request.name)
          .setLastName(request.lastName)
          .build()
  }
  override fun saveUserStream(requests: Flow<SaveUserRequest>): Flow<UserResponse> =         flow {
        var id = 1
        requests.collect {
            println("Salvando usuário...")
            emit(
                UserResponse.newBuilder()
                    .setId(id++)
                    .setName(it.name)                    .setLastName(it.lastName)
                    .build()
            )            println("Concluído...")
      }
  }
}

Aqui sobrescrevemos os métodos abstratos do stub gerado.

No método saveUser, uma requisição chega e uma resposta é retornada. Simplesmente criamos um objeto do tipo UserResponse, usando como base o próprio SaveUserRequest, adicionando apenas um ‘id’ hard coded com valor “1”.

Já no método saveUserStream que foi definido como Stream no arquivo “.proto”, implementaremos utilizando Flow, que são basicamente Stremings assíncronos que dependem de Coroutines para execução. 

A Coroutine é uma solução mais simples e mais leve do que threads para a programação assíncrona. Nela é possível que uma execução seja suspensa e retornada mais tarde. Utilizaremos Flow, tanto para receber várias requisições, quanto para responder cada uma delas.

Agora podemos iniciar nosso servidor gRPC através da execução do seguinte comando:

./gradlew run

Você deverá visualizar este log:

Log do Micronaut com informação de sucesso.

Criando o 2º microsserviço do exemplo

Agora podemos criar o segundo microsserviço que funcionará como client. Com ele faremos requisições nos serviços que acabamos de criar no server. De novo, vamos utilizar o site do Micronaut para gerar o projeto. 

Página do Micronaut com algumas opções selecionadas. Versão: 2.5.0, Linguagem: Kotlin, Build: Gradle Kotlin e Test Framework: Junit.

  

Precisamos fazer as mesmas alterações que fizemos no arquivo build.gradle.kts do projeto servidor, adicionando as dependências e configurações necessárias.

O servidor que já iniciamos, ficará disponível na porta 50051 por padrão, então precisamos definir outra porta para o microsserviço que acabamos de criar. 

Além disso, vamos configurar um canal de comunicação entre nosso client e o server. Para isso, basta adicionar as informações que segue no arquivo application.yml.

micronaut:
application:
  name: demoClient
grpc:
server:
  port: 50052
channels:
  tokenApi:
    target: "http://localhost:50051"
    plaintext: true

Como já possuímos um contrato de comunicação com o servidor (demoServer.proto), não precisaremos criar um novo. Vamos copiar e colar no mesmo diretório reservado para arquivos “.proto” da nossa aplicação cliente: “/src/main/proto“.

Agora precisamos executar o comando de build para que os stubs sejam gerados.

./gradlew clean build

Finalmente podemos criar um serviço que realizará as requisições no servidor.

Vamos criar uma classe chamada “DemoService” para implementar a comunicação.

Primeiro, vamos criar um método “saveUser”, que fará a chamada no serviço unário que criamos no servidor. Vai ficar assim:

import io.grpc.Channel
import io.grpc.ManagedChannelBuilder

class DemoService {

  suspend fun saveUser() {
      val demoServerStub = createStub()

      val saveUserRequest = SaveUserRequest.newBuilder()
          .setName("Fernando")
          .setLastName("Queiroz")
          .setDocument("07284650714")
          .build()

      val saveUserResponse = demoServerStub.saveUser(saveUserRequest)

      println("Usuário registrado com id = " + saveUserResponse.id)
  }

  private fun createStub(): DemoServerServiceGrpcKt.DemoServerServiceCoroutineStub {
      val channel: Channel = ManagedChannelBuilder.forAddress("localhost", 50051)
          .usePlaintext()
          .build()

      return DemoServerServiceGrpcKt.DemoServerServiceCoroutineStub(channel)
  }
}

Note que primeiro foi necessário gerar uma instância do stub utilizando as classes geradas a partir do “.proto” do servidor e, para isso, foi informado um “Channel” com parâmetro. O channel é, assim como o “Flow“, uma implementação de coroutine.

Através do stub criado, podemos utilizar os métodos definidos no “.proto. Nesse caso, vamos chamar o método “saveUser”.

Agora vamos criar uma instância do serviço e executar uma chamada no método que acabamos de criar. Para agilizarmos o teste, vamos fazer isso dentro do método main que “sobe” a aplicação. Ele fica no arquivo “Application.kt”. 

Basta deixá-lo assim:

package com.example

import io.micronaut.runtime.Micronaut.build

suspend fun main(args: Array<String>) {

  println("Hello World!")
  val demoService = DemoService()
  demoService.saveUser()

  build()
      .args(*args)
      .packages("com.example")
      .start()
}

Executando o comando:

./gradlew run

A seguinte mensagem será exibida no log.

Hello World!
Usuário registrado com id = 1

Agora vamos para a parte mais emocionante. Criaremos o método capaz de executar e manter uma comunicação bidirecional com o servidor. Também vamos chamá-lo de “saveUserStream”, assim como definido no server.


suspend fun saveUserStream() {
   val demoServerStub = createStub()

   val requests = generateOutgoingRequests()

   demoServerStub.saveUserStream(requests).collect { response ->
       println("Resposta: " + response.id)
   }
}


private fun generateOutgoingRequests(): Flow<SaveUserRequest> = flow {

   val request1 = SaveUserRequest.newBuilder()
       .setName("Eduardo")
       .setLastName("Silva")
       .setDocument("05262438594")
       .build()

   val request2 = SaveUserRequest.newBuilder()
       .setName("Carol")
       .setLastName("Souza")
       .setDocument("07262438594")
       .build()

   val request3 = SaveUserRequest.newBuilder()
       .setName("Murilo")
       .setLastName("Oliveira")
       .setDocument("09262438594")
       .build()

   val requests = listOf(request1, request2, request3)

   for (request in requests) {
       println("Requisição: " + request.name)
       emit(request)
       delay(5000)
   }
}

Note que também foi criado o método “generateOutgoingRequest”. Ele gera 3 “SaveUserRequest” distintos e, em seguida, executa um laço for, enviando cada um dos requests, aguardando um intervalo de 5 segundos entre eles.

Voltemos no arquivo “Application.kt” para realizar a chamada no novo método e comentar a chamada anterior. Fica assim:

 package com.example

import io.micronaut.runtime.Micronaut.build

suspend fun main(args: Array<String>) {

  println("Hello World!")

  val demoService = DemoService()
//   demoService.saveUser()
  demoService.saveUserStream()


  build()
      .args(*args)
      .packages("com.example")
      .start()
} 

Executando o comando:

./gradlew run

Podemos visualizar os logs informando o que foi enviado e o que foi recebido entre as aplicações através dos dois consoles abertos:

Log final desse tutorial sobre como usar Kotlin, Micronaut e gRPC para criar uma stack moderna.

Podemos visualizar, no log do servidor (demo-server), as requisições sendo recebidas e a informação “Salvando usuário…” sendo mostrada, no momento em que um usuário é enviado pelo cliente (demo-client).

Já no log do cliente, é possível visualizar qual usuário foi informado na requisição e qual o id que foi respondido pelo servidor, sempre com intervalo de 5 segundos entre uma e outra.

The End!

É isso!

Após a leitura deste artigo espero que você sinta segurança para, no mínimo, validar se a sua stack de desenvolvimento atual realmente é pensada e implementada para solucionar o problema em questão ou se ela apenas surfa na onda da tecnologia default de mercado.

Caso tenha ficado alguma dúvida, sinta-se à vontade para me chamar! Além disso, os comentários estão aqui para isso também. 😉

See ya!

Referências:

capa do artigo Stack moderna com Kotlin, Micronaut e gRPC
Foto - Fernando Eduardo de Queiroz
Backend Developer
Curioso que gosta de aprender e descobrir fatos sobre qualquer assunto. Quando não estou programando estou pedalando de mountain bike.

This website uses cookies to ensure you get the best experience on our website. Learn more