結合GraalVM與Spring Native的Spring Boot原始碼教程 | foojay

發表於2021-03-24

在這篇文章中,我想檢查一下從現有的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找到

 

相關文章