发布于 2026-01-06 6 阅读
0

构建、编译、运行:类路径速成课程

构建、编译、运行:类路径速成课程

我称之为我的“Gradle学徒期”。上一份工作之前,我曾在Gradle工作了一年,有机会直接接触到我们这一代最顶尖的构建专家。我从那段经历中学到了一个秘密:一切都与类路径有关。事实上,一切皆为类路径。你的狗?也是一条类路径,由吠叫、口水和粪便组成。

javac -classpath barks.jar:drool.jar:poop.jar Dog.java
Enter fullscreen mode Exit fullscreen mode

好吧,这或许有些夸张。 但这足以说明,无论怎么强调类路径在 Java 和 Java 相关开发中的重要性都不为过:它们无处不在,而且至关重要。我花了四年时间才开始理解它们,又花了两年时间才写出这篇文章,这样你就可以一边喝着早晨的咖啡,一边花七分钟阅读,同时还能暂时摆脱 Jira 看板上那张怒目而视的分析工单。

这是给谁看的?

如果你使用 Java 或任何 JVM 语言(例如 Kotlin 或 Groovy)编写代码,无论你的目标平台是 JVM 还是 Android,这篇文章都可能对你有所帮助。理解类路径可以帮助你理解并避免棘手的 bug,并且,随着你对 JVM 语言的理解加深,它将有助于你成为一名更高效的程序员。

我将使用 Android 和 Gradle 的示例,因为这是我最熟悉的领域,而且这些示例对我来说也更容易理解。我们还会看一下 Gradle 构建扫描,因为它们是可视化类路径的绝佳工具。

将涵盖哪些内容

本文重点介绍 Java 中类路径和类加载的基础知识,未来将有文章专门讨论构建时、编译时和运行时的类路径。

什么是类路径?

类路径是告诉 SDK 工具2 的方式。 (例如java或),或者封装了 SDK 工具的构建工具(例如 Ant、Maven 或 Gradle),用于查找非扩展或非核心 JDK 一部分的javac第三方或用户自定义类。(官方文档

第三方类通常被称为库或依赖项,通常以 JAR 文件的形式提供。用户自定义类是由您和您的团队编写的,也就是您自己的应用程序。这与核心 Java 平台类(例如 `java.util.java`)形成对比java.lang.String,后者是 JDK 的一部分,无需在类路径中指定。(您可以将这些平台类视为启动类路径。)

正如我们将在下文详细探讨的那样,类路径实际上就是包含类文件的 jar 文件和目录的有序列表。回顾Dog.java上面的例子,该编译操作的类路径实际上就是三个指定的 jar 文件,按顺序排列:barks.jar、drool.jar 和 poop.jar。

当然,作为现代 JVM 开发人员,我们很少(甚至几乎从不javac)直接与 Java 编译器之类的工具交互;我们使用“构建工具”来协调日益复杂的构建过程。同样,我们通常不会直接指定类路径;而是“声明依赖项”。依赖项管理和解析本身就是一个极其复杂的话题,我们在此尽量省略。我们只想说明一点:当你在 Gradle 构建脚本中声明一个依赖项时,该工具会将其视为解析该依赖项的指令(通常涉及从互联网下载),最终结果是代表该“依赖项”的一个或多个 jar 文件会出现在类路径中。例如,如果你声明以下内容(取自一个典型的 Android 项目):

dependencies {
  implementation 'androidx.appcompat:appcompat:1.1.0'
}
Enter fullscreen mode Exit fullscreen mode

那么最可能的结果就是3 其中,Androidx appcompat 库 v1.1.0 以 jar 文件的形式呈现,4 最终会出现在编译时和运行时类路径中。5 此外,appcompat 的所有依赖项(以及它们的依赖项,也称为传递依赖项)都将被添加到相同的类路径中。以下是构建扫描的部分内容,显示了 appcompat 的顶级依赖项:

典型 Android 项目的部分调试编译类路径

如上所述,我们不会深入探讨 gradle 依赖项解析引擎的复杂性,因此,如果您想了解更多关于带有constraint标签的前三个依赖项的信息,请参阅文档

虽然构建扫描显示依赖项以树状结构组织,但当 Gradle 实际调用相关的编译任务时,该树状结构会被扁平化为一个简单的列表。

总结起来:

声明依赖项在功能上等同于将一个或多个 jar 文件添加到一个或多个类路径中。

要了解类路径如何影响Java运行时,我们必须先讨论类加载。现在我们就来谈谈这个。

类加载器加载类

想要深入了解类加载和类ClassLoader本身,我推荐Baeldung 的这篇教程。这里我将总结其要点。

Java 应用程序使用三种内置类加载器:

  1. Bootstrap 类加载器。用于加载 JDK 类,例如java.lang.Stringjava.util.ArrayList
  2. 扩展类加载器。用于加载扩展类(超出本文讨论范围)。
  3. 应用程序或系统类加载器。用于加载第三方类和用户自定义类。此类加载器已配置为使用用户自定义类路径。

您也可以在运行时定义自定义类加载器。

以下简单示例(改编自 Baeldung 教程)演示了第一种和第三种类型:

public class Main {
  public static void main(String... args) {
    System.out.println("Main class loader = " + Main.class.getClassLoader());
    System.out.println("String class loader = " + String.class.getClassLoader());
  }
}
Enter fullscreen mode Exit fullscreen mode

运行此程序会产生以下输出:

Main class loader = sun.misc.Launcher$AppClassLoader@7852e922
String class loader = null
Enter fullscreen mode Exit fullscreen mode

请特别注意,类String加载器显示为 `<classloader_name>` null,这表明它是引导类加载器。由于该类加载器是用本地代码编写的,因此它没有 Java 类的表示形式。

你可以通过执行以下命令来编译并运行此程序javac Main.java && java Main

这些类加载器以层级结构组织,引导类加载器是扩展类加载器的父级,扩展类加载器本身又是应用程序类加载器的父级。当被要求加载某个类时,类加载器会委托给其父级。这些父级又会委托给它们的父级,依此类推。如果父级无法加载某个类,则其直接子级会尝试加载,依此类推,直到最终加载成功或抛出ClassNotFoundException异常(取决于具体情况)。NoClassDefFoundError

每个Class实例都有一个指向加载它的对象的引用ClassLoader,该引用由该Class.getClassLoader()方法检索。

您定义的类路径用于配置应用程序类加载器,该加载器使用该信息在运行时加载您的程序请求的类。

类路径对顺序敏感,并且允许重复条目。

现在我们知道了类路径如何影响 Java 或与 Java 相关的应用程序:它指示应用程序的类加载器如何查找第三方库和用户自定义类。因此,类路径对 Java 运行时有着非常深远的影响。

Java 的有趣之处在于它的动态性,虽然这个词已经被用滥了。例如,类路径的微小改动就可能导致两次连续操作之间出现截然不同的结果。

您可以将类路径视为一个有序的元素列表,其中包含class文件和/或 JAR 包,而 JAR 包class又是资源文件的集合。这些类文件用于执行诸如编排构建、编译或运行项目等操作。

大多数情况下,从类路径中移除元素只会导致给定操作失败,但在某些情况下,您可能会看到不同的行为!此外,理解类路径的顺序至关重要。让我们再次考虑一下狗狗的例子。如果不是……

javac -classpath barks.jar:drool.jar:poop.jar Dog.java
Enter fullscreen mode Exit fullscreen mode

我们曾

javac -classpath drool.jar:barks.jar:poop.jar Dog.java
Enter fullscreen mode Exit fullscreen mode

也许一切都不会改变。但是,如果 ` <classpath> drool.jar` 和barks.jar`<classpath>` 存在重叠的类文件(可能是由于打包错误、大型重构中的中间步骤、架构缺陷等原因),那么在第一种情况下,我们会使用 `<classpath>` 中的类文件barks.jar,而在第二种情况下,我们会使用 `<classpath>` 中的类文件drool.jar,而这些类文件很可能并不相同。这里隐含的一点是,类路径也允许重复条目:SDK 工具会直接忽略列表中与前面条目重复的条目。

重要提示:Gradle 保证类路径顺序是确定的。

类路径对顺序敏感,在下一篇文章中,我们将通过构建领域的一个示例来利用这一点。

(暂时)就到这里吧。

在这篇文章中,我们了解了什么是类路径;Java 运行时使用它来配置应用程序类加载器;类加载器按层次结构组织,引导类加载器位于根部;类路径对顺序敏感,这对程序的运行时行为(包括构建程序)有着深远的影响。

掌握了这些基础知识后,我们现在可以开始探索构建、编译和运行领域中更多实际的、来自真实世界的例子了。下次见!

特别鸣谢

感谢César Puerta审阅了本文的多个草稿,并提供了出色而详尽的反馈。不过,文中如有任何错误,均由我本人承担!

尾注

1.我是个爱猫人士,这很明显吗?(向上
) 2.这个术语是从 Oracle 文档中借用的。(向上
) 3.我说过这很复杂,明白吗? (向上
) 4.我知道 appcompat 实际上打包成了一个 aar 文件,但 AGP 的功能之一是解压这个 aar 文件,并将其中的 jar 文件添加到类路径中。(向上)
5.我们将在以后的文章中讨论为什么它会出现在多个类路径中。(向上)

文章来源:https://dev.to/autonomousapps/build-compile-run-a-crash-course-in-classpaths-f4g