Spring 技巧:GraalVM 原生镜像构建器功能

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

演讲者:Josh Long (@starbuxman)

嗨,Spring 爱好者们!欢迎来到新一期的《Spring 技巧》。在本期中,我们将介绍刚刚发布的、用于使用 GraalVM 构建 Spring Boot 应用程序的新支持。在另一期关于 Spring Fu 的 Spring 技巧中,我们已经探讨过 GraalVM 和原生镜像。

GraalVM 有多种用途。它可以替代标准 OpenJDK 安装中的 C1 编译器。您可以收听我的播客《一个漂亮的播客》中与 GraalVM 贡献者兼 Twitter 工程师 Chris Thalinger 的访谈,了解 GraalVM 的更多用途。在某些条件下,它可以让您运行普通的 Spring 应用程序更快,仅凭这一点就值得探索。

在这个视频中,我们将不讨论这一点。相反,我们将关注 GraalVM 内的一个特定组件:原生镜像构建器和 SubstrateVM。SubstrateVM 允许您从 Java 应用程序构建原生镜像。顺便说一句,我还与 Oracle Labs 的 Oleg Shelajev 就 GraalVM 的这些及其他用途进行过一次播客访谈。原生镜像构建器是一种折衷的实践。如果您提供足够的关于应用程序运行时行为的信息给 GraalVM(例如动态链接库、反射、代理等),那么它可以将您的 Java 应用程序变成一个静态链接的二进制文件,有点像 C 或 Go 语言应用程序。老实说,这个过程有时会... 痛苦。但是,一旦您完成了,这个工具就能为您生成极快的原生代码。最终生成的应用程序会占用少得多的内存,并且在一秒钟内启动。远远低于一秒。相当诱人,不是吗?确实如此!

但请记住,运行应用程序时还需要注意其他成本。GraalVM 原生镜像不是 Java 应用程序。它们甚至不在传统的 JVM 上运行。GraalVM 由 Oracle Labs 开发,因此 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 Agent 的监视下运行您的程序,Java Agent 会记录您的应用程序执行的那些“技巧性”操作,然后在应用程序结束后,将所有这些信息写入配置文件中,这些文件随后可以提供给 GraalVM 编译器。

运行应用程序时还可以做另一件事,就是运行一个 feature。GraalVM 的 feature 有点像 Java Agent。它可以根据其进行的任何分析,将信息提供给 GraalVM 编译器。我们的 feature 了解 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 Feature 负责人 Andy Clement 搞定了这一切!)

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

我们希望利用尽可能多的方式向 GraalVM 编译器提供关于应用程序如何运行的信息。我们将利用 Java Agent 方法。我们将利用 GraalVM feature 方法。我们还将利用命令行配置。所有这些信息综合起来,为 GraalVM 提供了足够的信息,以便成功地将应用程序转换为静态编译的原生镜像。长远目标是让 Spring 项目和 Spring GraalVM feature 全面支持这一过程。

现在一切都配置好了,让我们构建应用程序。基本流程如下:

  • 像往常一样编译 Java 应用程序
  • 使用 Java Agent 运行 Java 应用程序以收集信息。此时我们需要确保充分“操练”应用程序。尽可能地演练所有路径!顺便说一句,这正是 CI 和测试的典型用例!人们总是说,你应该让你的应用程序能够工作(这可以通过测试来完成),然后再让它变得更快。现在,有了 Graal,你可以两者兼得!
  • 然后再次重新构建应用程序,这次激活 graal profile,使用第一次运行收集到的信息编译原生镜像。
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

如果一切顺利,您应该能在 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)

相当酷,不是吗?GraalVM 原生镜像构建器与 CloudFoundry 或 Kubernetes 等云平台结合使用时,非常契合。您可以轻松地将应用程序容器化,并在云平台上以最小的资源占用运行。尽情享受吧!一如既往,我们期待您的反馈。这项技术是否适合您的用例?有疑问?有意见?欢迎通过Twitter (@springcentral) 向我们提供反馈和评论。

获取 Spring 邮件列表

订阅 Spring 邮件列表保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速前进。

了解更多

获取支持

Tanzu Spring 通过一项简单的订阅,为您提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

近期活动

查看 Spring 社区的所有近期活动。

查看全部