Java:编译器和JRE是否需要访问所有第三方类文件?

时间:2011-04-03 01:06:46

标签: maven javac java

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

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

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

如果我在COMPILE时使用类加载器是正确的,那么如果需要用户定义的类加载器(L)并且是正在编译的项目的一部分呢?例如,假设L负责通过网络下载Z.class AT RUNTIME?在这种情况下,Java编译器将如何在编译时获取Z.class?它会先尝试编译L,然后在编译时使用L来获得Z吗?

据我所知,使用Maven构建项目,Z.jar可以在编译时通过Internet定位在远程存储库中 - 在ibiblio上,或在POM文件中定义的自定义存储库中。我希望我是正确的,它是MAVEN负责在编译时下载第三方JAR文件,而不是编译器的JVM?

但请注意,在RUNTIME,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的完整[byte]代码需要存在TWICE - 一次在编译期间,第二次在运行时 - 并且对于Java程序引用的 ALL 类必须如此。换句话说,每个类必须下载/呈现TWICE。在编译期间,没有一个类可以表示为头文件(就像在C ++中一样)。

6 个答案:

答案 0 :(得分:3)

  

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

几乎。它使用JavaFileManager,它在很多方面都像一个类加载器。它实际上并不加载类,因为它需要从.java文件和.class文件创建类签名。

  

我希望我是正确的,它是MAVEN负责在编译时下载第三方JAR文件,而不是编译器的JVM?

是的,虽然可以实现一个行为类似于URLClassLoader的JavaFileManager,但是Maven可以下载jar。 Maven管理jar的本地缓存,并根据需要从网络填充缓存。

  

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

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

  

我的整体困惑可归纳如下。似乎Z的完整[byte]代码需要存在TWICE - 一次在编译期间,第二次在运行时 - 并且对于Java程序引用的所有类必须为true。换句话说,每个类必须下载/呈现TWICE。在编译期间,没有一个类可以表示为头文件(就像在C ++中一样)。

也许一个例子可以帮助澄清这一点。 java语言规范要求编译器进行某些优化。内联static final原始内容和String s。

如果课程A仅依赖于B的常量:

class B {
  public static final String FOO = "foo";
}

class A {
  A() { System.out.println(B.FOO); }
}

然后可以在类路径上编译,加载和实例化A而不需要B.class。 如果您更改并发送了B.class并使用不同的FOO值,那么A仍然具有该编译时依赖性。

因此可以具有编译时依赖性而不是链接时依赖性。

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

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

在类加载时(运行时),字节码验证器检查预期的方法和属性是否确实存在。因此,字节码验证器会仔细检查编译器所做的假设(除了内联假设之外)。

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

答案 1 :(得分:3)

了解Maven如何适应图片的最佳方式是意识到它(大部分)没有。

Maven不参与编译器查找定义的过程,或者运行时系统加载类。编译器本身就是这样做的...基于构建时类路径所说的内容。当您运行该应用程序时,Maven根本不再处于图片中。

在构建时,Maven的职责是检查在POM文件中声明的项目依赖项,检查版本,下载缺少的项目,将JAR放在一个众所周知的地方并创建一个“类路径”要使用的编译器(和其他工具)。

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

编译完成后,Maven会负责打包到JAR,WAR,EAR文件等,这是由POM文件指定的。对于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依赖于实现IIC1的接口IC2。除非A显式依赖IC1IC2,否则编译器无需加载它们即可编译A

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


您写道:

  

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

这不是一个方便/进化的东西。 Java从未支持单独的头文件。詹姆斯·戈斯林等人从头文件和预处理器是一个坏主意的位置开始。

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

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

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

答案 2 :(得分:1)

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

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

答案 3 :(得分:1)

答案 4 :(得分:0)

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

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

答案 5 :(得分:0)

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

在任何情况下,没有什么能阻止你编译一个'header'类文件,然后针对实际的类文件运行(Java 主要是动态链接)但这似乎只是在做额外的为自己工作。