結合GraalVM與Spring Native的Spring Boot原始碼教程 | foojay
在這篇文章中,我想檢查一下從現有的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沒有發現這個類。我們需要自己新增它。有兩種方法可以做到這一點:
- 通過Spring Native依賴中的註釋
- 或通過標準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版本。同時,我可能會更仔細地研究其餘警告。
相關文章
- Spring Boot支援Java 16和新的Java記錄原始碼教程 | foojaySpring BootJava原始碼
- Spring Boot的Clean架構教程與原始碼 - BaeldungSpring Boot架構原始碼
- Spring Boot 啟動原始碼解析結合Spring Bean生命週期分析Spring Boot原始碼Bean
- 通往Spring Boot本機應用的道路:Spring GraalVM Native 0.7.0可以使用了 - spring.ioSpring BootLVM
- Spring Boot系列(四):Spring Boot原始碼解析Spring Boot原始碼
- Spring Boot的微服務分散聚集模式教程與原始碼 - vinsguruSpring Boot微服務模式原始碼
- Spring Boot本地原生映象Native-image原始碼 - frankelSpring Boot原始碼
- Spring Boot中使用gRPC與Protobuf驗證教程原始碼Spring BootRPC原始碼
- spring boot碼雲原始碼Spring Boot原始碼
- Spring Boot系列(三):Spring Boot整合Mybatis原始碼解析Spring BootMyBatis原始碼
- Spring Boot與Kafka + kafdrop結合使用的簡單示例Spring BootKafka
- 玩轉spring boot——結合dockerSpring BootDocker
- 玩轉spring boot——結合redisSpring BootRedis
- Spring Boot中實現規則引擎原始碼教程Spring Boot原始碼
- Spring Boot + JPA DataTable原始碼Spring Boot原始碼
- 使用Spring Boot排程WebSocket推送的教程和原始碼 - BaeldungSpring BootWeb原始碼
- Spring Boot 教程 - ElasticsearchSpring BootElasticsearch
- spring boot 結合Redis 實現工具類Spring BootRedis
- 玩轉spring boot——結合JPA事務Spring Boot
- 玩轉spring boot——結合JPA入門Spring Boot
- java工程管理系統原始碼+spring cloud + spring bootJava原始碼CloudSpring Boot
- Spring Boot2 系列教程(三)理解 Spring BootSpring Boot
- 精盡Spring Boot原始碼分析 - 序言Spring Boot原始碼
- Spring Boot 自動配置 原始碼分析Spring Boot原始碼
- Spring原始碼教程02--Spring的IoC容器分析Spring原始碼
- 基於Spring Batch的Spring Boot的教程 - BaeldungBATSpring Boot
- Spring Native釋出:使用GraalVM將Spring應用編譯為本機映象SpringLVM編譯
- 企業工程管理系統原始碼+spring cloud + spring boot原始碼CloudSpring Boot
- java+spring cloud + spring boot工程管理系統原始碼JavaCloudSpring Boot原始碼
- 玩轉spring boot——結合AngularJs和JDBCSpring BootAngularJSJDBC
- 玩轉spring boot——結合jQuery和AngularJsSpring BootjQueryAngularJS
- Spring Boot Security配置教程Spring Boot
- Spring Boot-Redis教程Spring BootRedis
- 在GraalVM中部署執行Spring Boot應用 - Indrek OtsLVMSpring Boot
- Spring Boot 揭祕與實戰 原始碼分析 - 工作原理剖析Spring Boot原始碼
- Spring Boot原始碼分析-啟動過程Spring Boot原始碼
- 如何才能讓Spring Boot與RabbitMQ結合實現延遲佇列Spring BootMQ佇列
- 精盡MyBatis原始碼分析 - Spring-Boot-Starter 原始碼分析MyBatis原始碼Springboot