【问题标题】:Java: Do BOTH the compiler AND the JRE require access to all 3rd-party class files?Java:编译器和 JRE 都需要访问所有 3rd-party 类文件吗?
【发布时间】:2011-04-03 01:06:46
【问题描述】:

我有 15 年的 C++ 经验,但对 Java 很陌生。我试图了解 Java 如何处理头文件的缺失。我有几个与此问题相关的问题。

具体来说,假设我为导入第 3 方类“Z”(并使用 Z)的类“A”编写源代码。我知道在编译时,Java 编译器必须“访问”有关 Z 的信息才能编译 A.java,创建 A.class。因此,Z.java 或 Z.class(或包含其中之一的 JAR;比如 Z.jar)在编译时必须存在于本地文件系统中 - 对吗?

编译器是否使用类加载器来加载 Z(重申 - 在编译时)?

如果我在编译时使用了类加载器是正确的,如果需要用户定义的类加载器 (L) 怎么办 - 并且是正在编译的项目的一部分?例如,假设 L 负责通过网络在运行时下载 Z.class?在这种情况下,Java 编译器如何在编译时获取 Z.class?会不会尝试先编译L,然后在编译时使用L获取Z?

我了解使用 Maven 构建项目时,Z.jar 可以在编译时位于 Internet 上的远程存储库中 - 位于 ibiblio 或 POM 文件中定义的自定义存储库中。我希望我是正确的,是 MAVEN 负责在编译时下载 3rd-party JAR 文件,而不是编译器的 JVM?

但是请注意,在运行时,A.class 再次需要 Z.class - JRE 怎么知道从哪里下载 Z.class(没有 Maven 的帮助)?还是开发人员有责任将 Z.class 与 A.class 一起随应用程序一起提供(比如在 JAR 文件中)? (...假设不使用用户定义的类加载器。)

现在是一个相关的问题,只是为了确认:我假设一旦编译,A.class 只包含指向 Z.class 的符号链接 - Z.class 的字节码不是 A.class 的一部分;如果我错了,请纠正我。 (在 C++ 中,静态链接会将字节从 Z.class 复制到 A.class,而动态链接则不会。)

关于编译过程的另一个相关问题:一旦描述 Z 的必要文件在编译时位于 CLASSPATH 上,编译器是否需要 Z.class 中的字节码才能编译 A.java(并将构建 Z.class ,如有必要,来自 Z.java),还是 Z.java 足以满足编译器的需要?

我的整体困惑可以总结如下。似乎 Z 的完整 [字节] 代码需要出现两次——编译期间一次,运行时第二次——这对于 Java 程序引用的 ALL 类必须是正确的。换句话说,每个课程都必须下载/呈现两次。在编译期间,不能将单个类仅表示为头文件(就像在 C++ 中一样)。

【问题讨论】:

    标签: maven javac java


    【解决方案1】:

    编译器是否使用类加载器来加载 Z(重申 - 在编译时)?

    几乎。它使用JavaFileManager,它在许多方面就像一个类加载器。但它实际上并没有加载类,因为它需要从 .java 文件以及 .class 文件创建类签名。

    我希望我是正确的,是 MAVEN 在编译时负责下载 3rd-party JAR 文件,而不是编译器的 JVM?

    是的,Maven 会拉下 jar,尽管可以实现一个行为类似于 URLClassLoader 的 JavaFileManager。 Maven 管理 jars 的本地缓存,并根据需要从网络中填充该缓存。

    关于编译过程的另一个相关问题:一旦描述 Z 的必要文件在编译时位于 CLASSPATH 上,编译器是否需要 Z.class 中的字节码才能编译 A.java(并将构建 Z.class ,如有必要,来自 Z.java),还是 Z.java 足以满足编译器的需要?

    它不需要所有字节码。只是类、方法和属性签名和元数据。 如果 A 依赖于 Z,则可以通过在源路径上找到的 Z.java、在任何(类路径、系统类路径)上找到的 Z.class 或通过某些自定义扩展(如 Z)来满足该依赖关系。 jsp.

    我的整体困惑可以总结如下。似乎 Z 的完整 [字节] 代码需要出现两次——在编译期间一次,在运行时第二次——这对于 Java 程序引用的所有类都必须如此。换句话说,每个课程都必须下载/呈现两次。在编译期间,不能将单个类表示为头文件(因为它可以在 C++ 中)。

    也许一个例子可以帮助你弄清楚这一点。 Java 语言规范要求编译器进行某些优化。 static final 原语和 Strings 的内联。

    如果类 A 仅依赖于 B 用于常量:

    class B {
      public static final String FOO = "foo";
    }
    
    class A {
      A() { System.out.println(B.FOO); }
    }
    

    然后 A 可以在类路径上没有B.class 的情况下编译、加载和实例化。 如果您更改并发布了具有不同值 FOOB.class,那么 A 仍将具有该编译时间依赖性。

    因此有可能存在编译时依赖而不是链接时依赖。

    当然,可以通过反射获得运行时依赖而没有编译时依赖。

    总而言之,在编译时,编译器会确保类访问的方法和属性可用。

    在类加载时(运行时),字节码验证器检查预期的方法和属性是否真的存在。所以字节码验证器会仔细检查编译器所做的假设(除了上述那些内联假设)。

    可以模糊这些区别。例如。 JSP 使用自定义类加载器,它调用 java 编译器在运行时根据需要从源代码编译和加载类。

    【讨论】:

    • 可以想象,开发人员可以创建一个非常小的(非功能性)Z.java 文件,其中包含相同的签名等 - 并将其用于编译阶段。它是否正确?如果是这样,这似乎与 C++ 头文件机制几乎相同。
    • @Dan Nissenbaum,是的。你可以这样做。除了任何static final 原语和Strings 必须提前绑定它们的值。
    • @Dan,头文件当然必须是合法的java文件,并且就基类和实现的接口而言是完整的。但是考虑到static final 警告,您可以针对骨架进行编译。
    • 是的,你可以。实际上,我相信Groovy(另一种编译为类文件格式并在 JVM 上运行并可以与 java 类交互的语言)实际上当前确实(或确实)将存根 java 文件作为其项目的 maven 编译过程的一部分java 和 groovy 源代码。
    • 快速更正(我认为)您的声明“然后可以在类路径上没有 B.class 的情况下编译、加载和实例化 A”。在您的示例中确实不需要 B.class 在 runtime (当 A 被加载和实例化时),但 B.class would 在编译时是必需的为了确定静态最终变量的文字值 - 正确吗?
    【解决方案2】:

    了解 Maven 如何融入图片的最佳方式是意识到它(大部分)不适合。

    Maven 不参与编译器查找定义或运行时系统加载类的过程。编译器自己会这样做......基于构建时类路径所说的内容。当您运行应用程序时,Maven 已经完全不在画面中了。

    在构建时,Maven 的作用是检查在 POM 文件中声明的 项目 依赖项,检查版本,下载缺失的项目,将 JAR 放在众所周知的位置并为要使用的编译器(和其他工具)。

    然后编译器从这些 JAR 文件中“加载”它需要的类,以在编译的类文件中提取类型签名信息。它不使用常规的类加载器来执行此操作,但定位类的基本算法是相同的。

    一旦编译器完成,Maven 就会按照 POM 文件的规定将其打包成 JAR、WAR、EAR 文件等。对于 WAR 或 EAR 文件,将所有必需的依赖 JAR 打包到文件中。

    在运行时不会进行 Maven 指导的 JAR 下载。但是,运行应用程序可能涉及下载 JAR 文件;例如如果应用程序是使用 Java WebStart 部署的。 (但在这种情况下,不会从 Maven 存储库下载 JAR ...)

    还有一些注意事项:

    • Maven 根本需要出现在图片中。您可以使用 IDE 进行构建、Ant 构建工具(可能使用 Ivy)、Make 甚至“哑” shell 脚本。根据构建机制,您可能需要手动处理外部依赖项;例如弄清楚要下载的外部 JAR、将它们放在哪里等等。

    • Java 运行时系统通常需要比编译器加载更多。编译器只需要加载那些对正在编译的类进行类型检查所必需的类。

      例如,假设类A 有一个使用类B 作为参数的方法,而类B 有一个使用类C 作为参数的方法。编译A时,需要加载B,但不需要加载C(除非A以某种方式直接依赖于C)。执行A时,BC都需要加载。

      第二个例子,假设类A 依赖于接口I,实现IC1IC2。除非A 显式依赖IC1IC2,否则编译器不需要加载它们来编译A

    • 还可以在运行时动态加载类;例如通过调用Class.forName(className),其中className 是一个字符串值表达式。


    你写道:

    对于第二个要点中的示例-我认为开发人员可以选择在编译时为 B 提供一个存根文件,该文件不包括 B 使用 C 的方法,并且 A 可以编译得很好。这将证实我的评估,即在编译时,Java 完全允许在编译时仅声明必要的函数(即使作为存根)的所谓“头”文件 - 所以只是为了方便/约定工具随着时间的推移而发展,而不是使用头文件/源文件的区别。 (如果我错了,请纠正我。)

    这不是一个方便/进化的东西。 Java 从来不支持单独的头文件。 James Gosling 等人的出发点是头文件和预处理器是个坏主意。

    B 的假设存根版本必须具有真实 B 的所有可见方法、构造函数和字段,并且方法和构造函数必须具有主体。存根B 否则不会编译。 (我猜理论上,body 可能是空的,返回一个虚拟值或抛出一个未经检查的异常。)

    这种方法的问题是它会非常脆弱。如果您在保持B 的存根和完整版本的过程中犯了最小的错误,结果将是类加载器(在运行时)会报告一个致命错误。

    顺便说一句,C 和 C++ 在拥有单独的头文件方面几乎是个例外。在大多数支持单独编译(组成应用程序的不同文件)的其他语言中,编译器可以从实现源代码中提取接口信息(例如签名)。

    【讨论】:

    • 对于第二个要点中的示例 - 我认为开发人员可以选择在编译时为 B 提供一个不包含 B 的 stub 文件使用 C 和 A 的方法可以编译得很好。这将证实我的评估,即在编译时,Java 完全允许在编译时仅声明必要的函数(即使作为存根)的所谓“头”文件 - 所以只是为了方便/约定工具随着时间的推移而发展,而不是使用头文件/源文件的区别。 (如果我错了,请纠正我。)
    • 另外-再次关于第二个要点中的示例-我是否更正了JVM的实现可以选择延迟C的加载直到/除非它被使用?在那种情况下,这样的实现将在执行 A 时加载 C - 对吗?
    • 最后,关于第二个要点的最后一部分——语句“A 取决于接口 I”意味着 A 中的某些函数使用了实现 I 的某个 ICX 类,对吗?因此,如果 A 不使用 IC1 或 IC2,它仍然确实在您的示例中使用 ICX,并且需要在编译时加载该类。对吗?
    • 关于第二条评论:是的,JVM 可以在限制范围内做到这一点。他们倾向于不这样做,因为延迟加载更慢且更复杂。 (限制是 Java 语言规范限制了类初始化 ...)
    • 关于第三条评论。 1)不。还有其他类型的依赖。 2)是的......假设依赖关系的正确定义。
    【解决方案3】:

    另一个可能有帮助的难题是,接口和抽象类也被编译为类文件。因此,在编译 A 时,理想情况下您将针对 API 进行编译,而不必针对具体类进行编译。因此,如果 A 在编译时使用接口 B(由 Z 实现),则您将需要 A 和 B 的类文件,但在运行时您将需要 A、B 和 Z 的类文件。您是正确的,所有类都是动态链接的(您可以破解文件并查看字节码并查看其中的完全限定名称。jclasslib 是检查类文件和读取字节码的绝佳实用程序,如果您好奇的话)。我可以在运行时替换类。但是运行时的问题往往会导致各种形式的LinkageErrors

    通常,是否应将类与已编译的 jar 文件一起提供取决于您的特定场景。假定在每个 JRE 实现中都有可用的类。但是,如果我有自己的 API 和实现,我将不得不以某种方式将两者都提供给它们运行的​​任何地方。虽然有一些 API,例如servlets,我将在其中针对 servlet API 进行编译,但容器(例如 Websphere)负责在运行时为我提供 servlet API 和实现(因此我不应该发送我自己的副本其中)。

    【讨论】:

    • 因此,Java中所谓的“缺少头文件”并不真正正确。在您的示例中,我认为 B 很像 C++ 头文件,而 Z 是相应的源文件(仅在运行时需要 [以编译形式])。我说的对吗?
    • 我认为接口/抽象类与 C++ 头文件非常相似。我承认我没有像我应该的那样精通 C++ 编译:)
    • 具体来说——假设我们有Z z = new Z(); z.somefunc();,其中somefunc()是接口B的一部分(由Z实现)。你能确认编译时不需要Z.class,而只需要B.class吗?我想会的。
    • 那个具体的例子,你在编译时和整个运行时都需要 Z 类(或存根或源代码),因为你正在针对 Z 进行编译。但如果你改为编写 (B b = B.getB(); b.sumfunc()) getB() 可以在运行时注入 Z,但您只能针对 B 进行编译。
    • 还有许多其他方法可以通过接口解决此类问题。 getB() 是静态工厂方法模式的一个示例。 (抽象工厂模式也可以在这里提供帮助)。您可能还对依赖注入和Guice framework 感兴趣。此外,OSGi 有一个服务框架,用于在运行时注册和检索类的实现。
    【解决方案4】:

    我有 15 年的 C++ 经验,但对 Java 很陌生。

    您可能面临的最大挑战是,许多在 C++ 中被视为重要的事情,例如对象的 sizeof()、无符号整数和析构函数,在 Java 中并不容易做到,并且没有使用同样的重要性,并有其他解决方案/解决方法。

    我试图了解 Java 如何处理头文件的缺失。我有几个与此问题相关的问题。

    Java 的接口在概念上类似于头文件,因为它们只包含声明(和常量)而没有定义。类通常与该类的接口配对,有时是一对一的。

    编译器是否使用类加载器来加载 Z(重申 - 在编译时)?

    当类加载器加载一个类时,它会调用静态初始化块,它几乎可以做任何事情。编译器只需要从类中提取元数据,而不是字节码,这就是它的作用。

    编译时负责下载3rd-party JAR文件的是MAVEN,而不是编译器的JVM?

    Maven 必须将文件加载到本地文件系统,默认位置是~/.m2/repository

    JRE 怎么知道从哪里下载 Z.class(没有 Maven 的帮助)?

    它必须要么使用 Maven;一些 OSGi 容器能够动态加载和卸载不同的版本,例如,您可以在正在运行的系统中更改库的版本,或者从 maven 构建更新 SNAPSHOT。

    或者你有一个独立的应用程序;使用像 appassembly 这样的 Maven 插件,您可以创建批处理/shell 脚本和一个目录,其中包含您需要的所有库的副本。

    或者一个网络档案war,其中包含元信息和其中的许多罐子。 (它只是一个装有罐子的罐子;)

    或者开发者有责任将 Z.class 与 A.class 一起提供给应用程序

    对于独立应用程序是的。

    现在是一个相关的问题,只是为了确认:我假设一旦编译,A.class 只包含指向 Z.class 的符号链接

    从技术上讲,它只包含带有Z 的字符串,而不是.class 本身。您可以更改很多 Z 而无需再次编译 A 并且它仍然可以工作。例如您可能会针对 Z 的一个版本进行编译,稍后将其替换为另一个版本,并且应用程序仍然可以运行。您甚至可以在应用程序运行时替换它。 ;)

    Z.class 的字节码不是 A.class 的一部分;

    编译器几乎没有优化。恕我直言,唯一重要的是它内联编译时间常量。这意味着如果您在编译 A 后更改 Z 中的常量,它可能不会在 A 中更改。(如果您在编译时使常量未知,则不会内联它)

    没有字节码被内联,来自字节码的本机代码在运行时根据程序的实际运行方式被内联。例如假设您有一个具有 N 个实现的虚拟方法。 C++ 编译器不知道要内联 esp 的那些,因为它们在编译时可能不可用。然而,JVM 可以查看哪些使用最多(它在程序运行时收集统计信息)并且可以内联两个最常用的实现。 (思考当您在运行时删除/更新其中一个类时会发生什么;)

    如果我错了,请纠正我。 (在 C++ 中,静态链接会将字节从 Z.class 复制到 A.class,而动态链接则不会。)

    Java 只有动态链接,但这并不妨碍在运行时内联代码,这与使用宏一样有效。

    关于编译过程的另一个相关问题:一旦描述 Z 的必要文件在编译时位于 CLASSPATH 上,编译器是否需要 Z.class 中的字节码才能编译 A.java(并将构建 Z.class ,如有必要,来自 Z.java),还是 Z.java 足以满足编译器的需要?

    编译器将根据需要编译所有.java 文件。您只需要提供.java,但它必须编译(即它的依赖项必须可用)但是如果您使用.class 文件,则并非所有依赖项都可以用于编译A。

    我的整体困惑可以总结如下。似乎 Z 的完整 [字节] 代码需要出现两次 - 在编译期间一次,在运行时第二次 -

    从技术上讲,一个类包含字节码和元数据,例如方法签名、字段和常量。在编译时不使用任何字节码,只使用元信息。编译时的字节码不需要与运行时使用的相匹配。 (使用的签名/字段确实如此)每个类都有一个副本更简单,但如果出于某种目的需要,您可以在编译时使用精简版本。

    并且对于 Java 程序引用的所有类都必须如此。换句话说,每个课程都必须下载/呈现两次。在编译期间,不能将单个类仅表示为头文件(就像在 C++ 中一样)。

    它只需要下载一次,因为它位于存储库或磁盘上的某个位置。像头文件这样的接口可能是你在编译时所需要的,这些可能是一个单独的库,但通常不是因为在大多数情况下拥有一个存档更简单(OSGi 是我知道它在哪里的唯一示例值得将它们分开)

    【讨论】:

      【解决方案5】:

      您的总结是正确的,但是我想补充一点,如果您编译成 jar,那么 jar 将包含 Z(如果 Z 是 jar,则只有 Z jar 中需要的文件。

      但是,相同的 Z 可用于编译和运行时。

      【讨论】:

      • 即使对于具有许多子项目的复杂 Maven 项目也是如此,每个子项目都指定类型 JAR 并导入相同的类 Z?每个 JAR 文件都包含 Z 吗? (这似乎是多余的。)
      【解决方案6】:

      简单地说,不。如果你看一下 JDBC 代码,它是针对一个接口编译的,为此目的,它就像一个头文件,并使用反射在运行时拉入正确的实现。驱动程序根本不需要存在于构建机器上,尽管现在做这种事情的一种更简洁的方法是通过依赖注入框架。

      在任何情况下,没有什么可以阻止您针对一个“头”类文件进行编译,然后针对实际的类文件运行(Java 大部分是动态链接的),但这似乎是在做额外的为自己工作。

      【讨论】:

        猜你喜欢
        • 2017-01-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-07-08
        • 2017-02-18
        • 1970-01-01
        • 2016-01-04
        相关资源
        最近更新 更多