Spring 小技巧:GraalVM Native Image Builder 功能

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

講者:Josh Long (@starbuxman)

嗨,Spring 的粉絲們!歡迎來到另一期的 Spring 小技巧。在這一期中,我們將看看最新發布的對使用 GraalVM 構建 Spring Boot 應用程式的支援。當我們研究 Spring Fu 時,我們已經在另一個 Spring 小技巧中看過 GraalVM 和 native images。

GraalVM 有好幾個用途。它是標準 OpenJDK 安裝的 C1 替代品。您可以收聽我的 podcast A Bootiful Podcast 的這一集,其中與 GraalVM 貢獻者和 Twitter 工程師 Chris Thalinger 討論了 GraalVM 的這個用途的更多細節。它可以讓您在某些條件下更快地運行常規 Sring 應用程式,因此僅僅因為這個原因就值得探索。

我們不會在這段影片中討論這個。相反,我們將查看 Graal VM 內部的一個特定元件,稱為 native image builder 和 SubstrateVM。SubstrateVM 讓您可以從您的 Java 應用程式構建 native images。順便一提,我Oracle Labs 的 Oleg Shelajev 做了一個 podcast,討論了 GraalVM 的這個和其他用途。 native image builder 是一種妥協的練習。如果您向 GraalVM 提供足夠的關於您的應用程式運行時行為的資訊,例如動態連結程式庫、反射、proxies 等,那麼它可以將您的 Java 應用程式變成一個靜態連結的二進位檔案,有點像 C 或 Go 語言的應用程式。說實話,這個過程有時... 很痛苦。但是,一旦您這樣做了,那麼該工具就可以為您生成極快的 native code。生成的應用程式佔用的 RAM 少得多,並且在一秒內啟動。遠遠低於一秒。非常誘人,不是嗎?確實是!

請記住,運行應用程式時,還有其他需要注意的成本。 GraalVM native images 不是 Java 應用程式。它們甚至不在傳統的 JVM 上運行。 Oracle Labs 開發 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 變體中存在一些我無法完全弄清楚的細微錯誤。

完成後,您還需要單獨安裝 native image builder 元件。運行此命令: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 針對資料庫執行的第一個 statment 是一個建立表格的 SQL statment。不幸的是,我們在此處使用的部落格軟體出於安全原因不允許我重新列印 SQL 語句,因此我改為將 您導向此處的原始碼

此應用程式中唯一值得注意的事情是,我們正在使用 Spring Boot 的 proxyBeanMethods 屬性,以確保我們避免在應用程式中使用 cglib 和任何其他非 JDK proxies。 GraalVM_hates_ 非 JDK proxies,即使使用 JDK proxies,也需要完成一些工作才能讓 GraalVM 知道它。此屬性是 Spring Framework 5.2 中的新增功能,部分旨在支援 GraalVM 應用程式。

那麼讓我們來談談。我之前暗示過,我們需要教導 GraalVM 關於我們可能在運行時在應用程式中所做的棘手的事情,如果我們在 native image 中執行這些操作,它可能不會理解。例如反射、proxies 等。有幾種方法可以做到這一點。您可以手工製作一些設定並將其包含在您的 build 中。 GraalVM 將自動新增它。您也可以在 Java agent 的監視下運行您的程式,該 agent 將記錄您的應用程式所做的棘手事情,並且一旦應用程式完成,就將所有這些東西寫入設定檔,然後可以將這些設定檔提供給 GraalVM 編譯器。

當您運行應用程式時,您可以做的另一件事是運行功能。 GraalVM 功能有點像 Java agent。它可以根據它所做的任何分析,將資訊饋送到 GraalVM 編譯器中。我們的功能了解並理解 Spring 應用程式的工作方式。它知道 Spring bean 何時是 proxies。它知道類別如何在運行時動態建構。它知道 SPring 的工作方式,並且它知道 GraalVM 想要什麼,在大多數情況下。(畢竟,這是一個早期版本!)

我們還需要自訂 build。這是我的 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 插件新增到 build 中。該插件還採用了一些命令列設定,以幫助它了解應該做什麼。 buildArgs 元素中的長長的命令列參數列表表示使此應用程式運行的所需命令列 switches。(我非常感謝 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 功能方法。我們還將利用命令列設定。所有這些資訊加在一起,為 GraalVM 提供了足夠的資訊,以成功地將應用程式轉換為靜態編譯的 native image。從長遠來看,目標是讓 Spring 專案和 Spring GraalVM 功能支援此流程的所有內容。

現在我們已經配置好所有這些,讓我們構建應用程式。這是基本工作流程。

  • 像往常一樣編譯 Java 應用程式
  • 使用 Java agent 運行 Java 應用程式以收集資訊。我們需要確保在此時執行應用程式。執行每條可能的路徑!順便說一句,這正是 CI 和測試的用例!他們總是說您應該使您的應用程式正常工作(您可以透過測試來做到這一點),然後使其快速。現在,有了 Graal,您可以同時做到這兩點!
  • 然後再次重建應用程式,這次啟用了 graal 設定檔,以使用從第一次運行中收集的資訊編譯 native image。
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)

很酷,不是嗎? GraalVM native image builder 與 CloudFoundry 或 Kubernetes 等雲端平台搭配使用時非常適合。您可以輕鬆地將應用程式容器化,並以最小的佔用空間使其在雲端平台上工作。請享用!與往常一樣,我們很樂意收到您的來信。此技術是否適合您的用例?問題?評論?請在 Twitter (@springcentral) 上向我們發送回饋和聲音。

取得 Spring 電子報

隨時掌握 Spring 電子報的最新消息

訂閱

搶先一步

VMware 提供培訓和認證,以加速您的進度。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

了解更多

即將舉行的活動

查看 Spring 社群中所有即將舉行的活動。

查看所有