问题来了#

要深入了解 Spring Boot 的加载机制,首先需要认识到每个第三方依赖包实际上都包含自己独特的 META-INF/spring.factories 文件。正如图中所示。

这些文件在应用程序启动时扮演着重要的角色,它们定义了自动配置的类和其他相关设置,帮助 Spring Boot 在运行时自动识别和加载相应的配置。

image

然而,当我们试图查看某个第三方依赖包时,可能会发现找不到相应的 META-INF/spring.factories 文件,甚至没有 *.imports 文件,这时该怎么办呢?不要慌张!并不是所有的项目都具备自动配置功能。例如,ZhiPuAiAutoConfiguration 的自动配置实际上已经包含在 Spring Boot 的核心库中。

@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
@ConditionalOnClass(ZhiPuAiApi.class)
@EnableConfigurationProperties({ ZhiPuAiConnectionProperties.class, ZhiPuAiChatProperties.class,
        ZhiPuAiEmbeddingProperties.class, ZhiPuAiImageProperties.class })
public class ZhiPuAiAutoConfiguration {
}

可以简单理解为,一旦你引用了相应的依赖包,它们的配置便会立即生效。然而,在查找配置的 *.imports 文件时,我发现了一个有趣的现象:许多依赖包下也存在 aot.factories 文件。这是用来做什么的呢?考虑到 Spring Boot 自身也包含此类文件,这表明这个概念并非无的放矢。

因此,带着这个疑问,我决定深入探究其背后的机制与作用。

一探究竟#

经过了一番 AI 问答和上网搜索,我大致了解了 aot.factories 文件的用途:它实际上是为打包和编译服务的。这个文件可以帮助将 Java 项目打包成可执行的 EXE 文件(在 Windows 系统下,其他操作系统则有不同的打包方式),这样就无需依赖 Java 运行环境即可直接运行。不过,这与 Spring Boot 的自动配置机制并没有直接关系。

那么,为什么会发明这样的东西呢?我知道你很着急,但是你先别着急!听我一点一点讲,你就更明白了!

image

Java当前痛点#

有过 Java 开发经验的朋友们应该都知道,以前的 Java 应用通常都是单体架构,这意味着启动一个项目往往需要耗费几分钟的时间,尤其是大型项目,启动时间更是让人头疼。因此,随着技术的发展,微服务架构应运而生,不仅显著缩短了启动时间,而且将业务逻辑进行了合理的切分。

然而,微服务架构也并非没有缺点。尽管启动速度更快,项目在启动后往往无法立即达到最佳的运转状态,也就是说,应用需要一段时间才能进入高效的运行峰值。

因为 Java 的底层通常使用的是 HotSpot 虚拟机,HotSpot 的运行机制是将常用的代码部分编译为本地(即原生)代码。这意味着在程序启动之初,HotSpot 并不知道哪些代码会成为“热点”代码,因此无法立即将这些代码转换为机器能够直接理解和执行的形式。

在这个过程中,HotSpot 会不断分析和监测代码的执行情况,以快速识别出哪些部分是频繁被调用的。只有在识别出热点代码并将其编译为本地代码之后,我们的项目才能实现最佳的吞吐量。

要想让 Java 像 Python 那样实现瞬时启动,几乎是不可能的。这一现象使得 Java 在很多情况下更适合用于企业级服务,主要原因在于其所追求的稳定性和可靠性。在企业环境中,系统的稳定性往往是首要考虑的因素。

然而,Java 也面临着一系列挑战,这些挑战在云计算时代尤为突出。

云时代#

以 Serverless 为例,Serverless 是一种在云计算环境中日益成为主流的部署模式。它通过将基础设施的管理和运维任务抽象化,使开发者能够更加专注于业务逻辑的实现,而不必过多关注底层的资源配置和管理。

image

我就不提以前那种需要自己部署物理机的老年代的情况了。如今,绝大多数公司都已经采用了 Kubernetes(K8s)作为集群管理的解决方案。在各大云服务提供商处购买服务器后,企业通常会自行管理其集群服务。运维团队则负责监控和优化资源配置,及时进行扩展以满足需求。

此外,随着技术的发展,Server Mesh 和边车模式也逐渐兴起,这些都是值得深入了解的概念。归根结底,这些改进的目的就是为了显著节省公司内部的开发时间,从而让团队能够更专注于核心业务。

目前的 Serverless 架构显著提高了资源利用的效率,因为所有的基础设施管理工作都由云服务提供商负责。实际上,云厂商的基础设施本身并没有发生根本变化,变化的主要是架构设计,使得客户的使用体验更加便捷和高效。在这种模式下,无论是运维人员还是开发人员,都只需关注函数的部署,而无需深入了解服务器的细节信息。

开发者不再需要关心函数的运行方式、底层有多少容器或服务器在支撑这些服务。对他们而言,这一切都被抽象化为一个简单的接口,只需确保参数对接得当即可。

但是,你敢用 Java 来部署 Serverless 函数吗?当系统的吞吐量急剧上升,需要迅速启动一个新节点来支撑额外的负载时,这位 Java 大哥可能还在忙着启动或者进行预热,这可真是耽误事啊!所以Java作为牛马届的老大哥怎么可能会愿意当小弟?

image

GraalVM 简介#

如果你还不熟悉 GraalVM,但一定听说过 OpenJDK。实际上,它们都是完整的 JDK 发行版本,能够运行任何面向 JVM 的语言开发的应用。不过,GraalVM 不仅限于此,它还提供了一项独特的功能——Native Image 打包技术。这项技术的强大之处在于,它能够将应用程序打包成可以独立运行的二进制文件,这些文件是自包含的,完全可以脱离 JVM 环境运行。

换句话说,GraalVM 允许你创建类似于常见的可执行文件(如 .exe 文件)的应用程序,这使得部署和分发变得更加简便和灵活。

image

如上图所示,GraalVM 编译器提供了两种模式:即时编译(JIT)和提前编译(AOT)。AOT全称为Ahead-of-Time Processing。

对于 JIT 模式,我们都知道,Java 类在编译后会生成 .class 格式的文件,这些文件是 JVM 可以识别的字节码。在 Java 应用运行的过程中,JIT 编译器会将一些热点路径上的字节码动态编译为机器码,以实现更快的执行速度。这种方法充分利用了运行时信息,能够根据实际的执行情况进行优化,从而提高了性能。

而对于 AOT 模式,GraalVM 则在编译期间就将字节码转换为机器码,完全省去了运行时对 JVM 的依赖。由于省去了 JVM 加载和字节码运行期预热的时间,AOT 编译和打包的程序具有非常高的运行时效率。这意味着在启动时,应用程序可以几乎瞬间响应,极大地提高了处理请求的能力。

那么,这种 AOT 编译到底有多快?它是否会成为 Serverless 函数的一种常用方案,超越 Python 等其他语言的应用呢?为了验证其性能优势,我们可以进行实际测试。

image

安装GraalVM#

我们大家基本上都在本地安装了 IntelliJ IDEA 开发工具,使用起来非常方便。在这里,我们可以直接通过 IDEA 的内置功能下载 GraalVM,省去了在官方网站上寻找和下载的时间。只需简单几步,我们就可以快速获取到最新的 GraalVM 版本,随时准备进行开发。

下载完成后,我们只需配置项目的 JDK 为 GraalVM。由于我目前使用的是 JDK 17,因此需要选择与之兼容的 GraalVM 17 版本。这种配置过程相对简单,只需在项目设置中更改 JDK 路径即可。

image

我们将继续使用之前研究过的 Spring AI 项目,在此基础上,我们需要添加一些相关的 Spring Boot 插件。

<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
</plugin>

在我们顺利完成所有配置后,准备进行编译时,却意外地遇到了错误提示,显示 JAVA_HOME 指向的是我们原本的 JDK 1.8。这一问题的出现主要是由于其中某个工具并不依赖于 IntelliJ IDEA 的启动变量,而是直接读取了 JAVA_HOME 的环境变量。

为了解决这个问题,我们需要确保 JAVA_HOME 环境变量正确指向我们新安装的 GraalVM 版本。因此,我们必须在本地系统中下载并安装 GraalVM,确保其版本与我们项目中所需的 JDK 版本相匹配。

首先我们找到官网:https://www.graalvm.org/downloads/

image

因为我是windows版本,所以自己请选择好相应的操作系统。等待下载完毕,解压完成后,将配置环境变量指向改目录后重启生效,再次编译即可。

运行后,还是报错如下:

Error: Please specify class (or /) containing the main entry point method. (see --help)

内容就是找不到启动类的意思,所以需要加一些配置。找了半天修改如下,切记前后顺序别变,否则还是会有点问题。

<build>
        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <configuration>
                    <!-- imageName用于设置生成的二进制文件名称 -->
                    <imageName>${project.artifactId}</imageName>
                    <!-- mainClass用于指定main方法类路径 -->
                    <mainClass>com.example.demo.DemoApplication</mainClass>
                    <buildArgs>
                        --no-fallback
                    </buildArgs>
                </configuration>
                <executions>
                    <execution>
                        <id>build-native</id>
                        <goals>
                            <goal>compile-no-fork</goal>
                        </goals>
                        <phase>package</phase>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

接下来,我们将继续使用常用的 Maven 命令,如 mvn clean package,来进行项目的打包。这个过程可能会显得有些漫长,尤其是与以前相比,打包速度似乎下降了不止一个级别。这次,我的打包过程持续了大约十分钟,这确实比我之前的体验慢了不少。

image

然后非常激动的点击了生成好的demo.exe文件,结果还是在报错:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.1)

Application run failed
org.springframework.boot.AotInitializerNotFoundException: Startup with AOT mode enabled failed: AOT initializer com.example.demo.DemoApplication__ApplicationContextInitializer could not be found
        at org.springframework.boot.SpringApplication.addAotGeneratedInitializerIfNecessary(SpringApplication.java:443)
        at org.springframework.boot.SpringApplication.prepareContext(SpringApplication.java:400)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:334)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352)
        at com.example.demo.DemoApplication.main(DemoApplication.java:10)

然后经过仔细查询,需要指定profile为native,因为之前打的包没有Aot信息。

image

好的,又经历了15分钟打包完毕,这次,我们终于成功了,我们可以直观的看下AOT方式打包后的启动时间与jar包方式的启动时间对比。简直是天壤之别。

image

哦?确实,现在的启动时间已经缩短到毫秒级别,真令人惊讶!通过 GraalVM 的 Native Image 技术,我们不仅实现了 Java 项目的快速启动,还有效去除了传统 Java 应用中的预热时间。这一切看似都非常理想,然而,问题也随之而来。

GraalVM 缺点#

说完了 GraalVM 能解决 Java 原来的问题后,我们也必须认识到,它并非没有缺点。如果没有这些不足,大家对 GraalVM 的了解程度肯定会超过对 OpenJDK 的熟悉度。毕竟,既然它如此出色,为什么大多数人却没有广泛使用呢?

首先,兼容性问题是一个显著的挑战。许多老旧版本的 JDK 项目根本无法与 GraalVM 兼容,这无疑限制了大部分企业的使用范围。对于那些依赖于较旧 JDK 的企业而言,迁移到 GraalVM 可能需要耗费大量时间和资源,甚至面临重构代码的风险。

其次,即便是使用新版本 JDK 的项目,开发者们也往往对使用 GraalVM 感到犹豫。原因在于,GraalVM 对某些动态特性的支持相对较弱。例如,反射机制、资源加载、序列化以及动态代理等功能的限制,可能会对现有代码的运行产生重大影响。这些动态行为在许多应用程序中都是核心部分,任何对它们的削弱都可能导致功能缺失或性能问题。

image

有人可能会产生疑问:Spring 框架本身依赖于工厂模式和各种动态代理功能,若 GraalVM 不支持这些高级特性,岂不是意味着 Spring 的运行将受到致命影响?如果动态代理无法正常使用,Spring 的许多核心功能将会受到制约,那刚才提到的打包成功又是怎么回事呢?

实际上,这一切的背后得益于 GraalVM 提供的 AOT(Ahead-of-Time)元数据文件功能。这个特性使得开发者能够在编译阶段明确哪些类和方法将会使用到动态代理,GraalVM 会在编译时将这些信息整合到最终的可执行文件中。

RuntimeHints与aot.factories#

GraalVM 的 API —— RuntimeHints 负责在运行时收集反射、资源加载、序列化和 JDK 代理等功能的需求。这一特性为我们理解 GraalVM 如何支持动态特性提供了重要线索。实际上,大家在这里就可以猜到 aot.factories 文件的作用。没错,这个文件的存在正是为了在 GraalVM 编译时,确保能够加载 Spring 框架所需的 JDK 代理相关需求。我们看下文件:

org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider.FreeMarkerTemplateAvailabilityRuntimeHints,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider.GroovyTemplateAvailabilityRuntimeHints,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.JacksonAutoConfigurationRuntimeHints,\
org.springframework.boot.autoconfigure.template.TemplateRuntimeHints

org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingProcessor

org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
org.springframework.boot.autoconfigure.flyway.ResourceProviderCustomizerBeanRegistrationAotProcessor

org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer

我们可以注意到 RuntimeHintsRegistrar 的存在,它的主要作用是识别并加载所有实现了相关接口的类,从而进行解析和处理。需要强调的是,GraalVM 默认并不会自动查找 aot.factories 文件,因为这属于 Spring 的特定机制。这就意味着,如果没有显式的指引,GraalVM 是无法主动识别和利用这些动态特性。

在 RuntimeHintsRegistrar 下面,我们还可以看到许多 AotProcessor 的实现。这个结构和我们之前讨论的 beanFactoryProcessor 有些相似,但这里我们不深入探讨具体的细节。今天我们只聚焦于表面现象,以便理解其基本功能。

Spring 实际上已经为我们解决了加载相关信息的问题,使得动态特性可以在编译时得到适当处理。然而,这并不意味着一切都已经准备就绪。第三方组件同样需要提供相应的实现,以确保与 Spring 的兼容性。如果你的依赖库使用了某些高级功能,但没有实现 Spring 的 aot.factories 扫描机制,那么这些功能在编译后将无法生效。

因此,仍然有许多工作需要进行,以确保整个生态系统的兼容性和功能性。

总结#

在探索 aot.factories 和 spring.factories 的过程中,我们不仅揭示了这两个文件的本质差异,还深入探讨了它们在 Spring Boot 3 中的作用及其应用场景。这一探索之旅引领我们进入了现代 Java 应用开发的前沿,尤其是在 Serverless 和微服务架构的背景下。随着云计算的发展,应用程序的性能与启动速度已成为开发者的核心关注点。在此背景下,GraalVM 的出现提供了一种新颖的解决方案,通过其 Native Image 功能,Java 应用的启动时间得以大幅度缩短,这为开发者们带来了巨大的便利。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐