将 Spring Boot与GraalVM原生映像的优点结合起来真的很酷。但是在Docker容器里也做这些魔术怎么样?在Heroku这样的云基础设施上运行那些本地应用程序怎么样?
再说一次“在我的机器上工作”!
在写第一篇关于 [将Spring Boot应用程序作为GraalVM本机映像运行]的文章时,我对今天我们尝试将Spring Boot与GraalVM本机映像一起使用的可能性感到非常兴奋。但是,无论我是在客户的网站上,还是在Fachhochschule Erfurt给我的学生讲课,我都会努力避免这种在我的机器上工作的困境。到目前为止,我们只在本地工作站上将Spring Boot应用程序编译成GraalVM本机映像。
既然我们已经到了2021年,我们不应该就此止步,而应该尝试使用某种容器来构建和运行我们的应用程序,对吧?我们应该借助一些持续集成的云平台来不断地做到这一点。最后,我们需要在某种云平台上部署和运行本机应用程序!
所以,首先要做的是——让我们弄清楚如何使用Docker将Spring Boot应用程序编译成GraalVM本机映像!
使用Docker将Spring Boot应用程序编译为GraalVM本机映像
在这里使用Docker最简单的方法是依赖Oracle的GraalVM Docker官方镜像。有趣的是,这个镜像既缺少Maven,也缺少原生镜像GraalVM插件。因此,让我们简单地将它们添加到镜像中,创建我们自己的Dockerfile。同样,所有代码示例都可以在GitHub上的示例项目中获得。
在这个博客文章系列的第一篇文章中,我们已经习惯了利用SDKMAN安装Maven。正如甲骨文官方的GraalVM Docker镜像是基于 oraclelinux:7-slim
,我们需要先安装 unzip
和 zip
。为了正常工作,SDKMAN需要两者:
FROM oracle/graalvm-ce:20.0.0-java11
# 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"; \
# Install Maven
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
# Always use source sdkman-init.sh before any command, so that we will be able to use 'mvn' command
ENTRYPOINT bash -c "source $HOME/.sdkman/bin/sdkman-init.sh && $0"
我们不应该忘记为Docker映像的用户启用 mvn
命令。因此,我们创建了一个稍微有趣的入口点,它总是在命令前面加上 source$HOME/.sdkman/bin/sdkman-init.sh
。定义了Dockerfile之后,我们应该用以下内容构建我们的镜像:
docker build . --tag=graalvm-ce:20.0.0-java11-mvn-native-image
构建完成后,我们可以在Docker容器中启动GraalVM本机映像编译。但是请稍候,下面的命令继承了第二个Docker卷定义 --volume“$HOME”/.m2:/root/.m2
。为什么?因为我真的想避免每次启动Docker容器时一遍又一遍地下载所有SpringMaven依赖项。通过这种装载,我们只需使用机器上缓存的Maven存储库:
docker run -it --rm \
--volume $(pwd):/build \
--workdir /build \
--volume "$HOME"/.m2:/root/.m2 \
graalvm-ce:20.0.0-java11-mvn-native-image ./compile.sh
第一个列 --volume$(pwd):/build
只是将Spring Boot应用程序的源代码(包括用于GraalVM本机映像编译的 .compile.sh
脚本)装载到Docker容器中。运行这个Docker构建,经过几分钟的繁重编译后,生成的 spring-boot-graal
本机应用程序应该已经准备好了。
防止java.lang.OutOfMemoryError错误
当我开始尝试Spring Boot应用程序的GraalVM本机映像编译时,我经常体验到 docker run
命令似乎需要很长时间才能完成。最后,一个 java.lang.OutOfMemoryError
错误被抛出到日志中,如下所示:
14:06:34.609 [ForkJoinPool-2-worker-3] DEBUG io.netty.handler.codec.compression.ZlibCodecFactory - -Dio.netty.noJdkZlibEncoder: false
Exception in thread "native-image pid watcher"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "native-image pid watcher"
在这种情况下,Docker引擎很可能无法使用足够的内存。在Mac的Docker安装中,默认值只有2.00GB。正如stackoverflow q&a的评论所述,您必须给Docker更多的内存,因为GraalVM本机映像编译过程实际上是RAM密集型的。分配Docker引擎大约9到12GB的RAM,我可以在Docker容器中进行编译:
如果一切正常,您应该在 /target/native-image
目录中找到本机编译的Spring Boot应用程序 spring-boot-graal 。因此,要运行我们的应用程序,只需使用 ./target/native-image/spring-boot-graal
:
$ ./spring-boot-graal
zsh: exec format error: ./spring-boot-graal
噢!原来这不管用!为什么?我们真的需要记住,我们是从Java应用程序编译本机可执行文件! 所以他们现在完全依赖平台了! 我们的Docker容器的基本映像将与我们的主机操作系统大不相同。我想这对所有的Java人来说都是新鲜事!从一开始我们就被告知Java是独立于平台的,这要归功于它的虚拟机。这个问题只有在这一点上我们才真正明白,我们开始在Docker容器中编译我们的应用程序。
这个问题的解决方案很简单:我们不仅在Docker容器中编译应用程序,而且在其中运行它们。
在Docker中运行本机Spring Boot应用程序
如果我们想在一个容器中运行本机Spring Boot