本文基于上一篇:在Docker&Heroku上运行Spring Boot GraalVM本机映像
新的spring-graalvm-native
原生版本0.7.1和graalvm 20.1.0都进行了大量的优化!本机映像命令的配置变得容易得多。那么让我们来看一下Spring Boot GraalVM本机映像编译的native-image-maven-plugin
。
新的Spring特性0.7.1版本&GraalVM 20.1.0
Spring的队伍真的很快!几天前,他们发布了spring-graalvm-native
原生项目的新版本0.7.1,它再次优化了我们将springboot应用程序编译成graalvm原生映像的方式。如果你想知道更多关于如何使用它的知识,请查看本博客系列的第一篇文章。
随着版本0.7.0的发布,Spring特性项目从Spring-graal-native
重命名为Spring-graalvm-native
!因此,在访问项目、文档或从Spring里程碑存储库下载最新的Maven依赖项时,不要感到困惑。
Spring实验项目spring-graalvm-native
的最新版本现在基于springboot2.3.0.release和graalvm20.1.0。它改进了对Kotlin、Spring数据MongoDB和日志的支持。此外,它还附带了专用的功能性Spring应用程序支持,并进一步减少了内存占用。另外,GraalVM团队发布了新的GraalVM版本20.1.0,并进行了大量的改进——还包括Spring。
这个博客系列的示例项目的pom.xml已经更新:https://github.com/jonashackt/spring-boot-graalvm/blob/master/pom.xml。要使用新版本,只需更新Maven依赖项(不要忘了还准备好Spring Milestone repositories):https://repo.spring.io/milestone/org/springframework/experimental/spring-graalvm-native/
<dependencies>
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-graalvm-native</artifactId>
<version>0.7.1</version>
</dependency>
...
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
</pluginRepositories>
由于我们现在也可以利用Docker进行Spring Boot本机映像编译,因此示例项目的Dockerfile现在也使用最新的GraalVM版本:
FROM oracle/graalvm-ce:20.1.0-java11
从编译脚本到本机映像maven插件
spring-graalvm-native本机项目的新版本还附带了一些更微妙的变化,使得将springboot应用程序编译成graalvm本机映像变得更加容易。其中一个更改是关于本机映像命令所需的配置选项。许多参数现在只是默认启用。所以我们不需要再明确地定义它们了。尤其是--no-server
和 --no-fallback
,在使用新版本时可以省略。示例springwebflux应用程序的最后一个本机映像命令如下所示(有关更多详细信息,请参阅示例项目的compile.sh
):
GRAALVM_VERSION=`native-image --version`
echo "[-->] Compiling Spring Boot App '$ARTIFACT' with $GRAALVM_VERSION"
time native-image \
-J-Xmx4G \
-H:+TraceClassInitialization \
-H:Name=$ARTIFACT \
-H:+ReportExceptionStackTraces \
-Dspring.graal.remove-unused-autoconfig=true \
-Dspring.graal.remove-yaml-support=true \
-cp $CP $MAINCLASS;
但是有了一个更简单的native-image
命令,现在是查看native-image-maven-plugin
的好时机。
不要混淆org.graalvm.nativeimage.native-image-maven-plugin
的包名!这个插件还有一个更老的版本,叫做com.oracle.substratevm.native-image-maven-plugin
,不再维护了。
使用本机映像maven插件将主要取代第一篇文章中描述的步骤6、7和8,准备SpringBoot成为一个通用的本机映像友好型。但如果出了什么问题,知道幕后发生了什么仍然是件好事。我认为这也是Spring团队为每个示例项目准备compile.sh
脚本的原因。
使用本机映像maven插件
为了使用这个插件,我们用一个名为native
的Maven概要文件扩展pom.xml
,如下所示:
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.nativeimage</groupId>
<artifactId>native-image-maven-plugin</artifactId>
<version>20.1.0</version>
<configuration>
<buildArgs>-J-Xmx4G -H:+TraceClassInitialization -H:+ReportExceptionStackTraces
-Dspring.graal.remove-unused-autoconfig=true -Dspring.graal.remove-yaml-support=true
</buildArgs>
<imageName>${project.artifactId}</imageName>
</configuration>
<executions>
<execution>
<goals>
<goal>native-image</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</profile>
</profiles>
buildArgs
标记在这里非常重要!我们需要为Spring启动应用程序配置成功运行native-image
命令所需的一切,就像compile.sh
中已经使用的那样。同样,spring-boot-maven-plugin
也需要在maven本机概要文件中,因为本机映像maven插件需要它才能正常工作。
我们可以省略-cp $CP $MAINCLASS
参数,因为在使用Maven时已经提供了它。添加${project.artifactId}
也是一个好主意,以便使用我们的artifactId作为结果可执行文件的名称。否则,我们将得到一个完全限定的类名,如io.jonashackt.springbootgraal.springboothelloapplication
。
正如compile.sh
脚本中已经使用的那样,我们还需要准备好start class
属性:
<properties>
<start-class>io.jonashackt.springbootgraal.SpringBootHelloApplication</start-class>
...
</properties>
这可能就是我们需要做的一切。但是等等!我遇到了这个错误…
防止“找不到默认构造函数无法实例化java.lang.NoSuchMethodException”错误
使用带有mvn-Pnative clean
包的新概要文件运行Maven构建成功地编译了我的Spring Boot应用程序。但当我试着运行它时,应用程序没有正常启动,出现了以下错误:
./target/spring-boot-graal
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot ::
Jun 05, 2020 10:46:27 AM org.springframework.boot.StartupInfoLogger logStarting
INFO: Starting application on PikeBook.fritz.box with PID 33047 (started by jonashecht in /Users/jonashecht/dev/spring-boot/spring-boot-graalvm/target)
Jun 05, 2020 10:46:27 AM org.springframework.boot.SpringApplication logStartupProfileInfo
INFO: No active profile set, falling back to default profiles: default
Jun 05, 2020 10:46:27 AM org.springframework.context.support.AbstractApplicationContext refresh
WARNING: Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springBootHelloApplication': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.jonashackt.springbootgraal.SpringBootHelloApplication]: No default constructor found; nested exception is java.lang.NoSuchMethodException: io.jonashackt.springbootgraal.SpringBootHelloApplication.<init>()
Jun 05, 2020 10:46:27 AM org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener logMessage
INFO:
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
Jun 05, 2020 10:46:27 AM org.springframework.boot.SpringApplication reportFailure
SEVERE: Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springBootHelloApplication': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.jonashackt.springbootgraal.SpringBootHelloApplication]: No default constructor found; nested exception is java.lang.NoSuchMethodException: io.jonashackt.springbootgraal.SpringBootHelloApplication.<init>()
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1320)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1214)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:557)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:895)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:878)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550)
at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:62)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
at io.jonashackt.springbootgraal.SpringBootHelloApplication.main(SpringBootHelloApplication.java:10)
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.jonashackt.springbootgraal.SpringBootHelloApplication]: No default constructor found; nested exception is java.lang.NoSuchMethodException: io.jonashackt.springbootgraal.SpringBootHelloApplication.<init>()
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:83)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1312)
... 18 more
Caused by: java.lang.NoSuchMethodException: io.jonashackt.springbootgraal.SpringBootHelloApplication.<init>()
at java.lang.Class.getConstructor0(DynamicHub.java:3349)
at java.lang.Class.getDeclaredConstructor(DynamicHub.java:2553)
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:78)
... 19 more
我很难搞清楚这个!尤其是因为compile.sh
的工作方式与本机映像maven插件完全没有区别。参数相同!但最后我发现了一个不同点——这都是关于Spring特性计算的spring.components
!
运行compile.sh
脚本时,Spring特性会动态计算一个spring.components
文件,其中包含示例项目的3个类,这些类用一个典型的Spring@Component
注释:
$ ./compile.sh
...
Excluding 104 auto-configurations from spring.factories file
Found no META-INF/spring.components -> synthesizing one...
Computed spring.components is
vvv
io.jonashackt.springbootgraal.HelloRouter=org.springframework.stereotype.Component
io.jonashackt.springbootgraal.HelloHandler=org.springframework.stereotype.Component
io.jonashackt.springbootgraal.SpringBootHelloApplication=org.springframework.stereotype.Component
^^^
Registered 3 entries
Configuring initialization time for specific types and packages:
#69 buildtime-init-classes #21 buildtime-init-packages #28 runtime-init-classes #0 runtime-init-packages
使用native-image-maven-plugin
,编译过程没有成功计算spring.components
文件,因此无法识别三个带注释的类:
$ mvn -Pnative clean package
...
Excluding 104 auto-configurations from spring.factories file
Found no META-INF/spring.components -> synthesizing one...
Computed spring.components is
vvv
^^^
Registered 0 entries
Configuring initialization time for specific types and packages:
#69 buildtime-init-classes #21 buildtime-init-packages #28 runtime-init-classes #0 runtime-init-packages
spring-context-indexer来拯救!
但是为什么我们需要在spring.components文件中包含所有这些类呢?这是因为我们从运行在SubstrateVM上的Spring Boot应用程序编译GraalVM本机映像,SubstrateVM的特性集非常小。使用本机映像不支持在运行时使用动态组件扫描!
这个问题的解决方案是在构建时进行组件扫描!一个已经做了相当长一段时间的实用程序是spring-context-indexer
。使用native-image-maven-plugin
,我们必须在pom.xml中显式包含spring-context-indexer
依赖项:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
</dependency>
现在运行Maven构建,创建了包含我们需要的3个类的文件target/classes/META_NF/spring.components
:
io.jonashackt.springbootgraal.HelloHandler=org.springframework.stereotype.Component
io.jonashackt.springbootgraal.HelloRouter=org.springframework.stereotype.Component
io.jonashackt.springbootgraal.SpringBootHelloApplication=org.springframework.stereotype.Component
最后,我们的Maven构建工作如预期,并执行本地镜像编译像一个魅力!只需使用以下命令运行构建:
$ mvn -Pnative clean package
有关使用Maven编译Spring Boot GraalVM本机映像的完整示例,请查看这个TravisCI构建:https://travis-ci.com/github/jonashackt/spring-boot-graalvm/jobs/347877683
与Docker一起使用本机映像maven插件
正如我们在上一篇文章中了解到的,使用Docker和Heroku运行Spring Boot GraalVM本机映像,使用Docker编译Spring Boot本机映像是一个很好的组合。如果您遵循了本文中的所有步骤,并使用本机概要文件扩展pom.xml,那么将native-image-maven-plugin
与Docker一起使用应该很容易。让我们看看Dockerfile:
# Simple Dockerfile adding Maven and GraalVM Native Image compiler to the standard
# https://hub.docker.com/r/oracle/graalvm-ce image
FROM oracle/graalvm-ce:20.1.0-java11
ADD . /build
WORKDIR /build
# For SDKMAN to work we need unzip & zip
RUN yum install -y unzip zip
RUN \
# Install SDKMAN
curl -s "https://get.sdkman.io" | bash; \
source "$HOME/.sdkman/bin/sdkman-init.sh"; \
sdk install maven; \
# Install GraalVM Native Image
gu install native-image;
RUN source "$HOME/.sdkman/bin/sdkman-init.sh" && mvn --version
RUN native-image --version
RUN source "$HOME/.sdkman/bin/sdkman-init.sh" && mvn -Pnative clean package
# We use a Docker multi-stage build here in order to only take the compiled native Spring Boot App from the first build container
FROM oraclelinux:7-slim
MAINTAINER Jonas Hecht
# Add Spring Boot Native app spring-boot-graal to Container
COPY --from=0 "/build/target/spring-boot-graal" spring-boot-graal
# Fire up our Spring Boot Native app by default
CMD [ "sh", "-c", "./spring-boot-graal -Dserver.port=$PORT" ]
在这里我们不需要做太多的更改——我们只需要使用Maven命令mvn -Pnative clean package
,而不是compile.sh。此外,GraalVM基本映像也更新为oracle/GraalVM-ce:20.1.0-java11。如果您关注这个博客系列的文章,那么您还需要更改从这个多阶段Docker构建中的第一个构建容器复制本机映像的位置。由于我们使用的是Maven插件,因此生成的spring-boot-graal
只驻留在/build/target/
中。
使用Docker build运行--tag=spring-boot-graal
,然后在容器内通过以下方式启动本机编译的spring boot应用程序:
docker run -p 8080:8080 spring-boot-graal
使用本机映像maven插件编译我们的Spring Boot GraalVM本机映像很有趣!
尝试使用像Spring Boot GraalVM本机映像支持这样目前正在进行大量开发的技术有时会遇到挑战。在这里使用bash脚本来更深刻地理解幕后发生的事情绝对是有意义的。尤其是如果我们需要为编译创建一个正常工作的本机映像命令!
但正如前面所说的,Spring团队确实做得很好,而且Spring实验项目springgraalvm native的每一个版本所要求的配置都变得越来越简单。到了更稳定的版本,开始使用native-image-maven-plugin
肯定是个好主意,我们已经习惯了,同时使用其他基于GraalVM的框架,比如Quarkus.io。正如我以前的同事Benedikt Ritter正确地说的,我们应该使用比bash脚本更现代的方式来构建我们今天的应用程序。
原文地址:https://blog.codecentric.de/en/2020/06/spring-boot-graalvm-native-image-maven-plugin/
除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/1979.html
暂无评论