【问题标题】:Can a program depend on a library during compilation but not runtime?程序可以在编译期间依赖库而不是运行时吗?
【发布时间】:2011-10-27 14:05:15
【问题描述】:

我了解运行时和编译时之间的区别以及如何区分两者,但我只是认为没有必要区分编译时和运行时依赖关系

我哽咽的是:一个程序如何不依赖在运行时它在编译期间依赖的东西?如果我的 Java 应用程序使用 log4j,那么它需要 log4j.jar 文件才能编译(我的代码与 log4j 内部的成员方法集成并调用成员方法)以及运行时(我的代码完全无法控制 log4j 内部的代码会发生什么.jar 已运行)。

我正在阅读 Ivy 和 Maven 等依赖关系解析工具,这些工具清楚地区分了这两种类型的依赖关系。我只是不明白它的必要性。

谁能给出一个简单的“国王英语”式的解释,最好是一个即使像我这样的笨蛋也能理解的实际例子?

【问题讨论】:

  • 您可以使用反射,并使用在编译时不可用的类。想想“插件”。

标签: java build-process runtime compile-time


【解决方案1】:

运行时通常需要编译时依赖项。在 maven 中,compile 范围的依赖将在运行时添加到类路径中(例如,在战争中,它们将被复制到 WEB-INF/lib)。

但是,这不是严格要求的;例如,我们可以针对某个 API 进行编译,使其成为编译时依赖项,但随后在运行时包含一个也包含该 API 的实现。

可能会有项目需要一定的依赖才能编译但实际上并不需要相应代码的边缘情况,但这种情况很少见。

另一方面,包括在编译时不需要的运行时依赖项是很常见的。例如,如果您正在编写 Java EE 6 应用程序,您可以针对 Java EE 6 API 进行编译,但在运行时,可以使用任何 Java EE 容器;正是这个容器提供了实现。

使用反射可以避免编译时依赖。例如,可以使用Class.forName 加载 JDBC 驱动程序,而加载的实际类可以通过配置文件进行配置。

【讨论】:

  • 关于 Java EE API——这不就是“提供的”依赖范围的用途吗?
  • lombok (www.projectlombok.org) 是需要编译但在运行时不需要依赖项的示例。该 jar 用于在编译时转换 java 代码,但在运行时根本不需要。指定范围“提供”会导致 jar 不包含在 war/jar 中。
  • @Kevin 是的,很好,provided 作用域添加了一个编译时依赖项,而没有添加运行时依赖项,期望依赖项将在运行时通过其他方式提供(例如,在容器)。另一方面,runtime 添加了运行时依赖项,而不使其成为编译时依赖项。
  • 所以可以肯定地说,“模块配置”(使用 Ivy 术语)和项目根目录下的主目录之间存在 通常 1:1 的相关性吗?例如,我所有依赖于 JUnit JAR 的 JUnit 测试都将位于 test/ 根目录下,等等。我只是看不到打包在同一源根目录下的相同类如何被“配置”为依赖于不同的任何给定时间的 JAR。如果你需要log4j,那么你需要log4j;没有办法告诉相同的代码在 1 个配置下调用 log4j 调用,而是在某些“非日志记录”配置下忽略 log4j 调用,对吧?
【解决方案2】:

您在编译时需要在运行时可能需要的依赖项。然而,许多库在没有其所有可能依赖项的情况下运行。即可以使用四个不同 XML 库的库,但只需要一个即可工作。

很多库,依次需要其他库。这些库在编译时不需要,但在运行时需要。即代码实际运行时。

【讨论】:

  • 你能给我们一些编译时不需要但运行时需要的库的例子吗?
  • @Cristiano 所有的 JDBC 库都是这样的。还有实现标准 API 的库。
【解决方案3】:

通常你是对的,如果运行时和编译时依赖项相同,这可能是理想的情况。

当这条规则不正确时,我会给你举两个例子。

如果 A 类依赖于 B 类,而 B 类依赖于 C 类,而 C 类又依赖于 D 类,其中 A 是您的类,B、C 和 D 是来自不同第三方库的类,您在编译时只需要 B 和 C,并且您需要在运行时也是 D。 通常程序使用动态类加载。在这种情况下,您不需要在编译时使用的库动态加载的类。此外,库通常会选择在运行时使用哪个实现。例如 SLF4J 或 Commons Logging 可以在运行时更改目标日志实现。在编译时您只需要 SSL4J 本身。

当您在编译时需要比运行时更多的依赖项时的相反示例。 认为您正在开发必须在不同环境或操作系统下工作的应用程序。您在编译时需要所有特定于平台的库,而在运行时只需要当前环境所需的库。

希望我的解释有所帮助。

【讨论】:

  • 您能否详细说明为什么在您的示例中编译时需要 C?我得到的印象(来自stackoverflow.com/a/7257518/6095334)在编译时是否需要 C 取决于 A 引用的方法和字段(来自 B)。
【解决方案4】:

在编译时,您启用依赖项所期望的合同/api。 (例如:在这里您只需与宽带互联网提供商签订合同) 在运行时实际上您正在使用依赖项。 (例如:这里你实际上是在使用宽带互联网)

【讨论】:

    【解决方案5】:

    通常,静态依赖关系图是动态依赖关系图的子图,参见例如this blog entry from the author of NDepend

    也就是说,有一些例外,主要是添加编译器支持的依赖项,在运行时变得不可见。例如,通过Lombok 生成代码或通过(pluggable type-)Checker Framework 进行额外检查。

    【讨论】:

      【解决方案6】:

      刚刚遇到一个可以回答您问题的问题。 servlet-api.jar 是我的 Web 项目中的临时依赖项,在编译时和运行时都需要。但是servlet-api.jar 也包含在我的 Tomcat 库中。

      这里的解决方案是使 maven 中的 servlet-api.jar 仅在编译时可用,而不是打包在我的 war 文件中,这样它就不会与我的 Tomcat 库中包含的 servlet-api.jar 冲突。

      我希望这能解释编译时和运行时的依赖关系。

      【讨论】:

      • 对于给定的问题,您的示例实际上是不正确的,因为它解释了 compileprovided 范围之间的区别,而不是 compileruntime 之间的区别。 Compile scope 在编译时是必需的,并且打包在您的应用程序中。 Provided scope 仅在编译时需要,但不会打包在您的应用程序中,因为它是通过其他方式提供的,例如它已经在 Tomcat 服务器中。
      • 好吧,我认为这是一个相当的例子,因为问题是关于编译时和运行时依赖而不是compileruntime maven 范围provided 范围是 maven 处理编译时依赖项不应包含在运行时包中的情况的方式。
      【解决方案7】:

      每个 Maven 依赖项都有一个范围,该范围定义了该依赖项在哪个类路径上可用。

      当您为项目创建 JAR 时,依赖项不会与生成的工件捆绑在一起;它们仅用于编译。 (但是,您仍然可以让 maven 在构建的 jar 中包含依赖项,请参阅:Including dependencies in a jar with Maven

      当您使用 Maven 创建 WAR 或 EAR 文件时,您可以将 Maven 配置为将依赖项与生成的工件捆绑在一起,您还可以将其配置为使用 provided 范围从 WAR 文件中排除某些依赖项。

      最常见的范围 - compile - 表示在您执行应用程序时,您的项目在编译类路径、单元测试编译和执行类路径以及最终的运行时类路径上都可以使用该依赖项。在 Java EE Web 应用程序中,这意味着将依赖项复制到您部署的应用程序中。然而,在 JAR 文件中,当使用 compile 范围时,将包含依赖项。

      runtime 范围表示您的项目在单元测试执行和运行时执行类路径上可以使用该依赖项, 但与compile 范围不同,它在您编译应用程序时不可用 或其单元测试。 运行时依赖项被复制到您部署的应用程序中,但在编译期间不可用。这有助于确保您不会错误地依赖特定库。想象一下,您正在使用一个特定的日志记录实现,但您只想在源代码中导入一个日志记录外观。您将包含具有runtime 范围的具体日志库,因此您不会错误地依赖它。

      最后,provided 范围表示您的应用程序在其中执行的容器代表您提供依赖项。在 Java EE 应用程序中,这意味着依赖项已经在 Servlet 容器或应用程序服务器的类路径中,并且不会复制到您部署的应用程序中。这也意味着您需要此依赖项来编译您的项目。

      【讨论】:

      • @Koray Tugay 答案更准确 :) 我有一个快速的问题,说我有一个包含运行时范围的依赖 jar。 maven 会在编译时寻找 jar 吗?
      • @gks 不,它在编译时不需要它。
      【解决方案8】:

      要回答“程序如何在运行时不依赖它在编译期间依赖的东西?”这个问题,让我们看一下注解处理器的示例。

      假设您已经编写了自己的注解处理器,并且假设它在编译时依赖于com.google.auto.service:auto-service,因此它可以使用@AutoService。此依赖项仅在编译注解处理器时需要,但在运行时不需要:所有其他依赖于注解处理器来处理注解的项目在运行时不需要 com.google.auto.service:auto-service 的依赖项(也不在编译时或任何其他时间)。

      这不是很常见,但确实会发生。

      【讨论】:

        【解决方案9】:

        我了解运行时和编译时之间的区别以及如何 区分两者,但我只是不认为有必要 区分编译时依赖和运行时依赖。

        一般的编译时和运行时概念和 Maven 特定的 compileruntime 范围依赖是两个非常不同的东西。你不能直接比较它们,因为它们没有相同的框架:一般的编译和运行时概念很广泛,而 maven compileruntime 范围概念是关于根据时间的依赖关系可用性/可见性:编译或执行。
        不要忘记 Maven 首先是一个 javac/java 包装器,在 Java 中,您有一个用 javac -cp ... 指定的编译时类路径和一个用 java -cp ... 指定的运行时类路径。
        将 Maven compile 范围视为在 Java 编译和运行时 classppath(javacjava)中添加依赖项的一种方式并没有错,而 Maven runtime 范围可以被视为一种方式仅在 Java 运行时 classppath (javac) 中添加依赖项。

        让我哽咽的是:程序怎么能不依赖于某些东西 在编译期间它依赖的运行时?

        您所描述的与runtimecompile 范围没有任何关系。
        您为依赖项指定的 provided 范围看起来更像是在编译时而不是在运行时依赖于它。
        您在需要编译依赖项时使用它,但您不想将其包含在打包的组件(JAR、WAR 或任何其他组件)中,因为环境已经 提供 依赖项:它可以被包含在服务器或类路径的任何路径中指定为 Java 应用程序启动。

        如果我的 Java 应用程序使用 log4j,那么它需要 log4j.jar 文件才能编译(我的代码 从 log4j 内部集成和调用成员方法)为 以及运行时(我的代码完全无法控制发生的事情 一旦运行 log4j.jar 中的代码)。

        在这种情况下是的。但是假设您需要在 log4j 前面编写一个依赖 slf4j 作为外观的可移植代码,以便以后能够切换到另一个日志记录实现(log4J 2、logback 或任何其他)。
        在这种情况下,在您的 pom 中,您需要将 slf4j 指定为 compile 依赖项(这是默认设置),但您将 log4j 依赖项指定为 runtime 依赖项:

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>...</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>...</version>
            <scope>runtime</scope>
        </dependency>
        

        这样,log4j 类无法在编译后的代码中引用,但您仍然可以引用 slf4j 类。
        如果您使用compile 时间指定了两个依赖项,则没有什么会阻止您在编译代码中引用 log4j 类,您可能会因此与日志记录实现产生不良耦合:

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>...</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>...</version>
        </dependency>
        

        runtime 范围的常见用法是 JDBC 依赖声明。 要编写可移植代码,您不希望客户端代码可能引用特定 DBMS 依赖项的类(例如:PostgreSQL JDBC 依赖项),但您希望将其包含在应用程序中,因为在运行时需要这些类来制作JDBC API 与这个 DBMS 一起工作。

        【讨论】:

          【解决方案10】:

          runtime 作用域用于防止程序员在代码中向实现库添加直接依赖项,而不是使用抽象或外观。

          换句话说,它强制使用接口。

          具体例子:

          1) 您的团队正在使用 SLF4J 而不是 Log4j。您希望您的程序员使用 SLF4J API,而不是 Log4j。 Log4j 仅供 SLF4J 在内部使用。 解决方案:

          • 将 SLF4J 定义为常规编译时依赖项
          • 将 log4j-core 和 log4j-api 定义为运行时依赖项。

          2) 您的应用程序正在使用 JDBC 访问 MySQL。您希望您的程序员针对标准 JDBC 抽象进行编码,而不是直接针对 MySQL 驱动程序实现。

          • mysql-connector-java(MySQL JDBC 驱动程序)定义为运行时依赖项。

          运行时依赖项在编译期间被隐藏(如果您的代码对它们具有“直接”依赖项,则会引发编译时错误),但在执行期间和创建可部署工件(WAR 文件、SHADED jar 文件等)时包含在内。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-05-15
            • 1970-01-01
            • 2017-12-01
            • 1970-01-01
            • 2014-12-23
            • 2018-01-26
            相关资源
            最近更新 更多