Spring 提示:GraalVM 原生镜像构建器功能

工程 | Josh Long | 2020年4月16日 | ...

演讲者:Josh Long (@starbuxman)

嗨,Spring 粉丝们!欢迎来到另一期《Spring 提示》。在本期节目中,我们将介绍刚刚发布的用于使用 GraalVM 构建 Spring Boot 应用程序的新支持。我们在另一期《Spring 提示》中介绍了 Spring Fu 时,也曾介绍过 GraalVM 和原生镜像。

GraalVM 包含多个方面。它是标准 OpenJDK 安装的 C1 替换。您可以在我的播客《A Bootiful Podcast》的这一集中收听,与GraalVM 贡献者和 Twitter 工程师 Chris Thalinger 了解更多关于 GraalVM 此用法的详细信息。它允许您在特定条件下更快地运行常规 Spring 应用程序,因此仅凭这一点就值得探索。

我们不会在本视频中讨论这一点。相反,我们将查看 Graal VM 中一个名为原生镜像构建器和 SubstrateVM 的特定组件。SubstrateVM 允许您从 Java 应用程序构建原生镜像。顺便说一句,我Oracle 实验室的 Oleg Shelajev 录制了关于此和其他 GraalVM 用法的播客。原生镜像构建器是一种折衷方案。如果您向 GraalVM 提供足够的信息来了解应用程序的运行时行为 - 动态链接库、反射、代理等 - 那么它可以将您的 Java 应用程序转换为静态链接二进制文件,有点像 C 或 Go 语言应用程序。老实说,这个过程有时会很痛苦。但是,一旦您完成了此操作,该工具就可以为您生成极快的原生代码。生成的应用程序占用更少的 RAM,并在不到一秒的时间内启动。远远不到一秒。非常诱人,对吧?确实如此!

但请记住,在运行应用程序时还需要注意其他成本。GraalVM 原生镜像不是 Java 应用程序。它们甚至不在传统的 JVM 上运行。Oracle 实验室开发了 GraalVM,因此 Java 和 GraalVM 团队之间存在一定程度的合作,但我不会称之为 Java。生成的二进制文件不是跨平台的。当应用程序运行时,它不会在 JVM 上运行;它将在另一个名为 SubstrateVM 的运行时上运行。

因此,权衡因素很多,但尽管如此,我认为使用此工具构建应用程序(尤其是在云环境中用于生产的应用程序,其中可扩展性和效率至关重要)具有很大的潜在价值。

让我们开始吧。您需要安装 GraalVM。您可以在这里下载它,也可以使用SDKmanager下载它。我喜欢使用 SDKManager 安装我的 Java 发行版。GraalVm 往往落后于 Java 的主线版本。目前,它支持 Java 8 和 Java 11。值得注意的是,当您阅读和观看此内容时,它不支持 Java 14 或 15 或任何当前版本的 Java。

执行以下操作以安装适用于 Java 8 的 GraalVM:sdk install java 20.0.0.r8-grl。我建议使用 Java 8 而不是 Java 11,因为 Java 11 变体存在一些我无法完全解决的细微错误。

完成此操作后,您还需要单独安装原生镜像构建器组件。运行以下命令:gu install native-imagegu 是您在 GraalVM 中获得的实用程序。最后,确保已将JAVA_HOME设置为指向 GraalVM。在我的机器上,一台带有 SDKMAN 的 Macintosh,我的JAVA_HOME如下所示

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

好的,现在您已经完成了所有设置,让我们看看我们的应用程序。首先,转到Spring Initializr并使用LombokR2DBCPostgreSQLReactive Web生成一个新项目。

您已经见过这种代码无数次了,所以我不会在此处重新打印它之外进行复习。

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("....")
                .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;
}

我使用DatabaseClient针对数据库执行的第一个语句是创建表的 SQL 语句。不幸的是,我们在此处使用的博客软件出于安全原因不允许我重新打印 SQL 语句,因此我改为将您转到此处查看原始语句

此应用程序中唯一值得注意的是,我们正在使用 Spring Boot 的proxyBeanMethods属性来确保我们避免在应用程序中使用 cglib 和任何其他非 JDK 代理。GraalVM_讨厌_非 JDK 代理,即使使用 JDK 代理,也需要进行一些工作才能使 GraalVM 了解它。此属性是 Spring Framework 5.2 中的新属性,部分旨在支持 GraalVM 应用程序。

那么让我们来谈谈这个。我之前提到过,我们需要告诉 GraalVM 我们在应用程序运行时可能执行的一些棘手操作,如果在原生镜像中执行这些操作,它可能不会欣赏。例如反射、代理等。有几种方法可以做到这一点。您可以手工制作一些配置并将其包含在构建中。GraalVM 将自动添加它。您还可以通过 Java 代理的监控运行您的程序,该代理将记录您的应用程序执行的棘手操作,并在应用程序结束后将所有内容写入配置文件中,然后可以将其提供给 GraalVM 编译器。

在运行应用程序时,您还可以运行一个功能。GraalVM 功能有点像 Java 代理。它可以根据其执行的任何分析向 GraalVM 编译器提供信息。我们的功能了解并理解 Spring 应用程序的工作原理。它知道 Spring Bean 何时是代理。它知道类如何在运行时动态构造。它了解 Spring 的工作原理,并且大部分时间都知道 GraalVM 需要什么。(毕竟,这是一个早期版本!)

我们还需要自定义构建。这是我的pom.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>

这里值得注意的是,我们已将native-image-maven-plugin插件添加到构建中。该插件还采用了一些命令行配置,以帮助它理解应该执行的操作。buildArgs元素中那个长长的命令行参数列表表示使此应用程序运行所需的命令行开关。(我非常感谢 Spring GraalVM 功能负责人 Andy Clement 解决了所有这些问题!)

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

我们希望利用尽可能多的方法向 GraalVM 编译器提供有关应用程序运行方式的信息。我们将利用 Java 代理方法。我们将利用 GraalVM 功能方法。我们还将利用命令行配置。所有这些信息共同提供给 GraalVM 足够的信息,以便成功地将应用程序转换为静态编译的原生镜像。从长远来看,目标是让 Spring 项目和 Spring GraalVM 功能为所有内容都支持此过程。

现在我们已经完成了所有配置,让我们构建应用程序。这是基本工作流程。

  • 像往常一样编译 Java 应用程序
  • 使用 Java 代理运行 Java 应用程序以收集信息。我们需要确保在此处使用该应用程序。尽可能地使用所有路径!顺便说一句,这正是 CI 和测试用例!他们总是说您应该使您的应用程序工作(您可以通过测试它来做到这一点),然后使其快速。现在,使用 Graal,您可以同时做到这两点!
  • 然后再次重建应用程序,这次激活graal配置文件以使用从第一次运行中收集的信息编译原生镜像。
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: https://127.0.0.1:8080/reservations 
## then hit CTRL + C to stop the running application.

tree $MI
mvn -Pgraal clean package

如果所有这些都成功了,那么您应该能够在target目录中看到生成的应用程序。像这样运行它。

./target/com.example.reactive.reactiveapplication 

您的应用程序应该启动,从应用程序的输出中可以明显看出这一点,如下所示。

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)

很酷,对吧?当与 CloudFoundry 或 Kubernetes 等云平台配对时,GraalVM 原生镜像构建器非常适合。您可以轻松地将应用程序容器化,并使其在云平台上以最小的占用空间运行。尽情享受!与往常一样,我们很乐意收到您的来信。这项技术是否适合您的用例?问题?评论?反馈和声音关闭到我们在 Twitter 上 (@springcentral)

获取 Spring Newsletter

与 Spring Newsletter 保持联系

订阅

获得支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多信息

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部