Usando Native Images em projetos Spring Boot com GraalVM
Antes de começarmos, é preciso entender que compilamos nossas aplicações como Just In Time(JIT) ou Ahead Of Time(AOT). Para compilação usando Native Images é utilizado a compilação AOT.
Nesse artigo não iremos tratar sobre JIT. Para isso, recomendo ler o artigo do Baeldung sobre JIT.
O que é compilação Ahead Of Time(AOT)?
É basicamente a conversão de um código escrito em uma linguagem de alto nível, para linguagem de máquina específica da arquitetura do processador, antes da execução do programa. Isso permite melhorar a performance da aplicação, principalmente na inicialização. Essa técnica habilita a VM a produzir um código super optimizado, melhorando o tempo da primeira execução(warming-up period), redução de uso de recursos e até melhorando a segurança da aplicação, por exemplo.
Outra vantagem de programas compilados com AOT é que eles podem ser executados sem a necessidade de uma Java Virtual Machine (JVM).
Trade-off
Para o AOT existem alguns trade-offs que precisam ser analisandos antes de começarmos a utilizar imagens nativas:
- Para a compilação AOT gerar essa otimização, a aplicação é compilada na mesma arquitetura da máquina onde a imagem nativa foi gerada. Para outras arquiteturas, a imagem nativa precisa ser regerada.
- O tamanho da aplicação fica um pouco maior, porque o código compilado AOT é normalmente maior do que o código fonte original.
- Como a aplicação é previamente compilada, pode ser necessário algumas configurações adicionais para recursos como reflections e arquivos de configuração, para serem inclusos na build da imagem nativa.
Usando GraalVM
Para conseguimos compilar nossa aplicação usando AOT vamos usar GraalVM.
Vamos ao código!
Vamos criar uma aplicação Spring Boot simples, com um endpoint que retornará um Hello, World! e iremos gerar essa aplicação usando Native Images.
Vamos no start.spring.io e vamos gerar um novo projeto conforme imagem abaixo:
Para o nosso exemplo, precisaremos das dependências GraalVM Native Support, Spring Web e Java 22(por que não?!). Caso não tenha utilizado ainda, mas queira saber algumas mudanças no Java 22, acesse: Java 22 foi lançado! Veja algumas das novas features.
Agora iremos criar uma @Controller
que retorne um Hello, World!:
@RestController
public class HelloWorldController {
@GetMapping
public String hello() {
return "Hello, World!";
}
No pom.xml
da aplicação podemos perceber que tem o plugin native-maven-plugin
, que será responsável por gerar a imagem nativa para nós. Iremos apenas modificar o plugin adicionando a configuração buildArgs:-Ob
para aumentar a velocidade da geração da imagem. recomendado apenas para tempo de desenvolvimento, ficando conforme abaixo:
<project>
...
<build>
<plugins>
...
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<buildArgs>
<!-- Speed up native builds for development purposes only -->
<buildArg>-Ob</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>
Gerando imagem nativa
Para gerar a imagem nativa iremos executar o comando no terminal:
$ mvn -Pnative native:compile
Após executar esse comando várias analises serão feitas antes da geração da imagem nativa, como:
- Analisar todas as classes utilizadas, a partir da classe Main.
- Analise estática instanciando essas classes utilizadas e adicionando-as ao Heap.
- É criado um Snapshot do Heap e depois esses objetos são removidos do Heap.
- Em seguida todo o código é compilado em tempo de build, de forma nativa para a plataforma alvo, Intel ou Arm64x, por exemplo.
Após a conclusão da geração da imagem nativa, é possível ver um relatório mostrando as classes que estão sendo utilizadas e seus relativos tamanhos na aplicação, identificadas na analise estática do código, conforme abaixo:
Execução da aplicação
Após a execução do comando, é possível ver dentro da nossa pasta /target
que temos a aplicação gerada nativamente e a compilada, respectivamente:
native-image-example
native-imagem-example-0.0.1-SNAPSHOT.jar
Agora iremos executar a aplicação e comparar o tempo de inicialização de cada uma.
Para aplicação gerada com JVM:
Para aplicação gerada nativamente:
Comparando os tempos a aplicação nativa inicializou aproximadamente 9 vezes mais rápido do que aplicação compilada(JIT).
Conclusão
A diferença de tempo entre as aplicações parece ser irrisória, mas devido a aplicação ser muito simples. Em aplicações mais complexas, com banco de dados, cache, message brokers e vários carrecamentos em tempo de execução, essa diferença será mais considerável.
Nessa comparação só analisamos o tempo de inicialização da aplicação, mas existem outros fatores que favorecem a utilização de Native images, como redução do consumo de memória e CPU.
Caso queira ver uma comparação de performance entre compilação usando JIT e AOT com GraalVM, fiz um exemplo e testei aqui.
Se você gostou do artigo, por favor não deixe de bater palmas 👏 (você pode fazer várias vezes), me seguir, ou até mesmo me comprar um café☕️https://www.buymeacoffee.com/valdemarjuniorr