我们使用GraalVM将Spring Boot应用程序编译为本机

在“ Spring框架开发人员 ”课程开始之前,准备了本文的翻译



您好春天恋人!欢迎使用Spring Tips的下一个版本。今天,我们将讨论最近实现的在GraalVM中编译Spring Boot应用程序的支持。我们已经在另一个关于Spring Fu的Spring Tips版本中讨论了GraalVM和本机应用程序。



让我们回顾一下GraalVM是什么。GraalVM替代了OpenJDK中的标准C1编译器。您可以与GraalVM贡献者和Twitter工程师Chris Thalinge 一起在Bootiful Podcast播客中阅读有关使用GraalVM的更多信息在某些情况下,GraalVM可以让您更快地运行常规Spring应用程序,至少由于这个原因,它值得关注。

但是我们不会谈论它。我们将研究其他GraalVM组件:本机图像生成器和SubstrateVM。 SubstrateVM允许您为Java应用程序创建本机可执行文件。顺便说一下,关于GraalVM的这种用途和其他用途,有来自Oracle Labs的Oleg Shelaev的播客。本机映像构建器是对折衷的测试。如果向GraalVM提供有关运行时应用程序行为的足够信息(动态链接库,反射,代理等),它将能够将Java应用程序转换为静态链接二进制文件,例如C或Golang中的应用程序。老实说,这个过程可能会很痛苦。但是,如果您这样做,则可以生成非常快的本机代码。结果,该应用程序将占用更少的RAM,并在不到一秒钟的时间内运行。不到一秒钟。很诱人,不是吗?当然!

但是,应记住,必须考虑一些要点。生成的GraalVM二进制文件不是Java应用程序。它们甚至无法在常规JVM上运行。 GraalVM由Oracle Labs开发,Java和GraalVM团队之间存在某种互动,但是我不会将其称为Java。生成的二进制文件不会跨平台。该应用程序不使用JVM。它在另一个称为SubstrateVM的运行时中运行。

因此,这里有很多折衷,但是我认为GraalVM具有巨大的潜力,尤其是对于规模和效率至关重要的基于云的应用程序。

开始吧。安装GraalVM。您可以在此处下载它,或使用SDKManager安装为了安装Java发行版,我喜欢使用SDKManager。GraalVM落后于Java的最新版本,并且当前支持Java 8和11。对Java 14或15或更高版本(阅读本文时将使用该版本)的支持已丢失。

要为Java 8安装GraalVM,请运行:

sdk install java 20.0.0.r8-grl

我建议使用Java 8而不是Java 11,因为Java 11中有些模糊的错误尚未弄清。

之后,您需要安装本机映像构建器组件。运行:

gu install native-image
gu-这是GraalVM的实用程序。

最后,检查什么JAVA_HOME指向GraalVM。在我的机器(带有SDKMAN的Macintosh)上,我的JAVA_HOME看起来像这样:

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

现在您已经完成了所有设置,让我们看一下我们的应用程序。转到Spring Initializr并使用Lombok,R2DBC,PostgreSQL和Reactive 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("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;
}

您可以在此处查看完整的代码

该应用程序的唯一功能是,我们使用Spring Boot属性proxyBeanMethods来确保该应用程序将不使用cglib和JDK以外的其他代理。GraalVM不支持非JDK代理。尽管即使使用JDK代理,您也必须对其进行修改,以便GraalVM了解它们。Spring Framework 5.2新增的此属性部分旨在支持GraalVM。

因此,让我们继续前进。前面我提到过,我们必须告诉GraalVM运行时可能在我们的应用程序中的某些要点,以及在执行本机代码时可能不理解的要点。这些是诸如反射,代理等之类的东西。有几种方法可以做到这一点。您可以手动描述配置并将其包括在装配中。 GraalVM将自动将其拾取。另一种方法是使用Java代理运行程序,该代理监视应用程序的运行情况,并在应用程序完成后将所有内容写入配置文件,然后将其传输到GraalVM编译器。

您也可以使用GraalVM功能。注意翻译:“功能” -术语GraalVM表示插件为本地编译,生成类文件中的本机二进制。 GraalVM功能类似于Java代理。它可以进行某种分析,并将信息传递给GraalVM编译器。 Feature知道并了解Spring应用程序的工作方式。她知道何时四季豆是代理。她知道如何在运行时动态创建类。她知道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和测试的一个很好的例子!每个人都在不断谈论测试应用程序和提高性能。现在,借助GraalVM,您可以同时做到!
  • 然后,使用活动的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: 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)



了解有关该课程的更多信息

All Articles