Micronaut x Spring Boot: Uma análise comparativa de desenvolvimento, desempenho e consumo de recursos.

Introdução

O Spring domina a área de aplicações web desenvolvidas com Java devido a facilidade e agilidade no desenvolvimento, graças a grande quantidade de módulos existente no framework.

No entanto, o Spring deixa a desejar em algumas questões como o consumo de recursos e tempo de startup da aplicação. Esses fatores negativos têm um peso significativo se pensarmos em aplicações que utilizam IaaS (Infrastructure as a Service), como AWS, Azure, Google Cloud, dentre outras, uma vez que essas plataformas cobram pela quantidade de recursos consumidos.

Será que vale abrir mão da facilidade de desenvolvimento que o Spring provê para adotar soluções que consomem menos recursos e têm um menor tempo de startup? E se existir uma solução tão simples quanto o Spring mas que apresentem um melhor desempenho?

Objetivo

Este artigo tem como objetivo realizar uma análise comparativa entre o Spring Boot e o Micronaut, através de um experimento onde foram criadas três versões de uma mesma API simples, com apenas dois endpoints, onde uma versão utiliza Spring Boot, outra versão utiliza Micronaut e a última versão também utiliza o Micronaut, mas será compilada para native.

A comparação entre os frameworks será feita em duas partes, na primeira parte serão abordados aspectos relativos ao desenvolvimento, comparando como as APIs foram criadas e mostrando as semelhanças entre o Spring e o Micronaut a nível de código. Na segunda fase serão comparados aspectos relativos a desempenho, como tempo de startup das aplicações, consumo de memória, tamanho final dos executáveis e tempo de build para cada projeto.

Por fim serão apresentados os resultados do estudo comparativo e, como nem tudo tem apenas aspectos positivos, também serão apresentados os problemas enfrentados durante a realização deste experimento.

Descrição da API

Para este experimento, a API que será implementada terá apenas os seguintes endpoints:

GET /greetings
GET /greetings/{name}

O response terá o seguinte formato:

{
message: String,
date: Long
}

Onde o campo message irá retornar uma mensagem simples de saudação e o campo date terá o timestamp no qual a mensagem foi gerada.

As aplicações criadas nesse experimento terão os mesmo artefatos. Uma classe de domínio chamada Greeting que será utilizada para gerar o response. Para mapear os endpoints citados anteriormente, será utilizada a classe GreetingsController que receberá, através de injeção de dependência, a classe GreetingsService, responsável por montar o objeto Greetings, que será retornado ao controller.

Criação dos projetos

Antes de iniciar a implementação dos três projetos (Spring Boot, Micronaut e Micronaut compilado para nativo) é necessário criar os projetos, e cada um deles tem algumas particularidades.

O primeiro projeto criado será o projeto utilizando Spring Boot. Para isso, será utilizado o Spring Initializr.

Basta clicar no botão “Generate Project” para realizar o download do projeto e então executar o seguinte comando no terminal, na raiz do projeto, para instalar as dependências.

$ mvn clean compile

Criado o primeiro projeto, agora é necessário criar os dois projetos do Micronaut. Para esses projetos será utilizado o Micronaut CLI. Caso ainda não tenha este CLI instalado.

Para criar o projeto do Micronaut rodando na JVM, utilize o seguinte comando no terminal

$ mn create-app com.jvoliveira.greetings-micronaut -b maven

Este comando irá criar uma pasta “greetings-micronaut” contendo os arquivos do projeto. Acesse esta pasta e execute o mesmo comando utilizado anteriormente no projeto Spring para instalar as dependências.

Por fim, resta a criação do projeto do Micronaut compilado para nativo. Da mesma forma que no projeto Micronaut para JVM, utilizaremos o Micronaut CLI para criação do projeto. O comando para este projeto será:

$ mn create-app com.jvoliveira.greetings-micronaut-native -f graal-native-image -b maven

Para instalar as dependências, basta utilizar o mesmo comando do maven utilizado nos projetos anteriores.

Implementação da API

Para comparar o desenvolvimento de uma API utilizando Spring e outra utilizando Micronaut, está seção irá apresentar o desenvolvimento das aplicações de forma paralela, começando pelo arquivo de configuração da aplicação (application) e depois implementando as classes Greetings, GreetingsService GreetingsController.

Para esta seção será abordada apenas a comparação entre Spring e Micronaut, visto que não existe diferença entre a implementação do Micronaut para JVM e native. A diferença entre ambos será apresentada mais a frente.

A configuração dos projetos será feita através do arquivo application, que existe tanto no projeto Spring quanto nos projetos Micronaut. No Spring será utilizada a extensão .properties e no Micronaut a extensão .yml para este arquivo. Ambos os projetos aceitam essas duas extensões.

O arquivo abaixo está sendo utilizado para configurar a aplicação Spring:

E este é o arquivo de configuração do projeto Micronaut:

Ambos os projetos foram configurados para serem executados na porta 8085 e possuem a propriedade greetings.message.default, uma propriedade específica para a aplicação que está sendo criada.

O desenvolvimento da classe de domínio Greetings não difere entre as aplicações Spring e Micronautjá que basicamente é um POJO contendo dois campos, a mensagem e o timestamp referente a data de criação da mensagem.

A próxima classe a ser criada será o GreetingsService, que será um bean responsável por construir o objeto Greeting. A implementação abaixo foi feita para o projeto Spring.

Nesta implementação, a classe GreetingsService foi definida como um bean através da anotação Service, um estereotipo da anotação Component indicando que este bean faz parte da camada de negócio da aplicação. Este bean possui a propriedade defaultMessage, que através da anotação Value recebe o valor definido na propriedade greetings.message.default, definida no arquivo application.properties. O restando do código é apenas código Java para construir uma instância do objeto Greetings.

A classe GreetingsService foi implementada no projeto Micronaut da seguinte forma:

A implementação da classe GreetingsService no projeto Micronaut difere do projeto Spring apenas pela anotação utilizada para definir esta classe como um bean. Ao invés de utilizar a anotação Service do Spring, é utilizada a anotação Singleton, que intuitivamente faz com que esse bean tenha uma única instância, criada pelo Micronaut, já que se trata de um singletonO restante do código não difere em nada da implementação para o projeto Spring, nem mesmo a forma como obter valores definidos no arquivo de configuração da aplicação, através da anotação Value.

Por fim, resta a implementação da classe GreetingsController, que será responsável por mapear as duas rotas existentes na aplicação. A implementação abaixo foi utilizada no projeto Spring.

A anotação RestController faz da classe GreetingsController um bean através deste estereotipo, indicando que se trata de um controller para uma API Rest. Esta classe tem como dependência o GreetingsService, que é injetado no construtor deste controller. Essa injeção de dependência é realizada de forma implícita pelo Spring, mas pode ser feita de forma explícita utilizando a anotação Autowired no construtor ou diretamente no atributo service. Além do construtor, existem dois outros métodos responsáveis por mapear as duas rotas existentes na API. O mapeamento é realizado pela anotação GetMapping que recebe como argumento o endpoint desejado. O retorno do método é um objeto do tipo ResponseEntity que permite, além de definir o corpo da response, também permite definir status, cabeçalhos, dentre outras informações.

Para o projeto greetings-micronaut a implementação da GreetingsController é apresentada abaixo:

A implementação do GreetingsController é quase idêntica ao projeto Spring, a diferença fica para a anotação Controller, que também existe no Spring. Além disso a anotação que mapeia os endpoints no Micronaut é mais simples e intuitiva do que no Spring, sendo composta apenas pelo verbo HTTP utilizado, neste caso foi utilizada a anotação Get. A última diferença encontrada fica para o objeto de resposta, que no Spring temos o ResponseEntity e no Micronaut pode ser substituído pelo objeto HttpResponse.

Da mesma forma que no Spring, o Micronaut realiza a injeção de dependência de forma implícita, sem precisar declarar explicitamente uma anotação para tal. Também é possível realizar a injeção de dependência de forma explícita, através da anotação Inject, seguindo a especificação JSR-330, também conhecida como CDI ou Context and Dependency Injection.

Para executar a aplicação greetings-spring via terminal, o comando utilizado é:

$ mvn spring-boot:run

E para o projeto greetings-micronaut, o comando utilizado é:

$ mvn exec:exec

Atenção: Ambas as aplicações foram configuradas para escutar a porta 8085. Iniciar ambas as aplicações simultaneamente, sem modificar as configurações de porta realizadas previamente, irá causar erro ao subir as aplicações devido ao conflito de portas.

Configurações específicas para Micronaut Native

O processo de compilação do Micronaut para native tem algumas particularidades se comparado com os outros dois projetos executados na JVM, que podem ser facilmente iniciados com o auxílio do Maven. Aqui segue um guia oficial de como compilar a aplicação para código nativo, já que isto requer uma JVM específica (GraalVM) e algumas outras configurações auxiliares.

Para gerar um executável nativo, já tendo instalado a GraalVM e o native-image (recomendo o SDKMAN para isso), basta executar o seguinte comando na raiz do projeto

$ native-image — no-server -cp target/greetings-micronaut-native-*.jar

O comando acima irá gerar o executável greetings-micronaut-native. Para iniciar a aplicação, basta executa-lá da mesma forma que um app nativo para o SO utilizado.

$ ./greetings-micronaut-native

Comparando desempenho e consumo de recursos

Sabendo que ambos os projetos, Spring e Micronaut, fazem exatamente a mesma coisa, é possível realizar alguns testes de desempenho e consumo de recursos de forma mais justa e assertiva.

Startup

O primeiro comparativo será com o tempo de startup. Iniciando cada uma das aplicações via terminal, o tempo de startup apresentado foi:

Startup da aplicação greetings-spring, desenvolvida com Spring Boot
Startup da aplicação greetings-micronaut, desenvolvida com Micronaut para JVM
Startup da aplicação greetings-micronaut, desenvolvida com Micronaut compilada para código nativo

Neste primeiro comparativo, a aplicação desenvolvida com Micronaut para JVM tem um tempo de startup de 6,3 segundos, enquanto a aplicação desenvolvida com Spring inicializou depois de 16,5 segundos. Já a aplicação Micronaut compilada para nativo inicia em apenas 108 milisegundos.

App size

O próximo comparativo realizado é sobre o tamanho dos executáveis de cada aplicação. Tanto a aplicação Micronaut para JVM quanto a aplicação Spring geram um arquivo jar como executável, enquanto o Micronaut compilado para nativo gera uma executável de acordo com o SO utilizado para compilar o projeto.

Tamanho do executável para o projeto greetings-spring
Tamanho do executável para o projeto greetings-micronaut, executado na JVM
Tamanho do executável para o projeto greetings-micronaut-native, compilado para nativo e executado fora da JVM.

O executável gerado para o projeto greetings-spring, utilizando Spring Boot, possui 16MB de tamanho. Já o projeto greetings-micronaut sendo executado na JVM possui 12MB de tamanho. Por fim, o projeto greetings-micronaut-native, compilado para código nativo, possui 53MB de tamanho.

Tempo de Build

Esse aspecto avalia o tempo necessário para compilar o projeto e gerar um executável da aplicação.

Para calcular o tempo de build dos projetos Spring e Micronaut para JVM, basta utilizar o comando abaixo:

$ mvn clean package

Este comando gera um executável da aplicação e informa o tempo desse processo.

Para a aplicação Micronaut Native o comando utilizado é:

$ native-image — no-server -cp target/greetings-micronaut-native-*.jar

Seguem os resultados obtidos referente ao tempo de build de cada projeto:

Tempo de build para o projeto greetings-spring, utilizando Spring Boot
Tempo de build para o projeto greetings-micronaut, utilizando Micronaut compilado para rodar na JVM
Tempo de build para o projeto greetings-micronaut-native, utilizando Micronaut compilado para rodar nativamente.

Consumo de memória e CPU

Para esse comparativo serão criadas três imagens docker diferentes, uma para cada aplicação, utilizando a mesma imagem base. O Dockerfile para o projeto greetings-micronaut-native já é criado no momento da criação deste projeto, ao adicionar a dependência graal-native-image.

Após criar a imagem de cada projeto, é possível iniciar um container para cada nova imagem criada. Através do comando abaixo, é possível ver os recursos consumidos por cada container em execução:

$ docker stats 

O consumo de recursos para os containers em execução pode ser visto na imagem abaixo:

Consumo de recurso dos três containers em execução simultaneamente.

Na imagem acima os containers estão em execução mas as aplicações não estão recebendo requisições, apenas foram iniciadas.

O primeiro container da lista, seguindo a ordem de cima para baixo, está executando a aplicação criada com Spring Boot e está consumindo 64MB de memória e 0.79% de CPU. O container seguinte está executando a aplicação Micronaut rodando na JVM e consumindo 45MB de memória e 0.06% de CPU. Por último, a aplicação desenvolvida com Micronaut compilado para código nativo, consumindo 10MB de memória e 0.01% de CPU.

Resultados

Comparando os dois frameworks, Micronaut e Spring Boot, os dois apresentam várias semelhanças em termos de código. Em ambos os frameworks é fácil iniciar um novo projeto e desenvolver uma API, permitindo que desenvolvedores já ambientados ao framework da Pivotal não sintam dificuldades ao se adaptar com o Micronaut. Algumas anotações serão um pouco diferentes, mas nada que exija muito de quem já conhece e utiliza o Spring.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *