Compilamos la aplicación Spring Boot en nativo usando GraalVM

Se preparó una traducción del artículo antes del comienzo del curso "Desarrollador en el marco de Spring" .



Hola amantes de la primavera! Bienvenido a la próxima versión de Spring Tips. Hoy hablaremos sobre el soporte implementado recientemente para compilar aplicaciones Spring Boot en GraalVM. Ya hablamos sobre GraalVM y las aplicaciones nativas en otro lanzamiento de Spring Tips sobre el tema de Spring Fu.



Recordemos qué es GraalVM. GraalVM es un reemplazo para el compilador C1 estándar en OpenJDK. Puede leer más sobre el uso de GraalVM en mi podcast Bootiful Podcast con Chris Thalinge, un colaborador de GraalVM e ingeniero de Twitter. Bajo ciertas condiciones, GraalVM le permite ejecutar aplicaciones regulares de Spring más rápido y, al menos por esta razón, merece atención.

Pero no hablaremos de eso. Examinaremos otros componentes de GraalVM: generador de imágenes nativas y SubstrateVM. SubstrateVM le permite crear ejecutables nativos para su aplicación Java. Por cierto, sobre este y otros usos de GraalVM hubo un podcast con Oleg Shelaev de Oracle Labs. Native Image Builder es una prueba de compromiso. Si proporciona a GraalVM información suficiente sobre el comportamiento de su aplicación en tiempo de ejecución (bibliotecas vinculadas dinámicamente, reflexión, proxies, etc.), podrá convertir su aplicación Java en un binario estáticamente vinculado, como una aplicación en C o Golang. Honestamente, este proceso puede ser bastante doloroso. Pero si lo hace, puede generar código nativo que será increíblemente rápido. Como resultado, la aplicación ocupará mucho menos RAM y se ejecutará en menos de un segundo. Menos de un segundo Bastante tentador, ¿no? ¡Por supuesto!

Sin embargo, debe recordarse que algunos puntos deben tenerse en cuenta. Los binarios de GraalVM resultantes no son aplicaciones Java. Ni siquiera se ejecutan en una JVM normal. Oracle Labs está desarrollando GraalVM y existe algún tipo de interacción entre los equipos de Java y GraalVM, pero no lo llamaría Java. El binario resultante no será multiplataforma. La aplicación no usa la JVM. Se ejecuta en otro tiempo de ejecución llamado SubstrateVM.

Por lo tanto, hay muchas compensaciones aquí, pero no obstante, creo que GraalVM tiene un gran potencial, especialmente para aplicaciones basadas en la nube donde la escala y la eficiencia son primordiales.

Empecemos. Instalar GraalVM. Puede descargarlo aquí o instalarlo utilizando SDKManager. Para instalar distribuciones Java, me gusta usar SDKManager. GraalVM está ligeramente por detrás de las últimas versiones de Java y actualmente es compatible con Java 8 y 11. Falta el soporte para Java 14 o 15 o posterior (qué versión estará allí cuando lea esto).

Para instalar GraalVM para Java 8, ejecute:

sdk install java 20.0.0.r8-grl

Recomiendo usar Java 8 en lugar de Java 11, ya que hay algunos errores oscuros en Java 11 que aún no he descubierto.

Después de eso, debe instalar el componente generador de imágenes nativo. Ejecutar:

gu install native-image
guesta es una utilidad de GraalVM.

Por último, verifique qué JAVA_HOMEseñala a GraalVM. En mi máquina (Macintosh con SDKMAN) la mía se JAVA_HOMEve así:

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

Ahora que tiene todo configurado, echemos un vistazo a nuestra aplicación. Vaya a Spring Initializr y genere un nuevo proyecto utilizando Lombok, R2DBC, PostgreSQL y Reactive Web.

Viste un código similar un millón de veces, así que no lo desarmaré, pero solo dalo aquí.

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;
}

Puedes ver el código completo aquí .

La única característica de esta aplicación es que usamos el atributo Spring Boot proxyBeanMethodspara asegurarnos de que la aplicación no use cglib u otros proxies que no sean JDK. GraalVM no admite proxies que no sean JDK. Aunque incluso con el proxy JDK, debe jugar con él para que GraalVM los conozca. Este atributo, nuevo en Spring Framework 5.2, está destinado en parte a soportar GraalVM.

Entonces, sigamos adelante. Mencioné anteriormente que tenemos que decirle a GraalVM sobre algunos puntos que pueden estar en nuestra aplicación en tiempo de ejecución y que puede que no se entienda al ejecutar código nativo. Estas son cosas como la reflexión, los poderes, etc. Hay varias maneras de hacer esto. Puede describir la configuración manualmente e incluirla en el ensamblaje. GraalVM lo recogerá automáticamente. Otra forma es ejecutar el programa con un agente Java que supervisa lo que está haciendo la aplicación y, una vez que la aplicación finaliza, escribe todo en los archivos de configuración, que luego se pueden transferir al compilador GraalVM.

También puede usar la función GraalVM. (Nota Traductor: "característica": el término GraalVM denota un complemento para compilación nativa, creando un binario nativo a partir de un archivo de clase ) . La función GraalVM es similar a un agente Java. Puede hacer algún tipo de análisis y pasar información al compilador GraalVM. Feature sabe y comprende cómo funciona la aplicación Spring. Ella sabe cuándo los frijoles de primavera son representantes. Ella sabe cómo crear dinámicamente clases en tiempo de ejecución. Ella sabe cómo funciona Spring, y sabe lo que GraalVM quiere, al menos la mayor parte del tiempo (¡después de todo, esta es una versión anticipada!)

También debe configurar la compilación. Esta es la míapom.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>

Aquí prestamos atención al complemento native-image-maven-plugin. Toma parámetros a través de la línea de comando, que lo ayudan a comprender lo que hay que hacer. Todos estos parámetros son buildArgsnecesarios para que la aplicación se inicie. (¡Tengo que expresar mi profunda gratitud a Andy Clement, el responsable de Spring GraalVM Feature, por ayudarme a descubrir todas estas opciones!)

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

Queremos que el compilador GraalVM tenga tantas formas como sea posible para proporcionar información sobre cómo debería funcionar la aplicación: agente de Java, función GraalVM, parámetros de línea de comandos. Todo esto en conjunto brinda a GraalVM suficiente información para convertir con éxito la aplicación en un binario nativo estáticamente compilado. A la larga, nuestro objetivo son los proyectos de primavera. Y la función Spring GraalVM proporciona todo lo que necesita para admitirlos.

Ahora que tenemos todo configurado, creemos una aplicación:

  • Compile la aplicación Java de la forma habitual
  • Inicie una aplicación Java con un agente Java para recopilar información. En esta etapa, debemos asegurarnos de que la aplicación esté funcionando. Debe revisar todos los casos de uso posibles. Por cierto, este es un muy buen caso para usar CI y pruebas. Todos hablan constantemente de probar la aplicación y mejorar el rendimiento. Ahora, con GraalVM, ¡puedes hacer ambas cosas!
  • Luego, reconstruya la aplicación, esta vez con el perfil de graal activo, para compilarla en la aplicación nativa, utilizando la información recopilada durante el primer lanzamiento.

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

Si todo salió sin errores, en el directorio targetverá una aplicación compilada. Ejecutarlo.

./target/com.example.reactive.reactiveapplication

La aplicación se inicia, como se puede ver en un resultado similar a este.

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)

¿No está mal? El generador de imágenes nativas GraalVM es ideal para combinar con una plataforma en la nube como CloudFoundry o Kubernetes. Puede ensamblar fácilmente la aplicación en un contenedor y ejecutarla en la nube con recursos mínimos.
Como siempre, estaremos encantados de saber de usted. ¿Es esta tecnología adecuada para usted? Preguntas? Comentarios? Twitter (@springcentral) .



Aprende más sobre el curso

All Articles