Spring Tips:Spring 和 GraalVM (第 2 部分)

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

演讲者:Josh Long (@starbuxman)

嗨,Spring 粉丝们!欢迎收听 Spring Tips 一个非常特别的、过渡期的节目,我们将重新审视 Spring 和 GraalVM 原生镜像。我想发布这个视频,以回应最近发布的 Spring Graal 0.7.1 版本,该版本大大简化了事情,甚至比我们上次在 2020 年 4 月很久以前看 Spring 和 Graal 的时候 还要简单

简而言之:GraalVm 是一个 JIT 替代方案,你可以用标准的 JVM 来使用它,这本身就值得研究。GraalVM 提供了一个独立的、支持原生镜像编译的功能。这个 native-image 构建器可以解析字节码并将其转换为一个与特定架构相关的二进制文件,该文件摆脱了 JVM 的束缚,并嵌入了一个称为 SubstrateVM 的东西。原生镜像是 启动速度快运行时内存占用极低 的。这些特性使其在容器化、以云计算为中心的环境中备受青睐。

在四月份的文章中,我不得不手动编写大量的定制化配置。在最新的这期中,可以实现大量应用程序无需变量配置即可运行。在视频中,我演示了如何让 Spring Data JPA(带 Hibernate)和 Apache Tomcat 工作。我还演示了如何让一个响应式应用程序工作。我们先来看响应式应用程序,然后再看 JPA 的例子。在第一个例子中我们将采取的步骤对于大多数应用程序来说都是通用的。

我们将为这个项目使用 GraalVM 和 Java 8。我使用 SDKManager 来安装各种版本的 Java:sdk install java 20.1.0.r8-grl。然后,你可以选择将其设为默认:sdk default java 20.1.0.r8-grl。你还需要将原生镜像构建器安装到你的 GraalVM 安装中。使用 gu install native-image。现在我们可以构建应用程序了。

响应式示例

首先,前往 Spring Initializr,生成一个包含 R2DBCLombokH2Reactive Web 的新项目,并使用 Java 8。

你以前见过这些 Java 代码

package com.example.reactive;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.data.annotation.Id;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@SpringBootApplication(
        exclude = SpringDataWebAutoConfiguration.class,
        proxyBeanMethods = false
)
public class ReactiveApplication {

    public static void main(String[] args) {
        SpringApplication.run(ReactiveApplication.class, args);
    }

}


@RestController
@RequiredArgsConstructor
class CustomerRestController {

    private final CustomerRepository customerRepository;

    @GetMapping("/customers")
    Flux<Customer> customers() {
        return this.customerRepository.findAll();
    }
}

@Component
@RequiredArgsConstructor
class Initializer implements ApplicationListener<ApplicationReadyEvent> {

    private final CustomerRepository customerRepository;

    private final DatabaseClient databaseClient;

    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
        Flux<Customer> save = Flux.just("Madhura", "Dr. Syer")
                .map(name -> new Customer(null, name))
                .flatMap(this.customerRepository::save);

        String sql = "create table CUSTOMER(id serial primary key, name varchar(255))";

        this.databaseClient
                .execute(sql)
                .fetch()
                .rowsUpdated()
                .thenMany(save)
                .thenMany(this.customerRepository.findAll())
                .subscribe(System.out::println);
    }
}

interface CustomerRepository extends ReactiveCrudRepository<Customer, Integer> {

}

@Data
@AllArgsConstructor
@NoArgsConstructor
class Customer {

    @Id
    private Integer id;
    private String name;

}

唯一值得注意的、与 Graal 和原生镜像相关的就是,我们禁用了 @Configuration 类的代理创建(使用 proxyBeanMethods = false),并排除了 SpringDataWebAutoConfiguration.class Java 自动配置。希望最后这一点在不久的将来会变得无关紧要。

这就是应用程序。启动它,你会看到它能正常工作。我们需要对构建做一点小小的改动来适应 Graal。你需要在你的构建中包含快照和里程碑版本的 Spring 构件仓库。

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </pluginRepository>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>

然后,添加这三个 Maven 依赖。

    <dependency>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-graalvm-native</artifactId>
        <version>0.7.1</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
    </dependency>

就是这样!在这个仓库的代码中,我还有一个 Spring Data MongoDB 的演示。这是一个简单的 Spring Data MongoDB 应用程序,使用了 Spring MVC。这个例子需要与我刚才向你展示的响应式例子完全相同的依赖和属性。

现在我们需要编译它。首先,运行常规的 mvn clean package。然后,我们需要将 .jar 文件传递给 Graal 的 native-image 构建器。我有一个脚本 compile.sh,我为所有三个例子重复使用它。这是脚本。

#!/usr/bin/env bash

ARTIFACT=${1}
MAINCLASS=${2}
VERSION=${3}

JAR="${ARTIFACT}-${VERSION}.jar"

rm -rf target
mkdir -p target/native-image
mvn -ntp package  
rm -f $ARTIFACT
cd target/native-image
jar -xvf ../$JAR  
cp -R META-INF BOOT-INF/classes

LIBPATH=`find BOOT-INF/lib | tr '\n' ':'`
CP=BOOT-INF/classes:$LIBPATH
GRAALVM_VERSION=`native-image --version`

time native-image \
  --verbose \
  -H:EnableURLProtocols=http \
  -H:+RemoveSaturatedTypeFlows \
  -H:Name=$ARTIFACT \
  -Dspring.native.verbose=true \
  -Dspring.native.remove-jmx-support=true \
  -Dspring.native.remove-spel-support=true \
  -Dspring.native.remove-yaml-support=true \
  -cp $CP $MAINCLASS  

当你使用这个脚本时,你需要提供三样东西:构建的构件、主类名和版本。所以,对于这个应用程序,我们可以在同一个目录下像这样运行它:

./compile.sh reactive com.example.reactive.ReactiveApplication 0.0.1-SNAPSHOT

然后去泡一杯咖啡。很快的一杯。因为这至少需要三分钟。

JPA

完成了吗?很好。让我们构建另一个例子,这次使用 Spring Data JPA(Hibernate)和 Spring MVC(带 Apache Tomcat)。

前往 Spring Initializr,生成另一个项目。这次,指定 JPAH2Web,然后点击 Generate。这是代码。

package com.example.jpa;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.Collection;
import java.util.stream.Stream;

@SpringBootApplication(
        exclude = SpringDataWebAutoConfiguration.class,
        proxyBeanMethods = false
)
public class JpaApplication {

    public static void main(String[] args) {
        SpringApplication.run(JpaApplication.class, args);
    }

}


@RestController
@RequiredArgsConstructor
class CustomerRestController {

    private final CustomerRepository customerRepository;

    @GetMapping("/customers")
    Collection<Customer> customers() {
        return this.customerRepository.findAll();
    }
}

@Component
@RequiredArgsConstructor
class Initializer implements ApplicationListener<ApplicationReadyEvent> {

    private final CustomerRepository customerRepository;

    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
        Stream.of("Madhura", "Dr. Syer")
                .map(name -> new Customer(null, name))
                .map(this.customerRepository::save)
                .forEach(System.out::println);
    }
}

interface CustomerRepository extends JpaRepository<Customer, Integer> {

}

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
class Customer {

    @Id
    @GeneratedValue
    private Integer id;
    private String name;

}

现在,这个应用程序使用了 JPA(和 Hibernate)。Hibernate,就像 Spring 一样,可以在运行时做很多动态的事情。Graal 不喜欢 这样。所以我们需要让 Hibernate 在构建时增强我们的应用程序中的实体。在你的构建中添加以下 Maven 插件。

<plugin>
    <groupId>org.hibernate.orm.tooling</groupId>
    <artifactId>hibernate-enhance-maven-plugin</artifactId>
    <version>${hibernate.version}</version>
    <executions>
        <execution>
            <configuration>
                <failOnError>true</failOnError>
                <enableLazyInitialization>true</enableLazyInitialization>
                <enableDirtyTracking>true</enableDirtyTracking>
                <enableExtendedEnhancement>false</enableExtendedEnhancement>
            </configuration>
            <goals>
                <goal>enhance</goal>
            </goals>
        </execution>
    </executions>
</plugin>

我们最后需要做的是在运行时告诉 Hibernate 不要 进行任何增强。创建一个文件 src/main/resources/hibernate.properties

hibernate.bytecode.provider=none

现在你可以像编译响应式应用程序一样编译这个应用程序,只需替换主类即可。花几分钟时间。现在你应该在每个应用程序的 target/native-image 目录下有两个不同的应用程序。运行它们。

在我机器上,reactive 应用程序在 0.106 秒内启动。jpa 应用程序在 0.181 秒内启动。启动速度快,而且——最棒的是——运行时,这些应用程序将占用 几十 MB,而不是像典型的基于 JVM 的应用程序那样占用 几百(或 几千)MB。

后续步骤

我迫不及待地等待 Spring Graal 0.8.0 的发布,届时将基于 Spring Framework 5.3 中的许多改进进行基线更新,并可能包含将 @Configuration 中心化的 Java 配置转换为 Spring 的“函数式配置”的工具。这种配置不需要代理或反射,并且资源效率更高。三年前,我在另一期 Spring Tips 的节目中 已经看过了函数式配置

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

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

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

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

查看所有