为什么SpringBoot胖Jar不再流行?

公平地说,我有时会怀念 JavaEE 流行的日子。

当然,当时的情况很复杂,但整个 JavaEE 平台设计合理,符合企业开发的需要。

我可以很轻松地将当时的 JavaEE 应用服务器与现代 Kubernetes 架构进行比较,后者现在也有同样的复杂性、安全性和可维护性问题,甚至可能更糟。而且与 JavaEE 时代不同的是,它没有真正的替代方案。

当时的 JavaEE 和现在的 Kubernetes 都试图(但都失败了)将开发人员从构建可扩展分布式应用的复杂性中解脱出来。

事实上,还有更多的类比,我发现我们现在正在绕一个大圈,先是尝试应用服务器集群,然后是微服务、容器、Kubernetes,现在又回到了模块化单体,而使用类似应用服务器这样允许多个应用/模块并排部署的东西,可能并不是那么疯狂的想法。

考虑到 uber jar 在容器镜像中的效率太低,而将应用程序与其依赖关系分开部署在一个单独的镜像层中更有意义,你就会再次想到单独的应用程序服务器,只不过是在无休止的螺旋式上升的另一个层次上。

JavaEE 规范成功地度过了 Spring 占据主导地位的时代,现在可能会迎来复兴。不知何故,基于 JavaEE 标准的 Quarkus 如今看起来比 Spring 更现代。

欢迎回到新旧 JavaEE 世界。

网友讨论
JEE 背后的许多设计决策都是考虑垂直扩展,容器和微服务有利于水平扩展,不需要应用程序服务器的复杂性。与之互补的是,像 kubernetes 这样的东西使集群充当平台,有可能比大型应用程序服务器规模更大,并且成本更低(许多小型机器与一台大型专用机器)。

==================

如果您想在容器中运行 JVM 应用程序,以获得它能带来的所有好处(可扩展性、声明式方法、自动重启......),那么您绝不会想运行一个完整的应用服务器。

在容器中运行一个完整的应用服务器是没有意义的:它很重,启动速度慢,而且失去了应用服务器的优势。

这不是唯一的原因,因为在容器出现之前,人们就已经开始远离应用服务器了,但这无疑 "扼杀 "了应用服务器。

==================
这其实是个很好的问题,考虑到 Spring Boot 捆绑了Web服务器--我在 Docker 中对捆绑了 Wildfly 服务器的 Jakarta EE 应用程序也做了同样的处理,机制相同,结果相同。

但不管怎么说,Spring Boot 已经过时了,Micronaut、Quarkus 和 GraalVM 的本地可执行文件才是未来的趋势--速度更快,占用内存更少。

=================

应用服务器的最大优点是,你可以用一个 JVM 来管理一切。最大的缺点是只有 1 个 JVM。如果我运行 10 个应用程序,其中一个占用了 JVM 的所有可用内存,那么就会有 10 个应用程序崩溃。而在容器中,尽管内存效率较低,但一个应用程序崩溃也只是该应用程序崩溃。

甚至在容器出现之前,我们公司就遇到过这个问题。我们编写了自己的调度程序和 Java 运行程序,因为这比让大型 tomcat 服务器运行所有程序要简单得多。结果,我们的可靠性大大提高了。

如果 Java 有类似 erlang 的内存空间/进程,那么 EE 模型就会更好用。但是,Java 并不具备这种能力,以后也不会有(太贵了)。

================

可能是因为 Java EE 的最后一次发布是在 2017 年。真实答案为什么你认为它不流行?Jakarta 工作组正在向前推进。你可以在这里看到所有兼容的实现:https://jakarta.ee/compatibility/。很多人正在在 Jakarta EE 上部署应用程序。很抱歉,它在 twitter/tiktok/youtube 上并不流行。

================
不可变容器是一种卓越的部署模式。我们这些多年来一直在管理应用程序/Web 服务器的人再也不想回到那种模式了。先不说容器模式为生产系统管理带来的相对便利,单是开发人员工作效率上的差异就值得我们这样做。

此外,由于部署单元是容器,因此不需要胖 jar。我个人更喜欢使用 Quarkus 的瘦 jar。

在你的容器中,你要么需要一个内嵌有 servlet 容器的胖jar,要么需要一个预部署了应用程序的 servlet 容器。

人们通常会选择带有嵌入式 servlet 容器的 胖jar。

胖 jar 只是一个包含所有依赖项的 jar:当你把应用程序从一个地方复制到另一个地方,或者分发你的应用程序以便在其他地方运行时,它非常方便。

在容器化的世界里,你分发的就是一个容器:在容器中,如果你的应用程序是一个单独的 jar,而它的依赖项在文件系统的其他地方,或者它们作为胖 jar 的一部分包含在 jar 中,那么这并不重要。

我的意思是,如果你要分发一个容器,就真的不需要像 Spring Boot 那样将应用程序及其依赖项打包在一个单一的 jar 中。

Jib 在某种程度上就是这么做的,它不是创建一个胖Jar,而是创建一个多层容器,在不同的层上有不同类别的依赖关系。一个额外的步骤/层可以是网络服务器。

对于这样的应用来说,Jib 就显得有些矫枉过正了。在 Spring Boot 中,spring-boot-thin-launcher 可以生成应用程序的精简 jar 发行版。您必须在容器创建前(或作为创建过程的一部分)将其配置为下载依赖项。否则,它会在您启动容器时下载您的依赖项,这不是好的做法。

Quarkus 默认生成 fast-jar(瘦 jar):

  • 它使用自定义类加载器加载依赖项,速度比系统类加载器快很多。
  • 任何生成瘦 jar 的构建都会将所有依赖项和构建的应用程序 jar 复制到 "target "下的文件夹中。然后,你只需在构建容器时复制该文件夹即可。

这种瘦 jar方式为什么比胖Jar好?
如果你在编译/编辑/运行循环中进行 docker 部署,它就会非常有用。或者,当你在更高的环境中四处奔波,试图让东西正常工作时,它也非常有用。

如果把复制 jar 分成两个 COPY 命令:

  • 第一个复制所有依赖库
  • 第二个复制应用逻辑 jar,

这意味着 docker 会创建两个独立的层。依赖库很少变化,所以 docker 只需要在极少数情况下推送这一层。

对于有大量依赖库的大型项目来说,在网络连接速度较慢的情况下(我在家里工作,网络连接速度很差),大多数情况下,推送操作可以从 2-3 分钟缩短到小于 30 秒。

它还能节省推送到的 docker 仓库的空间。所有使用相同依赖关系的镜像都指向同一层。

因此,在容器环境中,生成胖 jar 是不必要的。瘦 jar 格式是自然的 Java 方式。

生成廋Jar的小技巧
很少有人知道 MANIFEST.MF classpath 条目。Maven 可以自动生成这个条目,例如

<properties>
    <exec.classpathPrefix>${settings.localRepository}</exec.classpathPrefix>
</properties>

    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <executions>
        <execution>
          <id>default-jar</id>
          <configuration>
            <archive>
              <manifest>
                <addClasspath>true</addClasspath>
                <mainClass>${exec.mainClass}</mainClass>
                <classpathPrefix>${exec.classpathPrefix}</classpathPrefix>
                <classpathLayoutType>repository</classpathLayoutType>
                <useUniqueVersions>false</useUniqueVersions>
              </manifest>
            </archive>
          </configuration>
        </execution>
      </executions>
    </plugin>

这将使 JAR 可以运行,前提是类路径条目是相对或绝对的。

这比 Spring Boot 的 fatjar 启动时间要快得多

基本上,你在开发过程中的运行方式已经发生了变化。由于无需为开发和 CI 打包一个巨大的 jar,构建时间大大缩短。

至于 docker,你可以先构建,然后复制依赖项,更改 classpathprefix 并使用依赖项插件,或者使用 maven 构建插件和 Transitive Deps(~/.m2/),接受一个稍大的镜像......例如,你可以用本地 maven 仓库装运一个容器。