Gradle 进阶学习之 Dependencies 【依赖】

Java
59
0
0
2024-09-15

1、依赖的方式

Gradle 中的依赖分别为直接依赖,项目依赖,本地 jar 依赖。

在Gradle中,依赖管理是一个非常重要的部分,它允许你指定项目所需的各种库和模块。你的案例中提到了三种常见的依赖类型:项目依赖、本地JAR依赖和远程仓库的直接依赖。下面我将分别解释这三种依赖类型,并提供相应的配置方法。

1.1 直接依赖(远程仓库依赖)

直接依赖指的是从远程仓库(如Maven Central)中获取的依赖。在Gradle中,你可以直接通过坐标来声明这些依赖,如下所示:

dependencies {
    // 直接依赖的简写形式
    implementation 'cn.hutool:hutool-all:5.8.27'

    // 直接依赖的完整形式
    iimplementation group: 'cn.hutool', name: 'hutool-all', version: '5.8.27'
}

image-20240420194013517

在Maven的pom.xml文件中,这相当于:

<dependencies>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.27</version>
	</dependency>
</dependencies>

在Gradle中,implementation是依赖配置之一,它相当于Maven中的compile作用域。

1.2 项目依赖

项目依赖是指在一个多模块项目中,一个模块依赖于另一个模块。在Gradle中,你可以使用project方法来声明这种依赖,如下所示:

dependencies {
    // 项目依赖
    implementation project(':subject01')
}

image-20240420194227026

settings.gradle文件中,你需要配置子模块的路径,以便Gradle能够识别它们:

rootProject.name = 'root'
include 'subject01' // 包含名为subject01的子模块
1.3 本地JAR依赖

本地JAR依赖指的是直接引用项目目录中的JAR文件。在Gradle中,你可以使用files方法或fileTree方法来声明这些依赖:

dependencies {
    // 直接依赖特定的JAR文件
    implementation files('libs/foo.jar', 'libs/bar.jar')

    // 配置一个文件夹作为依赖项,自动包含该文件夹下的所有JAR文件
    implementation fileTree(dir: 'libs', include: ['*.jar'])
}

使用fileTree方法时,Gradle会自动包含指定目录下的所有JAR文件作为依赖。

总结
  • 直接依赖:从远程仓库获取的依赖,使用坐标声明。
  • 项目依赖:在一个多模块项目中,一个模块对另一个模块的依赖。
  • 本地JAR依赖:直接引用本地文件系统中的JAR文件。

每种依赖类型都有其适用场景,你可以根据项目的具体需求选择合适的依赖管理方式。

2、依赖的下载

当执行 build 命令时,gradle 就会去配置的依赖仓库中下载对应的 Jar,并应用到项目中。

3、依赖的类型

类似于 Maven 的 scope 标签,gradle 也提供了依赖的类型,具体如下所示:

配置选项

描述

适用插件

compileOnly

编译时需要但不打包的依赖。曾短暂称为provided。

Java

runtimeOnly

运行时需要,编译时不需要的依赖,如数据库驱动。

Java

implementation

编译和运行时都需要的依赖。

Java

testCompileOnly

仅用于编译测试的依赖,运行时不需要。

Java

testRuntimeOnly

仅在测试运行时需要的依赖。

Java

testImplementation

针对测试代码的依赖,取代老版本中的testCompile。

Java

providedCompile

WAR插件专用,编译和测试需要,运行时由容器提供。

WAR

compile

已在Gradle 7.0中移除,原表示编译时和打包时都需要的依赖。

Java (已移除)

runtime

已在Gradle 7.0中移除,原表示运行和测试时需要的依赖。

Java (已移除)

api

编译时和运行时都需要的依赖,并且可以被使用者传递性地导出。

java-library

compileOnlyApi

仅在编译时需要的依赖,运行时不需要,可以被使用者传递性地导出。

java-library

请注意,compileruntime配置选项在Gradle 7.0中已经被移除,推荐使用implementationruntimeOnly作为替代。此外,apicompileOnlyApijava-library插件提供的,用于更细粒度地控制模块间的依赖关系。

官方文档参考: https://docs.gradle.org/current/userguide/java_library_plugin.html#java_library_plugin: 各个依赖范围的关系和说明 https://docs.gradle.org/current/userguide/upgrading_version_6.html#sec:configuration_removal : 依赖范围升级和移除 https://docs.gradle.org/current/userguide/java_library_plugin.html#java_library_plugin:API 和 implemention 区别 https://docs.gradle.org/current/userguide/java_plugin.html#java_plugin: 执行 java 命令时都使用了哪些依赖范围的依赖。

提示:java 插件提供的功能,java-library 插件都提供

4、api 与 implementation 区别

以下是您提供的关于Gradle依赖配置选项apiimplementation的比较表格,我对其进行了优化和整理:

特性/场景

api配置选项

implementation配置选项

编译时

- 依赖可以传递给模块的使用者。

- 依赖不会传递给模块的使用者。


- 当底层依赖发生变化时,所有依赖了这些底层依赖的模块都需要重新编译,可能导致编译速度变慢。

- 当底层依赖发生变化时,只有直接依赖了这些底层依赖的模块需要重新编译,编译速度相对较快。

运行时

- 所有模块的类都会被加载。

- 所有模块的类都会被加载。

应用场景

- 适用于多模块的项目,特别是当你想要避免重复依赖同一个模块时。

- 在大多数情况下使用,尤其是当你不希望依赖传递给模块使用者时。

apiimplementation是Gradle中常用的两种依赖配置选项,它们在编译时和运行时的行为有所不同。api配置选项允许依赖传递,这在多模块的项目中非常有用,可以确保模块间的依赖关系一致性。而implementation配置选项则不会将依赖传递给使用者,这在大多数情况下是推荐使用的,因为它可以减少不必要的依赖传递,从而提高项目的构建效率。

以下是对这两种依赖类型的详细解释,以及它们在编译时和运行时的不同影响:

4.1 api 依赖
  • 编译时:当一个库(如 libC)被声明为 api 依赖时,它的内容变化会导致所有直接和间接依赖它的项目(如 libA 和 projectX)都需要重新编译。这是因为 api 依赖的变更可能会影响所有使用该 API 的代码。
  • 运行时:在运行时,所有通过 api 依赖的库(如 libC、libA)以及最终的应用程序(如 projectX)中的类都会被类加载器加载。
  • 适用场景api 适用于多模块项目中的依赖管理,特别是当你想避免重复依赖时。例如,如果 moduleA 依赖 moduleB,而 moduleB 又依赖 moduleC,同时 moduleA 也需要 moduleC 中的某些类或方法,你可以将 moduleC 作为 api 依赖添加到 moduleB 中。这样,moduleA 只需要依赖 moduleB,而 moduleC 的依赖会被传递。
4.2 implementation 依赖
  • 编译时:使用 implementation 依赖时,依赖的传递性会被限制。如果一个库(如 libD)被声明为 implementation 依赖,并且它的内容发生变化,只有直接依赖它的库(如 libB)需要重新编译。不依赖于 libD 的项目(如 libA 和 projectX)不需要重新编译,这可以加快编译速度。
  • 运行时:尽管在编译时 implementation 依赖不会被传递,但在运行时,所有通过 implementation 依赖引入的库(如 libD、libB)以及最终的应用程序(如 projectX)中的类都会被加载。
  • 适用场景implementation 适用于那些不应该被其他模块或应用程序直接使用的库。它通常是内部实现细节,不是用来暴露公共 API 的。
4.3 拓展

api 和 implementation 案例分析 :

在多模块项目中,使用 apiimplementation 可以有效地管理模块之间的依赖关系:

  • 使用 api:当你希望一个模块的依赖成为另一个模块的公共 API 时,使用 api。这样,任何依赖于该模块的项目都能够访问到这些 API。
  • 使用 implementation:当你希望隐藏一个模块的实现细节,只将必要的功能暴露给依赖它的模块时,使用 implementation。这有助于减少编译时的依赖传递,提高构建效率。

总之,除非涉及到多模块依赖,为了避免重复依赖,咱们会使用api,其它情况我们优先选择implementation,拥有大量的 api依赖项会显著增加构建时间。

5、依赖冲突及解决方案

依赖冲突是指 “在编译过程中, 如果存在某个依赖的多个版本, 构建系统应该选择哪个进行构建的问题”,如下所示:

image-20240420195402674

A、B、C 都是本地子项目 module,log4j 是远程依赖。

编译时: B 用 1.4.2 版本的 log4j,C 用 2.2.4 版本的 log4j,B 和 C 之间没有冲突

打包时: 只能有一个版本的代码最终打包进最终的A对应的 jar 或 war包,对于 Gradle 来说这里就有冲突了

5.1 案例演示:

我们在 build.gradle 引入依赖库

image-20240420195849916

修改 build.gradle

image-20240420195949082

如上所示:默认下,Gradle 会使用最新版本的 jar 包【考虑到新版本的 jar 包一般都是向下兼容的】,实际开发中,还

是建议使用官方自带的这种解决方案。当然除此之外,Gradle 也为我们提供了一系列的解决依赖冲突的方法: exclude

移除一个依赖不允许依赖传递强制使用某个版本

5.2 Exclude 排除某个依赖

image-20240420200138605

5.3 不允许依赖传递【一般不用】

image-20240420200227442

在添加依赖项时,如果设置 transitive 为 false,表示关闭依赖传递。即内部的所有依赖将不会添加到编译和运行时的类路径。

5.4 强制使用某个版本【官方建议】

image-20240420200349538

拓展

在 Gradle 中,如果你想要避免依赖冲突并确保构建的可预测性,你可以配置构建过程在遇到依赖冲突时立即失败。这可以帮助你快速发现并解决版本不一致的问题。

以下是如何配置 Gradle 在遇到依赖冲突时立即失败的示例:

configurations.all {
    resolutionStrategy.failOnVersionConflict()
}

这段代码应该放在你的 build.gradle 文件中的项目配置部分。通过使用 configurations.all 方法,你可以为项目中所有的配置应用这个策略。resolutionStrategy.failOnVersionConflict() 会让 Gradle 在解析依赖时,如果遇到任何版本冲突,就会立即停止构建并报告错误。

依赖冲突检查的好处
  • 及时发现问题:构建失败提供了一个明确的信号,表明依赖之间存在不兼容的版本,这可以防止不兼容的依赖被无意中包含进构建中。
  • 简化调试:立即失败可以简化调试过程,因为你不需要去猜测为什么构建成功但运行时却出现问题。
  • 避免运行时错误:通过确保所有依赖都是兼容的,可以减少运行时由于依赖不匹配导致的意外错误。
注意事项
  • 在团队协作中,这个策略可以帮助每个成员都意识到依赖版本的重要性。
  • 对于大型项目或有复杂依赖关系的情况,这个策略可能会导致频繁的构建失败,因此可能需要配合其他依赖管理策略使用。
  • 在实际操作中,可能需要结合项目的具体需求和依赖管理策略来决定是否使用这个选项。

通过这种方式,你可以更好地控制项目的依赖,确保依赖的一致性和项目的稳定性。