Can a program depend on a library during compilation but not runtime?

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

令我窒息的是: 一个程序 不依赖如何能够在运行时处理它在编译过程中所依赖的东西?如果我的 Java 应用程序使用 log4j,那么它需要 log4j.jar 文件来编译(我的代码集成并调用 log4j 内部的成员方法)和运行时(我的代码完全无法控制一旦运行 log4j.jar 内部的代码会发生什么)。

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

有人能给出一个简单的,“国王的英语”类型的解释,最好是一个实际的例子,即使像我这样一个可怜的傻瓜可以理解?

57251 次浏览

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

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

There may be fringe cases where the project requires a certain dependency to compile but then the corresponding code is not actually needed, but these will be rare.

另一方面,包括在编译时不需要的运行时依赖性是非常常见的。例如,如果您正在编写一个 JavaEE6应用程序,那么您将根据 JavaEE6 API 进行编译,但是在运行时,可以使用任何 JavaEE 容器; 正是这个容器提供了实现。

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

您在编译时需要依赖关系,这在运行时可能需要。然而,许多库在运行时没有所有可能的依赖项。例如,可以使用四个不同的 XML 库,但只需要一个就可以工作的库。

Many libraries, need other libraries in turn. These libraries are not needed at compile time but are needed at runtime. i.e. when the code is actually run.

一般来说,如果运行时和编译时的依赖关系相同,那么您是对的,并且可能是理想的情况。

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

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

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

I hope my explanations help.

在编译时,您可以启用从依赖项获得的契约/api。 (例如: 在这里你只需要与宽带互联网供应商签订一份合同) At run-time actually you are using the dependencies. (例如: 在这里你实际上正在使用宽带互联网)

通常,静态依赖图是动态依赖图的一个子图,参见 这篇博文来自 NDepend 的作者

尽管如此,还是有一些例外,主要是添加编译器支持的依赖性,这在运行时是不可见的。例如,通过 Lombok生成代码或通过 (可插类型 -)检查器框架进行额外检查。

只是碰到一个问题,回答了你的问题。servlet-api.jar是我的 web 项目中的一个临时依赖项,在编译时和运行时都需要它。但是我的 Tomcat 库中也包含 servlet-api.jar

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

我希望这解释了编译时和运行时依赖。

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

当您为一个项目创建一个 JAR 时,依赖项并不与生成的工件绑定在一起; 它们只用于编译。(但是,您仍然可以让 maven 包含构建的 jar 中的依赖项,参见: 包括与 Maven 的 jar 中的依赖项)

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

最常见的作用域ーー compileーー表示在编译类路径、单元测试编译和执行类路径以及执行应用程序时的最终运行时类路径上,项目可以使用该依赖项。在 JavaEEweb 应用程序中,这意味着将依赖项复制到已部署的应用程序中。但是,在 JAR 文件中,当使用 compile作用域时,依赖项将包括 没有

runtime作用域指示在单元测试执行和运行时执行类路径上依赖项对项目可用, 但与 compile作用域不同,它是 在编译应用程序时不可用或其单元测试。将运行时依赖项复制到已部署的应用程序中,但在编译期间不可用。这有助于确保您不会错误地依赖于某个特定的库。假设您正在使用一个特定的日志实现,但是您只想在源代码中导入一个日志 facade。您可以使用 runtime作用域包含具体的日志库,这样就不会错误地依赖它。

最后,provided作用域指示应用程序在其中执行的容器代表您提供了依赖项。在 JavaEE 应用程序中,这意味着依赖项已经在 Servlet 容器或应用服务器的类路径和 不会复制到已部署的应用程序中。上,这也意味着您需要这个依赖项来编译项目。

为了回答这个问题“一个程序怎么可能在运行时不依赖于它在编译过程中所依赖的东西?”,让我们看一个注释处理器的例子。

假设您已经编写了自己的注释处理器,并假设它对 com.google.auto.service:auto-service有编译时依赖关系,这样它就可以使用 @AutoService。这种依赖关系只在编译注释处理器时才需要,但在运行时不需要: 所有其他依赖于注释处理器来处理注释的项目在运行时 没有都需要依赖于 com.google.auto.service:auto-service(无论是在编译时还是其他任何时候)。

这种情况并不常见,但确实会发生。

我理解运行时和编译时之间的区别,以及如何 to differentiate between the two, but I just don't see the need to 区分编译时依赖项和运行时依赖项。

一般的编译时和运行时概念以及 Maven 特定的 compileruntime作用域依赖关系是两个非常不同的概念。您不能直接比较它们,因为它们没有相同的框架: 一般的编译和运行时概念是宽泛的,而专业的 compileruntime范围概念是关于依赖关系的可用性/可见性,根据时间: 编译或执行。
不要忘记 Maven 首先是一个 javac/java包装器,在 Java 中有一个用 javac -cp ...指定的编译时类路径和一个用 java -cp ...指定的运行时类路径。
将 Maven compile作为一种在 Java 编译和运行时类路径(javacjava)中添加依赖关系的方法来考虑是没有错的,而将 Maven runtime作为一种只在 Java 运行时类路径(javac)中添加依赖关系的方法来考虑则可以看作是一种只在 Java 运行时类路径(javac)中添加依赖关系的方法。

让我窒息的是: 一个程序怎么可能不依赖于 在编译过程中所依赖的运行时?

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

如果我的 Java 应用程序使用 log4j,那么它需要 log4j.jar 文件来编译(我的代码) integrating with and invoking member methods from inside log4j) as 以及运行时(我的代码完全无法控制会发生什么 一旦运行 log4j.jar 中的代码)。

在这种情况下,是的。但是假设您需要编写一个可移植代码,该代码依赖于 log4J 前面的 slf4j 作为 facade,以便以后能够切换到另一个日志实现(log4J2、 logback 或其他任何日志实现)。
在这种情况下,您需要将 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 一起工作。

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

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

具体例子:

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

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

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

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

运行时依赖关系在编译期间是隐藏的(如果代码对它们有“直接”依赖关系,则抛出编译时错误) ,但在执行期间和创建可部署构件(WAR 文件、 SHADED jar 文件等)时包含在内。