结合GraalVM与Spring Native的Spring Boot源码教程 | foojay

21-03-24 banq

在这篇文章中,我想检查一下从现有的Spring Boot应用程序生成Docker镜像有多么容易。

 

原理

GraalVM提供许多不同的功能。其中,称为Substrate VM的组件允许将常规字节码AOT编译为本地可执行文件。该过程从main构建时的方法开始“遍历”应用程序。Substrate VM会从生成的二进制文件中删除不遵循的代码。

对于Spring应用程序,这是一个大问题。该框架在运行时做了很多工作,例如,类路径扫描和反射。

解决此限制的常用方法是通过Graal VM提供的Java代理记录与在JVM上运行的应用程序的所有交互。运行结束时,代理将所有记录的交互转储到专用配置文件中:

  • 反射通道
  • 序列化的类
  • 代理接口
  • 资源和资源包
  • JNI

这些选项很引人注目,它允许人们从几乎所有可能的Java应用程序中创建本机镜像。但是,它也有一些缺点:

  • 提供代理的完整GraalVM发行版
  • 一个测试套件,用于测试应用程序的每个细节
  • 在每个新发行版中执行套件并创建配置文件的过程

 

尝试使用Spring Native

秉承真正的Spring精神,Spring Native旨在简化配置。主要思想是直接在代码中提供“提示”。一个专用的插件将使用这些提示并生成所需的配置文件。Spring团队已经为框架代码提供了这些提示。如果需要,您还可以注释应用程序的代码。

为了试验Spring Native,我使用了从命令式到响应式的演示代码。它为AOT提供了两个挑战:

  • 这是一个Spring应用程序
  • 我使用批注,并依赖于运行时反射和类路径扫描
  • 我用Kotlin
  • 我使用内存数据库H2
  • 最后,我将序列化的实体缓存在嵌入式Hazelcast实例中。这很重要,因为序列化是GraalVM最新版本提供的改进的一部分。

 

第一步是使应用程序与GraalVM兼容。我们需要从代码中删除Blockhound。Blockhound允许验证没有阻止代码在不需要的地方运行。它是需要JDK而不是JRE的Java代理。这对于演示非常有用,但与生产应用程序无关。

在撰写本文时,GraalVM提供了Java的两个版本:8和11。由于该演示最初使用Java 14,因此我们需要将Java的版本从14降级为11。 。

 

第二步是向POM添加一个依赖项和一个插件。我将两者都放入专用的配置文件中,以便应用程序可以“正常”运行。这些托管在Maven Central外部的专用Spring存储库中。

<profiles>
  <profile>
    <id>native</id>
    <build>
      <plugins>
        <plugin>
          <groupId>org.springframework.experimental</groupId>
          <artifactId>spring-aot-maven-plugin</artifactId>
          <version>0.9.0</version>
          <executions>
            <execution>
              <id>generate</id>
              <goals>
                <goal>generate</goal>
              </goals>
            </execution>
          </executions>
        </plugin>
      </plugins>
    </build>
    <dependencies>
      <dependency>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-native</artifactId>
        <version>0.9.0</version>
      </dependency>
    </dependencies>
  </profile>
</profiles>
<repositories>
  <repository>
    <id>spring-release</id>
    <url>https://repo.spring.io/release</url>
  </repository>
</repositories>
<pluginRepositories>
  <pluginRepository>
    <id>spring-release</id>
    <url>https://repo.spring.io/release</url>
  </pluginRepository>
</pluginRepositories>

使用此配置代码片段,可以使用native配置文件创建本机镜像:

mvn spring-boot:build-image -Pnative

 

第一道障碍

AOT编译过程需要很长时间。它应该成功(尽管它显示一些堆栈跟踪),最后,它会生成一个Docker映像。您可以使用以下命令运行映像:

docker run -it --rm -p8080:8080 docker.io/library/imperative-to-reactive:1.0-SNAPSHOT

不幸的是,此操作失败,但有以下异常:

Caused by: java.lang.ClassNotFoundException: org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryConfigurations$PooledConnectionFactoryCondition
    at com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:60) ~[na:na]
    at java.lang.Class.forName(DynamicHub.java:1260) ~[na:na]
    at org.springframework.util.ClassUtils.forName(ClassUtils.java:284) ~[na:na]
    at org.springframework.util.ClassUtils.resolveClassName(ClassUtils.java:324) ~[na:na]
    ... 28 common frames omitted

看来Spring Native没有发现这个类。我们需要自己添加它。有两种方法可以做到这一点:

  1. 通过Spring Native依赖中的注释
  2. 或通过标准GraalVM配置文件

在上一节中,我选择在专用的Maven配置文件中设置Spring Native。因此,我们使用常规配置文件:

[
{
  "name":"org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryConfigurations$PooledConnectionFactoryCondition",
  "methods":[{"name":"<init>","parameterTypes":[] }]
}
]

再次构建并运行将产生以下结果:

Caused by: java.lang.NoSuchFieldException: VERSION
    at java.lang.Class.getField(DynamicHub.java:1078) ~[na:na]
    at com.hazelcast.instance.BuildInfoProvider.readStaticStringField(BuildInfoProvider.java:139) ~[na:na]
    ... 79 common frames omitted

这次,缺少与Hazelcast相关的静态字段。我们需要配置缺少的字段,重新构建并重新运行。它仍然失败。冲洗并重复:我将为您省去细节;如果您有兴趣,请检查github

因为我使用XML配置Hazelcast,所以需要整个XML初始化过程。在某些时候,我们还需要在本机映像中保留一个资源包:

{
"bundles":[
  {"name":"com.sun.org.apache.xml.internal.serializer.XMLEntities"}
]
}

不幸的是,构建仍然失败。尽管我们正确配置了类,但它仍然是与XML相关的异常!

Caused by: java.lang.RuntimeException: internal error
    at com.sun.org.apache.xerces.internal.impl.dv.xs.XSSimpleTypeDecl.applyFacets1(XSSimpleTypeDecl.java:754) ~[na:na]
    at com.sun.org.apache.xerces.internal.impl.dv.xs.BaseSchemaDVFactory.createBuiltInTypes(BaseSchemaDVFactory.java:207) ~[na:na]
    at com.sun.org.apache.xerces.internal.impl.dv.xs.SchemaDVFactoryImpl.createBuiltInTypes(SchemaDVFactoryImpl.java:47) ~[org.hazelcast.cache.ImperativeToReactiveApplicationKt:na]
    at com.sun.org.apache.xerces.internal.impl.dv.xs.SchemaDVFactoryImpl.<clinit>(SchemaDVFactoryImpl.java:42) ~[org.hazelcast.cache.ImperativeToReactiveApplicationKt:na]
    at com.oracle.svm.core.classinitialization.ClassInitializationInfo.invokeClassInitializer(ClassInitializationInfo.java:375) ~[na:na]
    at com.oracle.svm.core.classinitialization.ClassInitializationInfo.initialize(ClassInitializationInfo.java:295) ~[na:na]
    ... 82 common frames omitted

 

切换到YAML

XML是一个巨大的野兽,我还不足以理解上述异常背后的确切原因。工程还涉及找到正确的解决方法。在这种情况下,我决定从XML配置切换到YAML配置。无论如何都很简单:

hazelcast:
  instance-name: hazelcastInstance

我们不应忘记将以上资源添加到资源配置文件中:

{
"resources":{
  "includes":[
    {"pattern":"hazelcast.yaml"}
  ]}
}

{ "resources":{ "includes":[ {"pattern":"hazelcast.yaml"} ]} }

由于在运行时缺少字符集,我们还需要在构建时初始化YAML阅读器:

Args = --initialize-at-build-time=com.hazelcast.org.snakeyaml.engine.v2.api.YamlUnicodeReader

我们需要继续添加几个与Hazelcast有关的反射访问类。

 

代理缺失

至此,我们在运行时遇到了一个全新的例外!

Caused by: com.oracle.svm.core.jdk.UnsupportedFeatureError: Proxy class defined by interfaces [interface org.hazelcast.cache.PersonRepository, interface org.springframework.data.repository.Repository, interface org.springframework.transaction.interceptor.TransactionalProxy, interface org.springframework.aop.framework.Advised, interface org.springframework.core.DecoratingProxy] not found. Generating proxy classes at runtime is not supported. Proxy classes need to be defined at image build time by specifying the list of interfaces that they implement. To define proxy classes use -H:DynamicProxyConfigurationFiles=<comma-separated-config-files> and -H:DynamicProxyConfigurationResources=<comma-separated-config-resources> options.
    at com.oracle.svm.core.util.VMError.unsupportedFeature(VMError.java:87) ~[na:na]
    at com.oracle.svm.reflect.proxy.DynamicProxySupport.getProxyClass(DynamicProxySupport.java:113) ~[na:na]
    at java.lang.reflect.Proxy.getProxyConstructor(Proxy.java:66) ~[na:na]
    at java.lang.reflect.Proxy.newProxyInstance(Proxy.java:1006) ~[na:na]
    at org.springframework.aop.framework.JdkDynamicAopProxy.getProxy(JdkDynamicAopProxy.java:126) ~[na:na]
    at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110) ~[na:na]
    at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(RepositoryFactorySupport.java:309) ~[na:na]
    at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.lambda$afterPropertiesSet$5(RepositoryFactoryBeanSupport.java:323) ~[org.hazelcast.cache.ImperativeToReactiveApplicationKt:2.4.5]
    at org.springframework.data.util.Lazy.getNullable(Lazy.java:230) ~[na:na]
    at org.springframework.data.util.Lazy.get(Lazy.java:114) ~[na:na]
    at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:329) ~[org.hazelcast.cache.ImperativeToReactiveApplicationKt:2.4.5]
    at org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactoryBean.afterPropertiesSet(R2dbcRepositoryFactoryBean.java:167) ~[org.hazelcast.cache.ImperativeToReactiveApplicationKt:1.2.5]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1845) ~[na:na]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1782) ~[na:na]
    ... 46 common frames omitted

这是关于代理的,非常简单。在这种情况下,Spring DataPersonRepository通过另外两个组件代理该接口。这些都在堆栈跟踪中列出。GraalVM可以处理代理,但需要您配置它们。

[
  ["org.hazelcast.cache.PersonRepository",
   "org.springframework.data.repository.Repository",
   "org.springframework.transaction.interceptor.TransactionalProxy",
   "org.springframework.aop.framework.Advised",
   "org.springframework.core.DecoratingProxy"]
]

 

现在进行序列化

使用以上配置,镜像应该成功启动,这让我感到内部温暖。

如果我们此时访问端点,则该应用程序将抛出运行时异常:

java.lang.IllegalStateException: Required identifier property not found for class org.hazelcast.cache.Person!
    at org.springframework.data.mapping.PersistentEntity.getRequiredIdProperty(PersistentEntity.java:105) ~[na:na]

AOT省略了序列化的类,我们需要对其进行管理。至于代理,GraalVM知道该怎么做,但是它需要显式配置。让我们配置Person类及其属性的类:

[
{"name":"org.hazelcast.cache.Person"},
{"name":"java.time.LocalDate"},
{"name":"java.lang.String"},
{"name":"java.time.Ser"}
]

 

成功!

现在,我们可以(终于!)curl运行镜像了。

curl http://localhost:8080/person/1
curl http://localhost:8080/person/1

 

结论

尽管有Spring Boot的所有“魔力”,Spring Native还是可以立即使用GraalVM的大多数必需配置。上述步骤主要针对应用程序的代码。

尽管该应用程序只是一个演示应用程序,但也不是一件容易的事。尽管进行了序列化,内存缓存和内存数据库,但希望看到本机镜像仍能正常工作。

当然,并非一切都完美:构建会显示一些异常,一些日志在运行时会重复,而且Hazelcast节点似乎无法加入集群。

但是,这已经足够好了,尤其是在我花费的时间上。我很想尝试1.0版本。同时,我可能会更仔细地研究其余警告。

这篇文章的完整源代码可以在GitHub找到

 

猜你喜欢