我一直在玩GraalVM AOT编译功能,因为我知道它。作为一个长期的Spring爱好者,我仔细地观察了Tanzu的工程师们为使Spring-AOT兼容所做的努力。最近,他们宣布了测试版的集成。在这篇文章中,我想看看制作一个(正在工作的!)来自现有Spring引导应用程序的Docker image镜像。
GraalVM提供了许多不同的特性。其中,名为substratevm
的组件允许AOT将常规字节码编译为本机可执行文件。这个过程在构建时从main
方法开始“遍历”应用程序。底层VM省略了它在结果二进制文件中没有遵循的代码。
对于Spring应用程序来说,这是一个大问题。这个框架在运行时做了很多工作,例如类路径扫描和反射。
处理此限制的通常方法是通过graalvm提供的Java代理记录与JVM上运行的应用程序的所有交互。在运行结束时,代理将所有记录的交互转储到专用配置文件中:
- 反射通道
- 序列化类
- 代理接口
- 资源和资源包
- JNI
这个选项很有吸引力,它允许人们从几乎所有可能的Java应用程序中创建本机映像。不过,根据你的需要,它也有一些缺点:
1. 提供代理的完整的GraalVM发行版
2. 测试应用程序每个角落的测试套件
3. 执行套件并在每个新版本中创建配置文件的进程
这些项目都不复杂,但过程耗时且容易出错。它可以自动化,但总有一个风险,即特定的版本忘记测试特定的用例,并在部署时崩溃。
试用Spring Native
在真正的Spring精神中,Spring Native旨在简化配置。其主要思想是直接在代码中提供“提示”。专用插件将使用这些提示并生成所需的配置文件。Spring团队已经为框架的代码提供了这些提示。如果需要,还可以对应用程序的代码进行注释。
为了使用spring native进行实验,我使用了命令式的反应式演示代码。它为AOT带来了一些挑战:
- 这是一个Spring应用程序
- 我使用注释并依赖于运行时反射和类路径扫描
- 我用Kotlin
- 我使用H2,一个内存数据库
- 最后,我在嵌入式Hazelcast实例中缓存序列化的实体。这一点很重要,因为序列化是GraalVM最新版本提供的改进的一部分。
第一步是使应用程序与GraalVM兼容。我们需要从代码中删除Blockhound
。Blockhound
允许验证没有阻塞代码在不需要的地方运行。它是一个需要JDK而不是JRE的Java代理。它非常适合演示,但与生产应用程序无关。
在撰写本文时,GraalVM提供了两个版本的Java,8和11。由于演示最初使用Java14,我们需要将Java的版本从14降到11。由于该项目是在Kotlin无论如何,它没有其他影响。
第二步是向POM添加依赖项和插件。我把这两个都放到一个专用的配置文件中,这样应用程序就可以“正常”运行了。它们托管在Maven Central之外的专用Spring存储库中。
pom.xml
<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>
使用此配置片段,可以使用本机配置文件创建本机映像:
mvn spring-boot:build-image -Pnative
第一个障碍
AOT编译过程需要很长时间。它应该会成功(尽管它显示了一些堆栈跟踪),并最终生成一个Docker映像。可以使用以下工具运行图像:
docker run -it --rm -p8080:8080 docker.io/library/imperative-to-reactive:1.0-SNAPSHOT
我使用--rm
,这样在容器运行之后就可以删除它,并且不会浪费磁盘空间
不幸的是,此操作失败,出现以下异常:
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。因此,让我们使用常规配置文件。
META-INF/native-image/org.hazelcast.cache/imperative-to-reactive/reflect-config.json
[
{
"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相关的静态字段丢失。我们需要配置丢失的字段,重新构建并重新运行。
因为我用XML配置Hazelcast,所以需要整个XML初始化过程。有时,我们还需要在本机映像中保留一个资源包:
META-INF/native-image/org.hazelcast.cache/imperative-to-reactive/resource-config.json
{
"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.yaml
hazelcast:
instance-name: hazelcastInstance
我们不应该忘记将上述资源添加到资源配置文件中:
META-INF/native-image/org.hazelcast.cache/imperative-to-reactive/resource-config.json
{
"resources":{
"includes":[
{"pattern":"hazelcast.yaml"}
]}
}
由于运行时缺少字符集,我们还需要在生成时初始化YAML读取器:
native-image.properties
Args = --initialize-at-build-time=com.hazelcast.org.snakeyaml.engine.v2.api.YamlUnicodeReader
我们需要继续添加几个reflectively
访问类,它们都与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数据通过几个其他组件代理PersonRepository
接口。这些都列在堆栈跟踪中。GraalVM可以处理代理,但需要您配置它们。
META-INF/native-image/org.hazelcast.cache/imperative-to-reactive/proxy-config.json
[
["org.hazelcast.cache.PersonRepository",
"org.springframework.data.repository.Repository",
"org.springframework.transaction.interceptor.TransactionalProxy",
"org.springframework.aop.framework.Advised",
"org.springframework.core.DecoratingProxy"]
]
现在开始序列化
通过以上配置,镜像image应该可以成功启动,这让我感觉到内心的温暖:
2021-03-18 20:22:28.305 INFO 1 --- [ main] o.s.nativex.NativeListener : This application is bootstrapped with code generated with Spring AOT
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.4.3)
...blah blah blah...
2021-03-18 20:22:30.654 INFO 1 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port 8080
2021-03-18 20:22:30.655 INFO 1 --- [ main] o.s.boot.SpringApplication : Started application in 2.355 seconds (JVM running for 2.358)
如果此时访问终结点,应用程序将引发运行时异常:
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
类及其属性的类:
META-INF/native-image/org.hazelcast.cache/imperative-to-reactive/serialization-config.json
[
{"name":"org.hazelcast.cache.Person"},
{"name":"java.time.LocalDate"},
{"name":"java.lang.String"},
{"name":"java.time.Ser"}
]
成功!
现在,我们终于可以了curl
运行image镜像了:
curl http://localhost:8080/person/1
curl http://localhost:8080/person/1
输出返回预期结果:
2021-03-15 09:54:18.994 INFO 1 --- [onPool-worker-3] o.h.c.CachingService : Person with id 1 not found in cache
2021-03-15 09:54:19.108 INFO 1 --- [onPool-worker-3] o.h.c.CachingService : Person with id 1 put in cache
2021-03-15 09:54:46.694 INFO 1 --- [onPool-worker-3] o.h.c.CachingService : Person with id 1 found in cache
我们需要将Sort
类配置为使用根“/
”端点,它一次检索所有实体。
结论
尽管SpringBoot有着所有的“魔力”,SpringNative还是可以直接处理GraalVM所需的大部分配置。上述步骤主要针对应用程序的代码。
虽然该应用程序只是一个演示应用程序,但它也不是微不足道的。尽管存在序列化、内存缓存和内存数据库,本机映像仍然可以正常工作。
当然,并不是所有的事情都是完美的:构建显示了一些异常,一些日志在运行时被复制,而且Hazelcast节点似乎不能加入集群。
然而,这已经足够好了,尤其是关于我花了多少时间。我很想试试1.0版。同时,我可能会更仔细地调查其余的警告。
本文的完整源代码可以在Github上找到:https://github.com/hazelcast-demos/imperative-to-reactive/tree/native
SpringNative文档:https://docs.spring.io/spring-native/docs/current/reference/htmlsingle/
除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/1779.html
暂无评论