Spring 提示:Spring 和 GraalVM(第 2 部分)

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

演讲者:Josh Long (@starbuxman)

嗨,Spring 粉丝们!欢迎收看 Spring Tips 的一期非常特别的间歇剧集,我们将重新回顾 Spring 和 GraalVM 原生镜像。我想发布这段视频是因为最近发布了 Spring Graal 0.7.1,与我们上次查看 Spring 和 Graal 时相比,它极大地简化了操作,早在 2020 年 4 月

TL;DR:GraalVm 是一种 JIT 替换,您可以将其与标准 JVM 一起使用,这本身就值得关注。GraalVM 提供了一个支持原生镜像编译的单独功能。此native-image构建器获取字节码并将其转换为特定于体系结构的二进制文件,从而去除 JVM 并嵌入称为 SubstrateVM 的内容。原生镜像启动速度快,并且在运行时占用更少的内存。这些特性使其在容器化、以云为中心的运行环境中非常理想。

在 4 月份的版本中,我不得不编写大量的精心制作的手工配置。在最新版本中,可以使大量应用程序无需变量配置即可正常工作。在视频中,我演示了如何使 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,然后单击生成。这是代码。

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 秒内启动。快速启动 - 最棒的是 - 在运行时,这些应用程序将占用几十兆字节,而不是像典型的基于 JVM 的应用程序那样占用数百(或数千)兆字节。

后续步骤

我迫不及待地想要看到 Spring Graal 0.8.0 版本的发布,它将(除其他事项外)基于 Spring Framework 5.3 中的许多改进,并且可能包含一个将@Configuration为中心的 Java 配置转换为 Spring 的“功能性配置”的功能,后者不需要代理或反射,并且资源效率更高。我在三年前的另一期 Spring Tips 中查看了功能性配置

获取 Spring 时事通讯

与 Spring 时事通讯保持联系

订阅

获得支持

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

了解更多信息

即将举行的活动

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

查看全部