Usando Native Images em projetos Spring Boot com GraalVM

Valdemar Júnior
4 min readApr 18, 2024

--

Logo do SpringBoot e da 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:

Tela inicial do site start.spring.io selelcionando a linguagem Java, com Maven e as dependências GraalVM Native Support e Spring Web
Vamos adicionar as dependências do GraalVM e Spring Web

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.xmlda 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:

Relatório de analise das classes utilizadas na geração da imagem nativa

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:

Resultado: Started WebApplication in 0.863 seconds

Para aplicação gerada nativamente:

Resultado: Started WebApplication in 0.083 seconds

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

Referências

--

--

Valdemar Júnior

Escrevo sobre lições aprendidas, estudos, dicas de produtividade e assuntos relacionados a Java, Spring e seus ecosistemas. Gosto de compartilhar conhecimento