一个程序在编译时是否可以依赖于库,但在运行时不依赖于该库?

127

我理解运行时和编译时的区别,以及如何区分二者,但我不觉得有必要区分编译时和运行时依赖关系

我所困惑的是:一个程序如何在运行时不依赖于编译时所依赖的东西?如果我的Java应用程序使用了log4j,则它需要log4j.jar文件才能进行编译(我的代码集成并调用log4j内部成员方法)以及运行时(我的代码对一旦运行log4j.jar中的代码发生的事情没有任何控制)。

我正在阅读诸如Ivy和Maven之类的依赖关系解析工具的相关文档,而这些工具明显区分了这两种类型的依赖关系。我只是不理解为什么需要区分。

可以有人给出一个简单的“通俗易懂”的解释,最好附带一个实际的例子,让像我这样的可怜人也能理解吗?


2
你可以使用反射,并使用在编译时不可用的类。想象一下"插件"。 - Per Alexandersson
10个回答

73

编译时依赖通常在运行时也是必需的。在Maven中,使用compile范围的依赖项将在运行时添加到类路径中(例如,在WAR文件中,它们将被复制到WEB-INF/lib目录下)。

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

可能存在一些边缘情况,项目需要某个依赖项来编译,但实际上对应的代码并不是必需的,但这种情况很少见。

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

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


21
Java EE API不是"provided"依赖范围的作用吗? - Kevin
2
@Kevin 是的,说得好。provided 作用域会在编译时添加依赖项,但不会在运行时添加依赖项,因为期望该依赖项将通过其他方式(例如容器中的共享库)在运行时提供。另一方面,runtime 会添加运行时依赖项,但不会使其成为编译时依赖项。 - Artefacto
那么可以说,在项目根目录下的每个主要目录和“模块配置”(使用Ivy术语)通常是一对一的吗?例如,所有依赖于JUnit JAR的JUnit测试将位于test/根目录下等等。我只是不明白,相同的类在相同的源代码根目录下如何在任何给定时间被“配置”为依赖于不同的JAR。如果您需要log4j,则需要log4j;没有办法告诉相同的代码在1个配置下调用log4j调用,但在某些“非日志记录”配置下忽略log4j调用,对吧? - IAmYourFaja

39
每个Maven依赖关系都有一个作用域,定义了该依赖关系可用的类路径。
创建项目JAR文件时,依赖项不会与生成的构件捆绑在一起;它们仅用于编译。(但是,您仍然可以让maven在构建的jar中包括依赖项,请参见:使用Maven将依赖项包含在jar中)
使用Maven创建WAR或EAR文件时,您可以配置Maven将依赖项与生成的构件一起打包,并且还可以配置它使用“provided”作用域从WAR文件中排除某些依赖项。
最常见的作用域是“compile”,它表示依赖项在编译类路径、单元测试编译和执行类路径以及执行应用程序时最终运行时类路径上对您的项目可用。在Java EE Web应用程序中,这意味着将依赖项复制到已部署的应用程序中。但是,在JAR文件中,当使用“compile”作用域时,依赖项不会被包含。
“runtime”作用域表示依赖项在单元测试执行和运行时执行类路径上可供您的项目使用,但与“compile”作用域不同,它在编译应用程序或其单元测试时不可用。运行时依赖项被复制到已部署的应用程序中,但在编译期间不可用。这对于确保您不会错误地依赖于特定库非常有用。想象一下,您正在使用特定的日志记录实现,但只想在源代码中导入一个日志记录外观。您将使用“runtime”作用域包含具体的日志库,以便您不会错误地依赖于它。
最后,“provided”作用域表示容器代表您的应用程序提供依赖项。在Java EE应用程序中,这意味着依赖项已经在Servlet容器或应用程序服务器的类路径上,并且不会被复制到已部署的应用程序中。这也意味着您需要此依赖项来编译项目。

@Koray Tugay的回答更加精确 :) 我有一个快速问题,假设我有一个运行时范围的依赖jar。 Maven会在编译时查找该jar吗? - thar45
@gks 不,它不需要在编译时要求。 - Koray Tugay

9

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

许多库反过来需要其他库。这些库在编译时不需要,但在运行时需要。也就是说,当代码实际运行时需要这些库。


你能给我们举一些这样的库的例子吗?这些库在编译时不需要,但在运行时需要。 - Cristiano
1
@Cristiano 所有的JDBC库都是这样的。同样也包括实现标准API的库。 - Peter Lawrey

7
我了解运行时和编译时的区别并且知道如何区分两者,但我认为没有必要区分编译时和运行时依赖关系。一般的编译时和运行时概念以及Maven特定的“compile”和“runtime”范围依赖是两个非常不同的东西。你不能直接进行比较,因为它们的框架不同:一般的编译和运行时概念是广泛的,而Maven“compile”和“runtime”范围概念是关于依赖项在编译或执行时的可用性/可见性。
不要忘记Maven首先是一个“javac”/“java”包装器,在Java中,您有一个编译时类路径,可以使用“javac -cp…”指定,并且有一个运行时类路径,可以使用“java -cp…”指定。
可以把Maven“compile”范围视为在Java编译和运行时类路径(“javac”和“java”)中同时添加依赖项的一种方式,而可以将Maven“runtime”范围视为仅在Java运行时类路径(“javac”)中添加依赖项的一种方式。 你所描述的与“runtime”和“compile”范围没有任何关系。
这更像是你为某个依赖项指定“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配合工作。


5

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

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

具体例子:

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

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

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

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

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


4

一般而言,您说得对,如果运行时和编译时的依赖关系是相同的话,那可能是理想的情况。

我将给您举两个例子,这种规则不正确的情况。

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

相反的例子是,您需要在编译时比在运行时有更多的依赖关系。 考虑您正在开发的应用程序必须在不同的环境或操作系统中工作。您需要在编译时获取所有特定于平台的库,并且仅在运行时获取当前环境所需的库。

希望我的解释有所帮助。


你能详细说明一下为什么在你的例子中需要在编译时使用C吗?我从http://stackoverflow.com/a/7257518/6095334上的回答中得出的印象是,A引用了B的哪些方法和字段决定了是否需要在编译时使用C。 - Hervian

3
通常情况下,静态依赖关系图是动态依赖关系图的子图,例如请参考NDepend作者的这篇博客文章
也就是说,有一些例外情况,主要是添加编译器支持的依赖关系,在运行时变得不可见。例如通过Lombok进行代码生成或通过(可插入类型-)Checker Framework进行附加检查。

2
我刚遇到一个问题,可以回答你的问题。 servlet-api.jar 是我 Web 项目中的一个瞬时依赖项,并且在编译时和运行时都需要它。但是,在我的 Tomcat 库中也包含了 servlet-api.jar
解决方法是使 Maven 中的 servlet-api.jar 只能在编译时使用,不要将其打包到 war 文件中,这样就不会与我的 Tomcat 库中包含的 servlet-api.jar 冲突。
希望这样解释了“编译时依赖”和“运行时依赖”的概念。

4
针对所给问题,你的例子实际上是不正确的,因为它解释了compileprovided范围之间的区别,而不是compileruntime之间的区别。compile范围在编译时需要,并且会打包进你的应用程序中。而provided范围在编译时需要,但不会打包进你的应用程序中,因为它是由其他方式提供的,例如它已经存在于Tomcat服务器中。 - MJar
1
我认为这是一个相当不错的例子,因为问题涉及编译时和运行时依赖关系,而不是compileruntime maven范围。 provided范围是maven处理编译时依赖项不应包含在运行时包中的情况的方式。 - Christian Gawron

1
要回答“如何使程序在运行时不依赖于编译时所依赖的内容?”这个问题,让我们看一个注解处理器的例子。
假设您已经编写了自己的注解处理器,并且假设它在编译时依赖于com.google.auto.service:auto-service,以便它可以使用@AutoService。这种依赖关系仅在编译注解处理器时需要,但在运行时不需要:所有依赖于您的注解处理器来处理注解的其他项目,在运行时(包括编译时和其他任何时间)都不需要依赖于com.google.auto.service:auto-service
这种情况并不是很常见,但确实会发生。

1

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接