JAR 清单类路径不适用于 Java 应用程序启动器

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

自从我开始学习 Java 以来,我几乎就知道 清单 文件中的 Class-Path 标头字段 指定 可执行 JAR 的相对运行时 类路径(JAR 的 应用程序起点 由另一个名为 Main-Class 的清单标头指定)。一位 同事 最近遇到了一个让我感到惊讶的问题,因为它证明了当运行 javac 时包含 JAR 包含在类路径中时, JAR 文件的清单 Class-Path 路径条目也会影响编译时类路径。这篇文章展示了这种新的细微差别。

The Java Tutorials Deployment Trail 的“ Adding Classes to the JAR File's Classpath ”部分指出,“您指定要包含在 applet 或应用程序的清单文件中的 Class-Path 标头字段中的类。”同一部分还指出,“通过在清单中使用 Class-Path 标头,您可以避免在调用 Java 运行您的应用程序时必须指定长类 -classpath 标志。”这两句话基本上总结了我一直以来对清单文件中 Class-Path 标头的看法:作为通过 Java 应用程序启动器( java 可执行文件)执行的包含 JAR 的类路径。

事实证明,JAR 清单中的 Class-Path 条目会影响 Java 编译器 ( javac ),就像它会影响 Java 应用程序启动器 ( java ) 一样。为了演示这一点,我将使用一个简单的接口 ( PersonIF )、一个实现该接口的简单类 ( Person ) 以及一个使用实现该接口的类的简单类 Main 。接下来显示这些代码清单。

PersonIF.java


 public interface PersonIF
{
   void sayHello();
}

人.java


 public interface PersonIF
{
   void sayHello();
}

主.java


 public interface PersonIF
{
   void sayHello();
}

从上面的代码清单可以看出, Main 类依赖于(使用) Person 类,而 Person 类依赖于(实现) PersonIF 。我将有意将 PersonIF.class 文件放在它自己的名为 PersonIF.jar 的 JAR 中,并将该 JAR 存储在(不同的)子目录中。 Person.class 文件将存在于它自己的 Person.jar JAR 文件中,并且该 JAR 文件包含一个 MANIFEST.MF file ,该文件 Class-Path 标头在相关子目录中引用 PersonIF.jar

我现在将尝试仅使用类路径上的当前目录从 Main.java 编译 Main.class 。当 javac 无法在单独的子目录中找到 PersonIF.jar 时,我以前预计编译会失败。但是,它不会失败!

这让我感到惊讶。为什么在我没有明确指定 PersonIF.class (或包含它的 JAR)作为通过 -cp 标志提供的类路径的值时编译?可以通过运行带有 -verbose 标志的 javac 来查看答案。

javac -verbose 的输出提供了“ 文件的搜索路径”和“ 文件的搜索路径”。 “类文件的搜索路径”在本例中很重要,因为我已将 PersonIF.java Person.java 源文件移动到一个完全不相关的目录,而不是在那些指定的搜索路径中。有趣的是,类文件的搜索路径(以及源文件的搜索路径)包括 archive/PersonIF.jar ,即使我没有在 -cp 的值中指定这个 JAR(甚至它的目录)。这表明 Oracle 提供的 Java 编译器 考虑类路径上指定的任何 JAR 的 MANIFEST.MF Class-Path 路径标头中指定的类路径内容。

下一个屏幕快照演示了运行新编译的 Main.class 类并从 archive/PersonIF.jar 中获取依赖项 PersonIF.class ,而没有在传递给 Java 应用程序启动器的 java -cp 标志的值中指定它。我希望运行时行为是这样的,尽管我从来没有尝试过,甚至没有想过用一个 MANIFEST.MF 文件没有 Main-Class 头文件(不可执行的 JAR)的 JAR 来做。此示例中的 Person.jar 清单文件未指定 Main-Class 标头,仅指定了 Class-Path 标头,但在使用 java 调用时仍然能够在运行时使用此类路径内容。

这篇文章的最后一个演示涉及从 JAR 文件中删除 Class-Path 标头和关联值,并尝试使用 javac 和相同的命令行指定类路径进行编译。在这种情况下,包含 Person.class 的 JAR 称为 Person2.jar ,下面的屏幕快照表明其 MANIFEST.MF 文件没有 Class-Path 标头。

下一个屏幕快照表明现在使用 javac 进行编译失败,因为正如预期的那样, PersonIF.class 没有在类路径上明确指定,并且不再通过类路径上的 JAR 的 MANIFEST.MFClass-Path 标头引用提供.

我们从前面的屏幕快照中看到,源文件和类文件的搜索路径不再包括 archive/PersonIF.jar 。如果没有可用的 JAR, javac 将无法找到 PersonIF.class 并报告错误消息:“找不到 PersonIF 的类文件”。

一般观察

  • MANIFEST.MF 文件中的 Class-Path 标头不依赖于存在于同一 JAR 的 MANIFEST.MF 文件中的 Main-Class 标头。
    • 带有 Class-Path 清单标头的 JAR 将使这些类路径条目可供 Java 类加载器使用,而不管该 JAR 是使用 java -jar ... 执行的还是只是放置在更大的 Java 应用程序的类路径中。
    • 如果 JAR 包含在为 Java 编译器指定 Class-Path 中,则具有类路径清单标头的 JAR 将使这些类路径条目可供 Java 编译器 ( javac ) 使用。
  • 因为在 JAR 的清单文件中使用 Class-Path 的范围不限于正在执行 Main-Class JAR,所以这些类依赖可能会无意中满足(甚至可能使用不正确的版本),而不是解析明确指定的类路径条目.使用 Class-Path 或使用清单文件中指定 Class-Path 的第三方 JAR 时,建议谨慎。
  • JAR 的清单文件的重要性有时被低估了,但是这个主题提醒我们 了解特定 JAR 的清单文件中的内容 的有用性。
  • 本主题提醒您可以通过不时地使用 -verbose 标志运行 javac 来了解它的含义。
  • 每当您将 JAR 放置在 javac 编译器或 java 应用程序启动器的类路径中时,您放置的不仅仅是该 JAR 中的类定义。您还将将该 JAR 的清单的 Class-Path 引用的任何类和 JAR 放在编译器或应用程序启动器的类路径上。

结论

Java 类加载器可以从许多地方加载用于构建和运行 Java 应用程序的类。正如这篇文章所展示的,JAR 的 MANIFEST.MF 文件的 Class-Path 标头是影响类加载器将在运行时和编译时加载哪些类的另一个接触点。 Class-Path 的使用不仅影响“可执行”的 JAR(在其清单文件中指定 Main-Class 标头并使用 java -jar ... 运行),而且会影响编译加载的类和任何Java 应用程序执行,其中带有包含 Class-Path 标头的清单文件的 JAR 位于类路径中。