Spring技巧:Spring与GraalVM(第二部分)

工程技术 | 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的东西。原生镜像启动速度快,并且运行时内存占用少很多。这些特性使其在容器化、云centric环境中非常受欢迎。

在四月份的那一期中,我不得不手写大量的精巧配置。而在这个最新版本中,无需变化配置就能让大量应用程序运行起来。在视频中,我演示了如何让一个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,生成一个新项目,选择R2DBC, Lombok, H2, Reactive 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。你需要在构建中加入snapshot和milestone的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 MVC的简单的Spring Data MongoDB应用程序。这个例子需要与我刚刚展示的响应式应用程序完全相同的依赖和属性。

现在我们需要编译它。首先,运行常规的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  

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

./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秒内启动。启动速度快,而且——最棒的部分——运行时,这些应用程序只会占用几十兆内存,而不是像典型的基于JVM的应用程序那样占用几百(或几千)兆。

下一步

我迫不及待地期待Spring Graal 0.8.0版本的发布,该版本将基于Spring Framework 5.3中的许多改进,并且可能包含一项功能,可以将以@Configuration为中心的Java配置转换为Spring的“函数式配置”,这不需要代理或反射,并且资源效率更高。我在另一期Spring Tips节目中探讨过函数式配置,那是三年多以前的事情了。

订阅Spring新闻通讯

通过Spring新闻通讯保持联系

订阅

抢占先机

VMware提供培训和认证,为你的进步注入强大动力。

了解更多

获取支持

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

了解更多

即将举行的活动

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

查看全部