在软件项目开发中,依赖项管理是至关重要的一环。sbt(Simple Build Tool)作为Scala领域最常用的构建工具之一,提供了便捷的依赖项管理机制,既支持托管依赖项,也支持非托管依赖项。sbt 使用 Apache Ivy 作为其依赖管理系统,支持 Maven 和 Ivy 依赖格式。本文将对sbt的依赖管理逻辑进行一些个人观点上概述,水平有限,还请见谅。
什么是依赖项
我们首先来了解一下依赖项的概念,依赖项(Dependency)通常指的是具体的软件包、库或模块,它是构建或运行一个软件项目所需的外部资源。在某种程度上,依赖项可以看作是依赖关系的实现,因为它们实际上是项目中需要的外部资源。例如: 以下是一个简单的Java项目,使用 Maven 来管理依赖项。假设你想要在你的 Java 项目中使用 Google 的 Gson 库,这个库可以帮助你处理 JSON 数据。但是在这之前,你需要创建一个 Maven 项目,然后在 pom.xml 文件中添加 Gson 作为依赖项。
代码语言:javascript复制<!-- pom.xml -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-java-project</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- 添加 Gson 依赖项 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.8</version>
</dependency>
</dependencies>
</project>
接下来,Maven 将会下载 Gson 库并将其添加到你的项目中。接下来,你可以编写 Java 代码来使用 Gson 库。
代码语言:javascript复制// MyMain.java
import com.google.gson.Gson;
public class MyMain {
public static void main(String[] args) {
// 创建 Gson 对象
Gson gson = new Gson();
// 将 JSON 字符串转换为 Java 对象
String json = "{"name":"John", "age":30}";
Person person = gson.fromJson(json, Person.class);
// 输出 Java 对象的属性
System.out.println("Name: " person.getName());
System.out.println("Age: " person.getAge());
}
}
// 定义一个简单的 Java 类来表示 JSON 数据
class Person {
private String name;
private int age;
// getter 和 setter 方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
托管依赖项和非托管依赖项
首先我们来了解一下什么是托管依赖项和非托管依赖项:
托管依赖项是指通过在项目的构建文件(通常是build.sbt)中声明依赖项,然后由构建工具自动从远程仓库(如Maven中心)下载所需的库和框架。这种方式省去了手动下载、安装和配置依赖项的繁琐步骤,使得项目的依赖项管理更加简单和自动化。
非托管依赖项是指开发人员手动管理和引入项目所需的依赖项,通常是通过将依赖项的 JAR 文件放置在项目的某个目录下,或者直接引用本地文件路径来实现。
我们举个例子:
如果您有要在项目中使用的 jar 文件(非托管依赖项),只需将它们复制到 sbt 项目根目录下的 lib 文件夹中,sbt 就会自动找到它们。如果这些 jar 依赖于其他 jar文件,则必须下载这些其他 jar 文件并将它们复制到 lib 目录。
如果您有一个托管依赖项,例如想要在项目中使用 Java HtmlCleaner 库,请在 build.sbt 文件中添加如下行(就像maven的.xml文件中添加依赖项):libraryDependencies (对于sbt的项目结构和代码部分在后面的部分会有一些小说明)
代码语言:javascript复制libraryDependencies = "net.sourceforge.htmlcleaner" % "htmlcleaner" % "2.4"
但是 build.sbt 中的配置行必须用空行分隔,因此具有一个依赖项的完整的文件如下所示:
代码语言:javascript复制name := "BasicProjectWithScalaTest"
version := "1.0"
scalaVersion := "2.10.0"
libraryDependencies = "org.scalatest" %% "scalatest" % "1.9.1" % "test"
但是为了方便,sbt的依赖项管理可以一次性添加多个依赖项到项目中: 用到了Seq
代码语言:javascript复制libraryDependencies = Seq(
"net.sourceforge.htmlcleaner" % "htmlcleaner" % "2.4",
"org.scalatest" % "scalatest_2.10" % "1.9.1" % "test",
"org.foobar" %% "foobar" % "1.8"
)
否则的话就需要一次一行地添加,并用空行隔开:
代码语言:javascript复制libraryDependencies = "net.sourceforge.htmlcleaner" % "htmlcleaner" % "2.4"
libraryDependencies = "org.scalatest" % "scalatest_2.10" % "1.9.1" % "test"
libraryDependencies = "org.foobar" %% "foobar" % "1.6"
这里我们可以发现有的是用%添加,有的是用%%添加,他们的区别是什么呢?
%%:用于 Scala 库依赖,会自动添加当前项目的 Scala 版本号。例如 :
代码语言:javascript复制"org.scalatest" %% "scalatest" % "3.2.9"
会被解析为 “org.scalatest:scalatest_2.13:3.2.9”,假设当前 Scala 版本为 2.13。
反之%则不会自动添加,%用于 Java 库依赖或需要指定 Scala 版本的情况。 例如:
代码语言:javascript复制"org.apache.commons" % "commons-lang3" % "3.12.0"
其实我们可以从build.sbt文件中看出build.sbt中的每一行都是一个简单的键值对,当然这也不全是,因为sbt中使用scala中的DSL来撰写的,可以简单地推断一下: sbt 的工作原理就是创建一个描述构建的键/值对的大型映射,当它解析此文件时,它会将您定义的对添加到其映射中。
其实两种依赖项的方式都各有各的优点:
托管依赖项只需要通过简单的声明,构建工具能够自动下载并管理项目所需的依赖项,节省了开发人员的时间和精力,同时还可以可以轻松指定所需依赖项的版本,确保项目的稳定性和一致性。托管依赖项的管理集中在构建文件中,因此更容易进行维护和更新。
而非托管依赖项的主要优势就在于灵活,开发者可以灵活选择所需的依赖项版本,甚至可以修改源代码以适应项目的特定需求,开发者还可以针对项目的特定需求进行定制,不受限于公共仓库中已有的依赖项。
在实际项目中,选择合适的依赖项管理方式取决于项目的具体需求和开发团队的偏好。对于常见且稳定的库和框架,使用托管依赖项是最为便捷和推荐的方式;而对于需要定制或特殊处理的依赖项,非托管依赖项则提供了更多的灵活性和控制权。
为了方便理解sbt的依赖管理逻辑,我们得先了解一下sbt的项目结构
sbt的项目结构
一个典型的 sbt 项目结构如下:
代码语言:javascript复制my-project/
├── build.sbt
├── project/
│ ├── build.properties
│ └── plugins.sbt
├── src/
│ ├── main/
│ │ ├── scala/
│ │ └── resources/
│ └── test/
│ ├── scala/
│ └── resources/
└── target/
它的每个部分的作用如下:
代码语言:javascript复制my-project/: 项目的根目录,所有项目文件和子目录都在这里。
build.sbt: 项目的主构建文件,包含了项目的设置(settings)、依赖项(dependencies)和任务(tasks)等。这是定义项目构建过程的关键文件。
project/: 这个目录通常包含了与项目构建相关的文件。
build.properties: 这个文件指定了sbt的版本,用于确定使用哪个版本的sbt来构建项目。
plugins.sbt: 这个文件包含了项目所使用的sbt插件的配置。插件可以添加新的功能和任务到项目的构建过程中。
src/: 这个目录包含了项目的源代码和资源文件。
main/: 主要的源代码目录,包含了项目的主要代码。
scala/: Scala源代码文件存放的目录。
resources/: 主要资源文件(如配置文件、图像等)存放的目录。
test/: 测试代码目录,包含了用于测试项目代码的测试代码和资源文件。
scala/: 测试用的Scala源代码文件存放的目录。
resources/: 测试用的资源文件存放的目录。
target/: 这个目录是sbt生成的,用于存放编译生成的类文件、打包文件以及其他构建过程中生成的临时文件。
回到开头的托管依赖项管理的内容,我们来聊聊在sbt中添加依赖项
sbt中添加依赖项
在 build.sbt
文件中,可以通过 libraryDependencies
来添加依赖。例如:
name := "MyProject" //项目名称
version := "0.1" //项目版本
scalaVersion := "2.13.6" //使用的scala版本
libraryDependencies = "org.apache.commons" % "commons-lang3" % "3.12.0" //项目依赖
记住每两行语句中间都得有空行
同时sbt还支持铜过配置来细分依赖:
代码语言:javascript复制libraryDependencies = Seq(
"org.typelevel" %% "cats-core" % "2.6.1", // 编译时依赖
"org.scalatest" %% "scalatest" % "3.2.9" % Test // 测试时依赖
)
Compile 配置:默认配置,编译时依赖。 Test 配置:测试时依赖,仅在测试时可用。 Provided 配置:编译时依赖,但不包含在打包中,通常用于容器或框架提供的库。 Runtime 配置:运行时依赖,不在编译时使用。
sbt的依赖冲突及解决
在sbt中,依赖冲突通常指的是当项目中存在多个依赖项,而这些依赖项又引入了相同的库但是不同的版本时所产生的问题。sbt提供了一些机制来解决这些依赖冲突,通常可以通过指定依赖的版本来处理。
可以使用 dependencyOverrides 明确指定版本:
代码语言:javascript复制dependencyOverrides = "org.scala-lang" % "scala-library" % "2.13.6"
此外还可以通过检查依赖树来帮助分析和解决依赖冲突问题,但是得先添加插件, 然后再使用 sbt dependencyTree 命令来查看项目的依赖树:
代码语言:javascript复制addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1")
或者说通过 exclude 方法可以排除特定的传递依赖。例如:
代码语言:javascript复制libraryDependencies = "org.example" %% "example-lib" % "1.0" exclude ("org.unwanted", "unwanted-lib")
前面对于sbt的依赖管理我们已经铺垫了很多东西,接下来我们就进入它的底层实现原理:
sbt 依赖管理的底层基本原理
我们首先需要了解的就是sbt的依赖树,我们在解决依赖冲突时提到过
依赖树
在依赖管理中,所有的依赖组成一个树状结构,称为依赖关系树。根节点是当前项目,叶子节点是项目所依赖的库。
例如,如果项目 A 依赖于库 B 和 C,而库 B 又依赖于库 D,则依赖关系树如下所示:
代码语言:javascript复制A
├── B
│ └── D
└── C
sbt 就是通过使用这种树结构来管理依赖,以确保所有的依赖关系都能正确解析并下载,保证了sbt的高效。
sbt依赖项的使用分析过程
sbt使用了 Apache Ivy 来管理项目的依赖项,因此它的依赖项解析过程与 Ivy 类似。你也可以添加自定义仓库,通过查找相关的资料我们可以了解到依赖项的解析过程大概分为以下几个步骤:
1 读取配置文件
sbt项目的依赖项通常在 build.sbt 或者 project/*.sbt 文件中指定。这些文件包含了项目的元数据,如项目名称、版本、依赖项等信息。
2 解析依赖项
当 sbt 启动时,它会读取配置文件,并解析项目的依赖项。依赖项通常以以下形式指定:
代码语言:javascript复制libraryDependencies = groupID % artifactID % revision
这个声明指定了一个依赖项,包括组(group)、模块(artifact)和版本(revision)信息。sbt 将解析这些声明并确定项目所需的所有依赖项。
3 下载依赖项
一旦依赖项被确定,sbt 将会尝试从 Maven 中央仓库或者其他指定的仓库下载这些依赖项。它会根据声明中指定的组、模块和版本信息来确定正确的依赖项,并下载对应的 JAR 文件。
4 依赖项冲突解决
在解析依赖项的过程中,可能会出现依赖项冲突的情况,即同一个模块被多个不同的版本所依赖。sbt使用 Ivy 的冲突解决策略来解决这些冲突,通常是选择最接近项目要求的版本。关于这些冲突问题后面会有提到。
5 更新元数据
一旦依赖项被解析和下载,sbt 将更新项目的元数据,以便后续构建过程可以正确地处理这些依赖项。这些元数据通常存储在项目目录下的 .ivy2 或者 .sbt 目录中。
其实总的来说,sbt 的依赖项的使用的这个过程涉及读取配置文件、解析依赖项声明、下载依赖项、解决依赖项冲突等步骤,而这些步骤的唯一目的以确保项目能够正确地获取和管理其所需的外部依赖项。
综合以上的sbt的依赖管理逻辑,我想把maven和sbt做个比较:
对比其他依赖管理工具
Maven
Maven 是一个流行的构建和依赖管理工具,主要用于 Java 项目。它使用 XML 格式的 pom.xml 文件来配置项目和依赖。
- 配置文件:使用 XML 格式的 pom.xml 文件。
- 优点:标准化强,广泛使用,有丰富的插件。
- 缺点:配置较为冗长,不够灵活,编译速度较慢。Maven 通常执行全量编译,这在大型项目中会导致编译时间较长。此外,Maven 的命令行工具需要每次执行任务时重新启动 JVM,这可能导致较长的启动时间。
Gradle
Gradle 是一个现代化的构建工具,支持增量编译和并行构建,使用 Groovy 或 Kotlin DSL 来配置项目。
- 配置文件:使用 Groovy 或 Kotlin DSL。
- 优点:灵活性高,支持增量编译和并行构建,易于扩展。
- 缺点:学习曲线较陡,复杂的配置可能难以管理。Gradle 的灵活性虽然高,但有时也会带来复杂性,特别是在大型项目中。
Ivy
Ivy 是一个依赖管理工具,通常与 Ant 集成使用。它使用 XML 格式的配置文件。
- 配置文件:使用 XML 格式。
- 优点:灵活性高,可以与 Ant 集成。
- 缺点:不如 Maven 和 Gradle 流行,生态系统较小。Ivy
sbt的优势
1. 增量编译
sbt 的一大特点是支持增量编译,这意味着它只编译自上次编译以来发生变化的代码部分。这大大减少了编译时间,特别是在大型项目中。sbt 还提供持续编译模式,开发者可以启动一个命令让 sbt 监听文件变化并自动重新编译。这种机制极大提高了开发效率。
2. 交互式命令行
sbt 提供一个交互式命令行界面,开发者可以在其中执行各种任务(如编译、测试、打包等)而无需每次重新启动构建工具。这减少了启动时间并提高了开发效率。
3. 动态构建定义
sbt 构建文件使用 Scala 语言,可以通过使用Scala语言的强大特性编写复杂的逻辑和动态配置。相对于maven(maven使用的XML语言并不是专门为maven而设计的),sbt的这种灵活性使得sbt适用于复杂项目和需求频繁变化的项目。
4. 更灵活的版本依赖管理
sbt 的 %%
语法可以自动选择与当前 Scala 版本匹配的依赖版本,简化了跨版本依赖管理。sbt 使用 Apache Ivy 进行依赖解析,支持更复杂的依赖解析策略和灵活的配置。
5. 更好的任务并行化
sbt 能够更好地并行执行任务,利用多核 CPU 提高构建效率。例如,编译和测试任务可以同时进行。
总的来说,sbt 通过其灵活的依赖管理系统和高效的映射构建机制,成为 Scala 和 Java 项目中强大的构建工具。相比于 Maven 和 Gradle,sbt 在增量编译、动态配置和任务并行化方面表现出色。通过sbt 的依赖管理逻辑和解决依赖冲突的方法,开发者可以更高效地管理项目依赖,提升开发效率和项目的可维护性。