【Gradle多模块系列2】在子项目之间声明依赖关系和共享构建逻辑示例详解

一、在子项目之间声明依赖关系

1. 项目依赖关系

如果一个项目在编译classpath中需要另一个项目生成的jar文件怎么办

如果它还需要另一个项目的传递依赖项怎么办?

很明显,这是Java多项目构建中非常常见的用例。如前所述,在项目依赖关系中,Gradle提供了项目依赖关系。

例1. 项目依赖关系

Project layout
.
├── buildSrc
│   ...
├── api
│   ├── src
│   │   └──...
│   └── build.gradle
├── services
│   └── person-service
│       ├── src
│       │   └──...
│       └── build.gradle
├── shared
│   ├── src
│   │   └──...
│   └── build.gradle
└── settings.gradle

我们有sharedapiperson-service三个项目。person-service项目依赖于其他两个项目。api项目依赖于shared项目。它没有构建脚本,并且没有被其他构建脚本注入任何内容。我们使用冒号分隔符来定义项目路径。有关定义项目路径的更多信息,请参阅Settings.include(java.lang.String[])的DSL文档。

settings.gradle文件的内容如下:

rootProject.name = 'dependencies-java'
include 'api', 'shared', 'services:person-service'

buildSrc/src/main/groovy/myproject.java-conventions.gradle文件中的内容如下:

plugins {
    id 'java'
}

group = 'com.example'
version = '1.0'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation "junit:junit:4.13"
}

api/build.gradle文件中的内容如下:

plugins {
    id 'myproject.java-conventions'
}

dependencies {
    implementation project(':shared')
}

shared/build.gradle文件中的内容如下:

plugins {
    id 'myproject.java-conventions'
}

services/person-service/build.gradle文件中的内容如下:

plugins {
    id 'myproject.java-conventions'
}

dependencies {
    implementation project(':shared')
    implementation project(':api')
}

共享的构建逻辑被提取到一个约定插件中,在子项目的构建脚本中应用该插件并定义了项目依赖关系。

项目依赖关系是执行依赖关系的一种特殊形式。

它会先构建其他项目,然后将其他项目的包含类的jar文件添加到类路径中。它还将其他项目的依赖项添加到类路径中。您可以触发gradle :api:compile命令。首先构建shared项目,然后构建api项目。项目依赖关系使得部分多项目构建成为可能。

2. 依赖于另一个项目产生的构件

项目依赖关系模型化了模块之间的依赖关系。

实际上,您是在说您依赖于另一个项目的主要输出。

在基于Java的项目中,通常是一个JAR文件。

有时,您可能希望依赖于另一个任务生成的输出。反过来,您将希望确保先执行该任务以生成该输出。从一个项目到另一个项目声明任务依赖关系是一种不好的建模方式,并且引入了不必要的耦合。建议建模此类依赖关系的方法是生成输出,将其标记为“输出”构件或将其添加到主源集的输出中,然后您可以在使用该构件的消费者项目中进行依赖。

假设您正在一个包含两个子项目producerconsumer的多项目构建中工作。子项目producer定义了一个名为buildInfo的任务,用于生成包含构建信息(例如项目版本)的属性文件。然后,您可以将任务提供程序映射到其输出文件,并且Gradle将自动建立任务依赖关系。

2.1 例2. 生成包含构建信息的属性文件的任务

build.gradle

plugins {
    id 'java-library'
}

version = '1.0'

def buildInfo = tasks.register("buildInfo", BuildInfo) {
    version = project.version
    outputFile = layout.buildDirectory.file('generated-resources/build-info.properties')
}

sourceSets {
    main {
        output.dir(buildInfo.map { it.outputFile.asFile.get().parentFile })
    }
}

consumer项目应该能够在运行时读取属性文件。通过在消费者项目中声明对生产者项目的项目依赖关系,可以确保在之前创建属性并将其提供给运行时类路径。

2.2 例3. 声明对生成属性文件的项目的项目依赖关系

build.gradle

dependencies {
    runtimeOnly project(':producer')
}

在上面的例子中,消费者项目现在声明对生产者项目输出的依赖关系。

依赖于另一个项目的主要输出构件只是一个例子。

Gradle拥有最强大的依赖管理引擎之一,允许您在项目之间共享任意构件,并让Gradle按需构建它们。有关更多详细信息,请参阅关于在项目之间共享输出的部分。

二、在子项目之间共享构建逻辑

1. 约定插件

通常,在多项目构建中,子项目之间共享一些共同的特征。例如,几个子项目可能包含特定编程语言的代码,而另一个子项目可能专门用于文档。代码质量规则适用于所有代码子项目,但不适用于文档子项目。同时,具有共同特征的子项目可能具有不同的目的 - 它们可能产生不同的构件类型,进一步区分它们,例如:

  • 公共库 - 发布到某个仓库的库
  • 内部库 - 项目内其他子项目依赖的库
  • 命令行应用程序 - 具有特定打包要求的应用程序
  • Web服务 - 具有与上述不同的特定打包要求
  • 等等

其他一些代码子项目可能专门用于测试等。

上述特征确定了子项目的类型,或者换句话说,子项目的类型告诉我们该项目具有哪些特征。

Gradle组织构建逻辑的推荐方式是使用其插件系统

插件应定义子项目的类型。实际上,Gradle的核心插件也是以相同方式进行建模的 - 例如,Java插件配置了通用的Java项目,而Java Library插件在内部应用Java插件,并在此基础上配置了特定于Java库的方面。类似地,Application插件应用并配置Java插件和Distribution插件。

您可以通过应用和配置核心和外部插件来组合自定义构建逻辑,并创建自定义插件来定义新的项目类型并配置特定于项目或组织的约定。对于本节开头的每个示例特征,我们可以编写一个插件,将通用逻辑封装在给定类型的子项目中。

我们建议将约定插件的源代码和测试放在项目根目录的特殊buildSrc目录中。有关buildSrc的更多信息,请参阅使用buildSrc组织构建逻辑。

请查看示例,演示了如何使用约定插件对构建逻辑进行建模的多项目构建。

另一个更复杂、实际的多项目构建的示例,它使用约定插件组合构建逻辑,是Gradle Build Tool自身的构建。

2. 跨项目配置

另一种不鼓励的共享构建逻辑的方法是通过subprojects {}allprojects {} DSL结构进行跨项目配置。通过跨配置,构建逻辑可以注入到子项目中,当查看子项目的构建脚本时,这并不明显,这使得更难理解特定子项目的逻辑。从长远来看,跨配置通常会越来越复杂,有越来越多的条件逻辑和更高的维护负担。跨配置还可能引入项目之间的配置时间耦合,这可能会阻止像按需配置这样的优化正常工作。

两种最常见的跨配置用法,可以使用约定插件更好地进行建模

  • 应用插件或其他配置到特定类型的子项目。通常,跨配置部分将执行以下操作:如果子项目是类型X,则配置Y。这相当于直接向子项目应用X-conventions插件。
  • 从特定类型的子项目中提取信息。此用例可以使用出站配置变体进行建模。

3. 在子项目之间共享构建逻辑示例

3.1 使用案例

以一个具有三个子项目的项目为例,其中两个子项目生成了作为内部共享库使用的两个公共Java库。这是项目结构:

Project structure
├── internal-module
│   └── build.gradle
├── library-a
│   ├── build.gradle
│   └── README.md
├── library-b
│   ├── build.gradle
│   └── README.md
└── settings.gradle

假设我们的所有项目都将是Java项目。在这种情况下,我们希望对它们都应用一组通用规则,例如源代码目录布局、编译器标志、代码风格约定、代码质量检查等等。

三个项目中有两个不仅仅是Java项目 - 它们是我们可能希望发布到外部存储库的库。发布配置,例如库的通用组名称以及存储库坐标,可能是两个库需要共享的横切关注点。对于此示例,假设我们还想强制执行我们的库公开了一些具有共同结构的文档。

3.2 组织构建逻辑

根据上面的使用案例,我们确定了两种类型的项目 - 通用的Java项目和公共库。我们可以通过分层两个单独的插件来模拟此使用案例,每个插件定义应用它们的项目类型:

  • myproject.java-conventions - 配置组织中任何Java项目的通用约定。它应用核心java和checkstyle插件以及外部的com.github.spotbugs插件,并配置了常见的编译选项和代码质量检查。
  • myproject.library-conventions - 添加发布配置以发布到组织的存储库,并检查README中是否有必需的内容。它应用java-library和maven-publish插件以及myproject.java-conventions插件。

内部库子项目应用myproject.java-conventions插件:

internal-module/build.gradle
plugins {
    id 'myproject.java-conventions'
}

dependencies {
    // internal module dependencies
}

两个公共库子项目应用myproject.library-conventions插件:

library-a/build.gradle
plugins {
    id 'myproject.library-conventions'
}

dependencies {
    implementation project(':internal-module')
}

library-b/build.gradle
plugins {
    id 'myproject.library-conventions'
}

dependencies {
    implementation project(':internal-module')
}

注意如何将约定插件应用于子项目有效地声明了其类型。通过应用myproject.java-conventions插件,我们声明:这是一个“Java”项目。通过应用myproject.library-conventions插件,我们声明:这是一个“Library”项目。

此示例中的所有插件都包含使用TestKit进行功能测试以验证其行为。

此示例没有任何项目源代码,只是布置了一个假设的项目结构,其中两个库子项目依赖于一个共享的内部子项目。

3.3 编译约定插件

在此示例中,约定插件被实现为预编译的脚本插件 - 这是最简单的方法,因为您可以直接使用Gradle的DSL之一来实现构建逻辑,就像插件是常规的构建脚本一样。

为了发现预编译的脚本插件,buildSrc项目需要在其build.gradle文件中应用groovy-gradle-plugin插件:

buildSrc/build.gradle
plugins {
    id 'groovy-gradle-plugin'
}

3.4 注意事项

3.4.1 在预编译的脚本插件中应用外部插件

myproject.java-conventions插件使用SpotBugs插件执行静态代码分析。

SpotBugs是一个外部插件 - 在预编译的脚本插件中应用外部插件之前,需要将它们作为实现依赖项添加:

buildSrc/build.gradle
repositories {
    gradlePluginPortal() // so that external plugins can be resolved in dependencies section
}

dependencies {
    implementation 'com.github.spotbugs.snom:spotbugs-gradle-plugin:5.0.12'
    testImplementation platform("org.spockframework:spock-bom:2.2-groovy-3.0")
    testImplementation 'org.spockframework:spock-core'
}

tasks.named('test', Test) {
    useJUnitPlatform()
}

插件的依赖项坐标(GAV)可能与插件id不同。

Gradle插件门户网站(gradlePluginPortal())被添加为插件依赖项的仓库

插件版本从依赖项版本确定。

添加依赖项后,可以通过id在预编译的脚本插件中应用外部插件

buildSrc/src/main/groovy/myproject.java-conventions.gradle
plugins {
    id 'java'
    id 'checkstyle'

    // NOTE: external plugin version is specified in implementation dependency artifact of the project's build file
    id 'com.github.spotbugs'
}
3.4.2 应用其他预编译的脚本插件

预编译的脚本插件可以应用其他预编译的脚本插件。

myproject.library-conventions插件应用了myproject.java-conventions插件:

buildSrc/src/main/groovy/myproject.library-conventions.gradle
plugins {
    id 'java-library'
    id 'maven-publish'
    id 'myproject.java-conventions'
}
3.4.3 使用主源集中的类

预编译的脚本插件可以使用插件项目的主源集中定义的类。

在此示例中,myproject.library-conventions插件使用了buildSrc/src/main/java中的自定义任务类来配置库README检查:

buildSrc/src/main/groovy/myproject.library-conventions.gradle
def readmeCheck = tasks.register('readmeCheck', com.example.ReadmeVerificationTask) {
    // Expect the README in the project directory
    readme = layout.projectDirectory.file("README.md")
    // README must contain a Service API header
    readmePatterns = ['^## API$', '^## Changelog$']
}

参考链接

小军李:【Gradle jvm插件系列1】 Java Application插件权威详解

小军李:【Gradle jvm插件系列2】 Java Library插件用法示例权威详解

小军李:【Gradle jvm插件系列3】 Java platform平台插件权威详解

小军李:【Gradle jvm插件系列4】 scala插件权威详解

小军李:【gradle多模块系列1】多项目构建和子项目的添加管理

小军李:【Gradle多模块系列2】在子项目之间声明依赖关系和共享构建逻辑示例详解

小军李:【Gradle 多模块系列3】如何开发自定义Gradle插件

小军李:【Gradle多模块系列4】JVM项目的依赖管理

https://docs.gradle.org/8.2.1/userguide/declaring_dependencies_between_subprojects.html

https://docs.gradle.org/8.2.1/userguide/sharing_build_logic_between_subprojects.html

https://docs.gradle.org/8.2.1/samples/sample_convention_plugins.html

  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BigDataMLApplication

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值