Compilamos o aplicativo Spring Boot em nativo usando o GraalVM

Uma tradução do artigo foi preparada antes do início do curso "Developer on the Spring Framework" .



Olá amantes da primavera! Bem-vindo ao próximo lançamento do Spring Tips. Hoje falaremos sobre o suporte implementado recentemente para compilar aplicativos Spring Boot no GraalVM. Já falamos sobre o GraalVM e aplicativos nativos em outro lançamento do Spring Tips sobre o tópico do Spring Fu.



Vamos lembrar o que é GraalVM. GraalVM é um substituto para o compilador C1 padrão no OpenJDK. Você pode ler mais sobre o uso do GraalVM no meu podcast Bootiful Podcast com Chris Thalinge, colaborador do GraalVM e engenheiro do Twitter. Sob certas condições, o GraalVM permite que você execute aplicativos regulares do Spring mais rapidamente e, pelo menos por esse motivo, merece atenção.

Mas não vamos falar sobre isso. Veremos outros componentes do GraalVM: construtor de imagens nativo e SubstrateVM. O SubstrateVM permite criar executáveis ​​nativos para o seu aplicativo Java. A propósito, sobre esse e outros usos do GraalVM, houve um podcast com Oleg Shelaev, do Oracle Labs. O construtor de imagens nativas é um teste de comprometimento. Se você fornecer ao GraalVM informações suficientes sobre o comportamento do seu aplicativo em tempo de execução (bibliotecas vinculadas dinamicamente, reflexão, proxies etc.), ele poderá transformar seu aplicativo Java em um binário vinculado estaticamente, como um aplicativo em C ou Golang. Honestamente, esse processo pode ser bastante doloroso. Mas se você fizer isso, poderá gerar código nativo que será incrivelmente rápido. Como resultado, o aplicativo ocupará muito menos RAM e será executado em menos de um segundo. Menos de um segundo. Bastante tentador, não é? Claro!

No entanto, deve-se lembrar que alguns pontos devem ser levados em consideração. Os binários GraalVM resultantes não são aplicativos Java. Eles nem são executados em uma JVM regular. O GraalVM está sendo desenvolvido pelo Oracle Labs e existe algum tipo de interação entre as equipes Java e GraalVM, mas eu não chamaria isso de Java. O binário resultante não será multiplataforma. O aplicativo não usa a JVM. Ele é executado em outro tempo de execução chamado SubstrateVM.

Portanto, existem muitas vantagens e desvantagens aqui, mas, no entanto, acho que o GraalVM tem um grande potencial, especialmente para aplicativos baseados em nuvem, onde a escalabilidade e a eficiência são fundamentais.

Vamos começar. Instale o GraalVM. Você pode baixá-lo aqui ou instalar usando o SDKManager. Para instalar distribuições Java, eu gosto de usar o SDKManager. O GraalVM está um pouco atrás das versões mais recentes do Java e atualmente suporta Java 8 e 11. O suporte para Java 14 ou 15 ou posterior (que versão estará disponível quando você ler isso) está ausente.

Para instalar o GraalVM for Java 8, execute:

sdk install java 20.0.0.r8-grl

Eu recomendo usar o Java 8 em vez do Java 11, pois existem alguns erros obscuros no Java 11 que eu ainda não descobri.

Depois disso, você precisa instalar o componente nativo do construtor de imagens. Execute:

gu install native-image
gu- este é um utilitário do GraalVM.

Por fim, verifique o que JAVA_HOMEaponta para o GraalVM. Na minha máquina (Macintosh com SDKMAN), a minha JAVA_HOMEfica assim:

export JAVA_HOME=$HOME/.sdkman/candidates/java/current/

Agora que você já configurou tudo, vamos dar uma olhada em nosso aplicativo. Vá para Spring Initializr e gere um novo projeto usando Lombok, R2DBC, PostgreSQL e Reactive Web.

Você viu um código semelhante um milhão de vezes, então não vou desmontá-lo, mas apenas fornecê-lo aqui.

package com.example.reactive;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.Id;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketMessage;
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
import reactor.core.publisher.Flux;

import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.stream.Stream;

import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

@Log4j2
@SpringBootApplication(proxyBeanMethods = false)
public class ReactiveApplication {

    @Bean
    RouterFunction<ServerResponse> routes(ReservationRepository rr) {
        return route()
            .GET("/reservations", r -> ok().body(rr.findAll(), Reservation.class))
            .build();
    }

    @Bean
    ApplicationRunner runner(DatabaseClient databaseClient, ReservationRepository reservationRepository) {
        return args -> {

            Flux<Reservation> names = Flux
                .just("Andy", "Sebastien")
                .map(name -> new Reservation(null, name))
                .flatMap(reservationRepository::save);

            databaseClient
                .execute("create table reservation ( id   serial primary key, name varchar(255) not null )")
                .fetch()
                .rowsUpdated()
                .thenMany(names)
                .thenMany(reservationRepository.findAll())
                .subscribe(log::info);
        };
    }


    public static void main(String[] args) {
        SpringApplication.run(ReactiveApplication.class, args);
    }
}

interface ReservationRepository extends ReactiveCrudRepository<Reservation, Integer> {
}


@Data
@AllArgsConstructor
@NoArgsConstructor
class Reservation {

    @Id
    private Integer id;
    private String name;
}

Você pode ver o código completo aqui .

O único recurso desse aplicativo é que usamos o atributo Spring Boot proxyBeanMethodspara garantir que o aplicativo não use cglib e outros proxies que não sejam o JDK. O GraalVM não suporta proxies não-JDK. Embora mesmo com o proxy JDK, você precise mexer nele para que o GraalVM aprenda sobre eles. Esse atributo, novo no Spring Framework 5.2, destina-se em parte ao suporte ao GraalVM.

Então, vamos seguir em frente. Mencionei anteriormente que precisamos informar o GraalVM sobre alguns pontos que podem estar em nosso aplicativo em tempo de execução e que ele pode não entender ao executar código nativo. São coisas como reflexão, proxies, etc. Existem várias maneiras de fazer isso. Você pode descrever a configuração manualmente e incluí-la na montagem. O GraalVM irá buscá-lo automaticamente. Outra maneira é executar o programa com um agente Java que monitora o que o aplicativo está fazendo e, depois que o aplicativo termina, grava tudo nos arquivos de configuração, que podem ser transferidos para o compilador GraalVM.

Você também pode usar o recurso GraalVM. (Nota Tradutor: “feature” - o termo GraalVM indica um plug-in para compilação nativa, criando um binário nativo a partir de um arquivo de classe ) . O recurso GraalVM é semelhante a um agente Java. Pode fazer algum tipo de análise e passar informações para o compilador GraalVM. O recurso conhece e entende como o aplicativo Spring funciona. Ela sabe quando os beans Spring são proxies. Ela sabe como criar classes dinamicamente em tempo de execução. Ela sabe como o Spring funciona, e sabe o que o GraalVM deseja, pelo menos na maioria das vezes (afinal, esse é um release antecipado!)

Você também precisa configurar a compilação. Aqui está o meupom.xml.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.M4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>reactive</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <start-class>
            com.example.reactive.ReactiveApplication
        </start-class>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.experimental</groupId>
            <artifactId>spring-graal-native</artifactId>
            <version>0.6.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-indexer</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-r2dbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>io.r2dbc</groupId>
            <artifactId>r2dbc-h2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <finalName>
            ${project.artifactId}
        </finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </pluginRepository>
    </pluginRepositories>


    <profiles>
        <profile>
            <id>graal</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.graalvm.nativeimage</groupId>
                        <artifactId>native-image-maven-plugin</artifactId>
                        <version>20.0.0</version>
                        <configuration>
                            <buildArgs>
-Dspring.graal.mode=initialization-only -Dspring.graal.dump-config=/tmp/computed-reflect-config.json -Dspring.graal.verbose=true -Dspring.graal.skip-logback=true --initialize-at-run-time=org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils --initialize-at-build-time=io.r2dbc.spi.IsolationLevel,io.r2dbc.spi --initialize-at-build-time=io.r2dbc.spi.ConstantPool,io.r2dbc.spi.Assert,io.r2dbc.spi.ValidationDepth --initialize-at-build-time=org.springframework.data.r2dbc.connectionfactory -H:+TraceClassInitialization --no-fallback --allow-incomplete-classpath --report-unsupported-elements-at-runtime -H:+ReportExceptionStackTraces --no-server --initialize-at-build-time=org.reactivestreams.Publisher --initialize-at-build-time=com.example.reactive.ReservationRepository --initialize-at-run-time=io.netty.channel.unix.Socket --initialize-at-run-time=io.netty.channel.unix.IovArray --initialize-at-run-time=io.netty.channel.epoll.EpollEventLoop --initialize-at-run-time=io.netty.channel.unix.Errors
                            </buildArgs>
                        </configuration>
                        <executions>
                            <execution>
                                <goals>
                                    <goal>native-image</goal>
                                </goals>
                                <phase>package</phase>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-maven-plugin</artifactId>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

</project>

Aqui prestamos atenção ao plugin native-image-maven-plugin. Ele leva os parâmetros pela linha de comando, o que o ajuda a entender o que precisa ser feito. Todos esses parâmetros são buildArgsnecessários para o aplicativo iniciar. (Tenho que expressar minha profunda gratidão a Andy Clement, o mantenedor do Spring GraalVM Feature, por me ajudar a descobrir todas essas opções!)

<dependency>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-graal-native</artifactId>
    <version>0.6.0.RELEASE</version>
</dependency>

Queremos que o compilador GraalVM tenha o maior número de maneiras possível para fornecer informações sobre como o aplicativo deve funcionar: agente java, recurso GraalVM, parâmetros da linha de comando. Tudo isso fornece ao GraalVM informações suficientes para transformar com êxito o aplicativo em um binário nativo compilado estaticamente. A longo prazo, nosso objetivo são os projetos da primavera. E o recurso Spring GraalVM fornece tudo o que você precisa para suportá-los.

Agora que já temos tudo configurado, vamos montar um aplicativo:

  • Compile o aplicativo Java da maneira usual
  • Inicie um aplicativo Java com um agente Java para coletar informações. Nesta fase, precisamos garantir que o aplicativo esteja funcionando. Você precisa passar por todos os casos de uso possíveis. A propósito, este é um caso muito bom para usar CI e testes! Todo mundo está constantemente falando sobre testar o aplicativo e melhorar o desempenho. Agora, com o GraalVM, você pode fazer as duas coisas!
  • Em seguida, recrie o aplicativo, desta vez com o perfil graal ativo, para compilá-lo no aplicativo nativo, usando as informações coletadas durante o primeiro lançamento.

mvn -DskipTests=true clean package
export MI=src/main/resources/META-INF
mkdir -p $MI 
java -agentlib:native-image-agent=config-output-dir=${MI}/native-image -jar target/reactive.jar

## it's at this point that you need to exercise the application: http://localhost:8080/reservations 
## then hit CTRL + C to stop the running application.

tree $MI
mvn -Pgraal clean package

Se tudo ocorreu sem erros, no diretório targetvocê verá um aplicativo compilado. Executá-lo.

./target/com.example.reactive.reactiveapplication

O aplicativo é iniciado, como pode ser visto em uma saída semelhante a esta.

2020-04-15 23:25:08.826  INFO 7692 --- [           main] c.example.reactive.ReactiveApplication   : Started ReactiveApplication in 0.099 seconds (JVM running for 0.103)

Não é ruim? O construtor de imagens nativas do GraalVM é ótimo para emparelhado com uma plataforma em nuvem como CloudFoundry ou Kubernetes. Você pode montar o aplicativo facilmente em um contêiner e executá-lo na nuvem com o mínimo de recursos.
Como sempre, teremos o maior prazer em ouvir de você. Esta tecnologia é adequada para você? Questões? Comentários? Twitter (@springcentral) .



Saiba mais sobre o curso

All Articles