【问题标题】:Compile time vs Run time Dependency - Java编译时间与运行时间依赖关系 - Java
【发布时间】:2011-05-15 07:57:33
【问题描述】:

Java 中的编译时依赖和运行时依赖有什么区别? 和类路径有关,但是它们有什么区别呢?

【问题讨论】:

    标签: java runtime classpath compile-time


    【解决方案1】:

    从@Jason S 的回答中,我用其他词得出我的答案,以防万一:

    应用的运行时依赖实际上是该应用的编译时依赖(L1)的依赖(我们称之为L2)。如果应用程序不使用它,则它可能不会被声明为依赖项。

    • 如果 L2 恰好被应用程序(通过 L1)使用但未声明为依赖项,则会出现 NoClassDefFoundError。

    • 如果 L2 被声明为应用程序的编译时依赖项,而不是在运行时使用,它会毫无用处地使 jar 更大并且编译时间比需要的长。

    将 L2 声明为运行时依赖项允许 JVM 仅在需要时延迟加载它。

    【讨论】:

      【解决方案2】:

      对于 Java,编译时依赖是源代码的依赖。例如,如果 A 类从 B 类调用一个方法,那么 A 在编译时依赖于 B,因为 A 必须知道要编译的 B(B 的类型)。这里的诀窍应该是:编译后的代码还不是完整的可执行代码。它包括尚未编译或存在于外部 jar 中的源的可替换地址(符号、元数据)。在链接期间,这些地址必须替换为内存中的实际地址。要正确执行此操作,应创建正确的符号/地址。这可以通过类 (B) 的类型来完成。我相信这是编译时的主要依赖。

      运行时依赖与实际的控制流更相关。它涉及实际的内存地址。这是您在程序运行时所拥有的依赖项。您需要此处的 B 类详细信息,例如实现,而不仅仅是类型信息。如果该类不存在,那么您将得到 RuntimeException 并且 JVM 将退出。

      这两个依赖项通常和不应该流向相同的方向。这是面向对象设计的问题。

      在 C++ 中,编译有点不同(不仅仅是即时编译),但它也有一个链接器。所以我猜这个过程可能被认为类似于 Java。

      【讨论】:

        【解决方案3】:

        一个简单的例子是查看像 servlet api 这样的 api。要编译 servlet,您需要 servlet-api.jar,但在运行时 servlet 容器提供了 servlet api 实现,因此您无需将 servlet-api.jar 添加到运行时类路径中。

        【讨论】:

        • 为了澄清(这让我很困惑),如果您使用 maven 并构建战争,“servlet-api”通常是“提供”依赖而不是“运行时”依赖,这会导致它如果我是正确的,我会被卷入战争。
        • 'provided' 表示在编译时包含,但不要将其捆绑在 WAR 或其他依赖项集合中。 'runtime' 则相反(编译时不可用,但与 WAR 打包在一起)。
        【解决方案4】:
        • 编译时依赖项:您需要 CLASSPATH 中的依赖项来编译您的工件。它们的产生是因为您对代码中硬编码的依赖项有某种“引用”,例如为某个类调用 new,扩展或实现某些东西(直接或间接),或者使用直接 @987654323 的方法调用@ 表示法。

        • 运行时依赖项:您需要 CLASSPATH 中的依赖项来运行您的工件。它们的产生是因为您执行访问依赖项的代码(以硬编码方式或通过反射或其他方式)。

        虽然编译时依赖通常意味着运行时依赖,但您也可以只拥有编译时依赖。这是基于 Java 仅在第一次访问该类时链接类依赖项的事实,因此如果您在运行时从不访问特定类,因为代码路径从未被遍历,Java 将忽略该类及其依赖项。

        这个例子

        在 C.java 中(生成 C.class):

        package dependencies;
        public class C { }
        

        在A.java中(生成A.class):

        package dependencies;
        public class A {
            public static class B {
                public String toString() {
                    C c = new C();
                    return c.toString();
                }
            }
            public static void main(String[] args) {
                if (args.length > 0) {
                    B b = new B();
                    System.out.println(b.toString());
                }
            }
        }
        

        在这种情况下,ACB 具有编译时依赖关系,但如果在执行 java dependencies.A 时传递一些参数,它只会对 C 有运行时依赖关系,因为JVM 只会在执行B b = new B() 时尝试解决BC 的依赖。此功能允许您在运行时仅提供您在代码路径中使用的类的依赖关系,而忽略工件中其余类的依赖关系。

        【讨论】:

        • 我知道这是一个非常古老的答案,但是 JVM 怎么能从一开始就没有 C 作为运行时依赖项呢?如果它能够识别“这是对 C 的引用,是时候将其添加为依赖项了”,那么自从 JVM 识别并知道它在哪里之后,C 不是本质上已经是一个依赖项了吗?
        • @wearebob 我猜可以这样指定,但他们认为惰性链接更好,我个人同意上述原因:它允许您在必要时使用一些代码,但是如果您不需要它,不会强制您将其包含在您的部署中。这在处理第三方代码时非常方便。
        • 如果我在某个地方部署了一个 jar,它已经必须包含它的所有依赖项。它不知道它是否会与参数一起运行(因此它不知道是否将使用 C),因此无论哪种方式都必须有 C 可用。我只是看不到从一开始就没有 C 在类路径中如何节省内存/时间。
        • @wearebob JAR 不需要包含其所有依赖项。这就是为什么几乎每个重要的应用程序都有一个 /lib 目录或包含多个 JAR 的类似目录。
        • @wearebob。这个问题涉及软件架构和应用程序生命周期。考虑公共 API 和服务实现。编译/运行时的概念也反映在 Gradle 等构建工具中。将“实现”视为一些可交换的服务代码。在简单的应用程序中,编译和运行时代码库通常来自 uber Jar。如果企业应用程序可能会经历多个版本,那么情况会更加复杂,因为您必须升级依赖项。编译/运行时有助于保持向后兼容性。希望这会有所帮助
        【解决方案5】:

        编译器需要正确的类路径才能编译对库的调用(编译时依赖)

        JVM 需要正确的类路径才能加载您正在调用的库中的类(运行时依赖项)。

        它们可能在以下几个方面有所不同:

        1)如果你的类C1调用库类L1,而L1调用库类L2,那么C1对L1和L2有运行时依赖,而对L1只有编译时依赖。

        2) 如果你的类 C1 使用 Class.forName() 或其他机制动态实例化接口 I1,并且接口 I1 的实现类是类 L1,那么 C1 对 I1 和 L1 有运行时依赖,但只有一个编译时对 I1 的依赖。

        编译时和运行时相同的其他“间接”依赖项:

        3) 你的 C1 类扩展库类 L1,L1 实现接口 I1 并扩展库类 L2:C1 对 L1、L2 和 I1 有编译时依赖。

        4) 你的类 C1 有一个方法 foo(I1 i1) 和一个方法 bar(L1 l1) 其中 I1 是一个接口,L1 是一个类,它接受一个接口 I1 的参数:C1 对 I1 和 L1 有编译时依赖性.

        基本上,要做任何有趣的事情,您的类需要与类路径中的其他类和接口进行接口。由该组库接口形成的类/接口图产生编译时依赖链。库实现产生运行时依赖链。请注意,运行时依赖链是运行时依赖的或失败缓慢的:如果 L1 的实现有时依赖于实例化一个L2 类的对象,并且该类仅在一个特定场景中被实例化,那么除了该场景之外没有依赖关系。

        【讨论】:

        • 示例1中的编译时依赖不应该是L1吗?
        • 谢谢,但是类加载在运行时是如何工作的呢?在编译时很容易理解。但是在运行时,如果我有两个不同版本的罐子,它会如何运作?它会选择哪一个?
        • 我很确定默认的类加载器采用类路径并按顺序遍历它,所以如果类路径中有两个包含相同类的 jar(例如 com.example.fooutils.Foo ),它将使用类路径中的第一个。要么,要么你会得到一个错误,说明歧义。但是,如果您想了解更多特定于类加载器的信息,您应该提出一个单独的问题。
        • 我认为在第一种情况下,编译时依赖关系也应该存在于 L2 上,即这句话应该是:1)如果你的 C1 类调用库类 L1,而 L1 调用库类 L2,那么C1 对 L1 和 L2 有运行时依赖,但对 L1 和 L2 只有编译时依赖。就是这样,正如在编译时 java 编译器验证 L1 时一样,它还会验证 L1 引用的所有其他类(不包括 Class.forName("myclassname) 等动态依赖项)......否则它如何验证编译工作正常。如果您不这么认为,请说明
        • 没有。您需要阅读 Java 中编译和链接的工作原理。当它引用一个外部类时,编译器关心的是如何使用那个类,例如它的方法和领域是什么。它并不关心该外部类的方法中实际发生了什么。如果 L1 调用 L2,那就是 L1 的一个实现细节,而 L1 已经在别处编译了。
        【解决方案6】:

        编译时依赖项只是您在正在编译的类中直接使用的依赖项(其他类)。运行时依赖项涵盖了您正在运行的类的直接和间接依赖项。因此,运行时依赖项包括依赖项的依赖项和任何反射依赖项,例如您在 String 中拥有但在 Class#forName() 中使用的类名。

        【讨论】:

        • 谢谢,但是类加载在运行时是如何工作的呢?在编译时很容易理解。但是在运行时,如果我有两个不同版本的罐子,它会如何运作?如果类路径中有多个不同类的类,Class.forName() 会拾取哪个类?
        • 当然是匹配名字的那个。如果您实际上是指“同一类的多个版本”,那么它取决于类加载器。将加载“最近”的。
        • 好吧,我想如果你有带有A 的A.jar、带有B extends A 的B.jar 和带有C extends B 的C.jar 那么C.jar 甚至取决于A.jar 上的编译时间虽然 C 对 A 的依赖是间接的。
        • 所有编译时依赖的问题是interface依赖(无论接口是通过类的方法,还是通过接口的方法,还是通过包含参数的方法那是一个类或接口)
        【解决方案7】:

        Java 在编译时实际上并没有链接任何东西。它仅使用在 CLASSPATH 中找到的匹配类来验证语法。直到运行时,所有东西才被放在一起并基于当时的 CLASSPATH 执行。

        【讨论】:

        • 直到加载时间...运行时与加载时间不同。
        猜你喜欢
        • 2015-11-10
        • 1970-01-01
        • 2023-04-05
        • 1970-01-01
        • 1970-01-01
        • 2010-10-25
        • 1970-01-01
        • 1970-01-01
        • 2021-02-13
        相关资源
        最近更新 更多