随着软件项目的增长,通常将大型系统组织成按照特定软件架构连接的组件。通常,在反映组件边界和体系结构的仓库和文件夹结构中组织构成软件的构件(源代码等)也是有意义的。如果使用 Gradle 来构建这样的软件系统,它可以帮助您执行此组织并强制执行组件之间的边界。

您可以将 Gradle 视为软件的建模工具:它允许您在用 Gradle 的 DSL 编写的模型中描述软件的粗粒度结构和体系结构。然后构建工具 Gradle 可以解释这个模型来构建软件。

一个例子

如何构建软件以及如何将其划分为组件取决于您正在构建的内容。没有万能的解决方案。因此,Gradle 不会对您强制执行特定的结构,而是提供工具来为您的个人设置建模。

尽管如此,为了举例说明这些功能,我们探索了一个具有以下架构的示例项目:

software architecture
您可以下载 完整样本 来探索、构建和运行它。

该结构遵循典型的设置,可以在许多常用的软件架构中以类似的形式找到。

  • 在底部,我们定义了我们的领域模型。有两个组件:domain-model 组件包含模型定义(即一组数据类),state 组件负责在应用程序运行时管理模型的可修改状态。

  • 在模型之上,不同特性的业务逻辑相互独立实现,独立于具体的应用技术。在此示例中,我们有两个特征:useradmin

  • 在顶部,我们有用户用来与功能交互的具体应用程序。在示例中,我们构建了一个通过 Web 浏览器支持这两种功能的 Spring Boot 应用程序。以及仅支持user 功能的Android 应用程序。

我们的组件可能依赖于从二进制仓库中检索到的现有组件。例如,Spring Boot 和 Android 框架。

除了生产代码,还有处理构建和交付产品的组件:

  • build-logic 组件包含有关构建软件的配置详细信息。例如,定义要使用的 Java 版本或配置测试框架。它还可能包含 Gradle 的其他构建逻辑(自定义插件自定义任务),这些逻辑未被常用的 Gradle 插件涵盖。

  • platforms 组件是定义要在我们自己的所有组件中使用哪些版本的外部组件的中心位置。通过这种方式,它定义了环境的约束——即the platforms——来构建、测试和运行软件产品。

  • aggregation 组件包含将产品推向生产所需的交付管道的设置,并作为其中的一部分进行自动端到端测试。基本上,这是本地开发机器不需要的构建部分。

我们示例的领域是构建一个工具来通知人们有关 Gradle 构建工具版本 的信息。具体来说,该应用程序列出了带有发行说明链接的 Gradle 版本(user 特性),并为要列出的版本范围提供了一个管理界面(admin 特性)。

在项目结构中反映软件架构

让我们看看如何使用 Gradle 实现示例的架构。我们可以将每个 components 表示为单独的 Gradle build 。我们将详细了解这意味着什么以及组件如何连接。

每个 Gradle 构建都有自己的文件夹。使 Gradle 构建这些文件夹的最低要求是向每个文件夹添加一个空的 settings.gradle(.kts) 文件。让我们对软件中的所有组件执行此操作:

KotlinGroovy
├── android-app
│   └── settings.gradle.kts
├── server-application
│   └── settings.gradle.kts
│
├── admin-feature
│   └── settings.gradle.kts
├── user-feature
│   └── settings.gradle.kts
│
├── state
│   └── settings.gradle.kts
│
├── domain-model
│   └── settings.gradle.kts
│
├── build-logic
│   └── settings.gradle.kts
│
├── platforms
│   └── settings.gradle.kts
│
└── aggregation
    └── settings.gradle.kts

在清单中,每个组件都位于一个单独的文件夹中。在这里,我们将它们排列为根文件夹中的平面列表。例如,此根文件夹可用作 Git 仓库的根目录。

这只是示例的设置。您可以自由选择组件的物理位置。例如,您可以将位于一个“层”中的所有组件分组到一个公共子文件夹中。或者,由于这些都是独立的 Gradle 构建,您可以让每个组件都位于单独的仓库中。由您决定什么最适合您、您正在构建的软件以及使用它的团队。

定义组件的内部结构

在我们进入连接组件的主题之前,让我们首先单独查看它们。

到目前为止,每个组件只是一个带有空 settings.gradle(.kts) 文件的空文件夹,表明这是 Gradle 可以以某种形式使用的组件。要用内容填充组件,您应该在其中至少定义一个项目(在 Gradle 的 DSL 中称为子项目)。

您可以从每个组件都包含一个项目开始,但稍后会引入其他项目以更内部地构建单个组件。在我们的示例中,我们从每个组件中的单个项目开始。

使用设置文件中的 include() 构造添加项目。

KotlinGroovy
domain-model/settings.gradle.kts
include("release") // a project for data classes that represent software releases

包含后,您可以创建一个与项目名称匹配的文件夹,并在其中创建一个 build.gradle(.kts) 文件来配置组件的那部分。您可以在有关 为单个软件组件构建 Gradle 构建 的章节中找到更多信息。

为组件分配类型

让我们放大 domain-model 组件:

KotlinGroovy
└── domain-model              <-- component
    ├── settings.gradle.kts   <-- define inner structure of component and where to locate other components
    └── release               <-- project in component
        └── build.gradle.kts  <-- defines type of the project and its dependencies

最初,release/build.gradle(.kts) 是空的。该项目没有特定类型,也不提供任何有用的内容。如果我们现在向 domain-model/release 文件夹添加更多文件,例如 Java 源文件,Gradle 将不知道如何处理这些文件,只会忽略它们。我们需要为项目分配一个类型,让 Gradle 知道这些文件的用途。

在 Gradle 中,您可以通过 应用插件 为项目分配类型。您可以做的最简单的事情是应用 Gradle 的核心插件之一,如 basejava-library

但是,通常您需要在正在构建的产品的上下文中进行额外的配置。例如,如果你的项目应该是一个“Java 库”,它不仅会应用 java-library 插件,还会配置细节,例如将 Java 版本设置为 11。你可以直接在 release/build.gradle(.kts) 中添加这样的细节,但你不必在其他也包含“Java 库”项目的组件中重复它们。

因此,建议立即开始使用自定义项目类型:

KotlinGroovy
user-feature/data/build.gradle.kts
plugins {
    id("com.example.java-library")
}
KotlinGroovy
domain-model/release/build.gradle.kts
plugins {
    id("com.example.kotlin-library")
}

如上所述,项目类型由 Gradle 中的插件表示。因此,我们将自定义项目类型(例如 com.example.java-librarycom.example.kotlin-library )定义为插件。下一节将解释如何定义此类插件。

将自定义项目类型定义为约定插件

我们从哪里得到 com.example.kotlin-library 插件?这就是我们的 build-logic 组件的用途。

build-logic 组件包含 Gradle 本身理解为构建配置的项目类型。也就是说,满足您特定需求的 Gradle 插件,我们称之为 convention plugins

目前,您可以使用不同的项目类型来定义convention plugins,具体取决于您喜欢的工具和语言。通常,任何 JVM 语言(Java、Groovy、Kotlin、Scala)都可用于将 Gradle 插件编写为实现 Plugin<Project> 接口的类。然而,最紧凑的方法是将它们编写为 Gradle 的 Groovy 或 Kotlin DSL 中的脚本。

您选择哪种方法取决于您。如果您熟悉 Gradle 的 DSL 之一,您可以选择它,因为它是编写 convention plugins 的最紧凑的方式。如果您是 Gradle 的新手(以及 Groovy 和 Kotlin 的新手),您可能更喜欢用 Java 或其他语言(如 Scala)编写convention plugins。然后将与 Gradle 的 Groovy 或 Kotlin DSL 的交互减少到最低限度。

您需要在 build-logic 组件的项目中使用以下项目类型之一(即 Gradle 核心插件):

  • kotlin-dsl – 使用此类型构建逻辑项目(即,应用 kotlin-dsl 插件)允许您将约定插件编写为 src/main/kotlin 中的 .gradle.kts 文件。

  • groovy-gradle-plugin – 使用此类型构建逻辑项目(即,应用 groovy-gradle-plugin 插件)允许您将约定插件编写为 src/main/groovy 中的 .gradle 文件。

  • java-gradle-plugin – 使用此类型构建逻辑项目(即,应用 java-gradle-plugin 插件)允许您将约定插件编写为 .java 类,这些类在 src/main/java 中实现 Plugin<Project> 接口。如果你在上面应用其他JVM语言插件,如groovyscalaorg.jetbrains.kotlin.jvm,你也可以用相应的语言编写插件类。

在我们的示例中,我们选择使用 Gradle 的 DSL 作为约定插件。 build-logic 组件有几个项目,每个项目都通过约定插件定义一个项目类型 - 其中之一:java-librarykotlin-libraryspring-applicationandroid-application。此外,还有一个名为 commons 的项目,用于我们所有项目类型共享的构建配置。

KotlinGroovy
build-logic/spring-boot-application/build.gradle.kts
plugins {
    `kotlin-dsl` (1)
}

dependencies {
    implementation(platform("com.example.platform:plugins-platform")) (2)

    implementation(project(":commons")) (3)

    implementation("org.springframework.boot:org.springframework.boot.gradle.plugin")  (4)
}

查看 spring boot 应用程序的 build-logic 项目的 build.gradle(.kts),我们看到:

1 它是 groovy-gradle-pluginkotlin-dsl 类型,以允许使用相应 DSL 编写的约定插件
2 这取决于我们自己的 plugins-platformplatforms 组件
3 它依赖于 build-logiccommons 项目来访问我们自己的通用约定插件
4 它依赖于 Gradle 插件门户中的 Spring Boot Gradle 插件,因此我们可以将该插件应用于我们的 Spring Boot 项目

现在,我们可以像这样为 Spring 应用程序编写约定插件:

KotlinGroovy
build-logic/spring-boot-application/src/main/kotlin/com.example.spring-boot-application.gradle.kts
plugins {
    id("com.example.commons")
    id("org.springframework.boot")
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
}

我们看到我们应用了我们自己的 com.example.commons 插件,这是另一个约定插件,除其他外,它配置我们目标的 Java 版本并添加对平台的依赖(com.example.platform:product-platform 来自我们的 platforms 组件)。我们应用了 spring boot 插件。此外,我们添加了两个 Spring Boot 项目在我们的上下文中应该始终具有的依赖项。

同样,我们为“Java 库”、“Kotlin 库”和“Android 应用程序”定义了约定插件。这样,我们定义了四种不同的项目类型,我们将它们分配给生产代码组件的项目。

您可以在 共享构建逻辑关联样本 部分找到有关编写约定插件的更多信息。要使用类来实现插件,以及编写更高级的自定义构建逻辑,请参阅 Gradle插件开发 章节。

连接组件

如架构图所示,我们的生产代码组件相互依赖。上面,我们已经看到 platforms 组件被用在了 build-logic 组件中。我们还说过,我们想使用 build-logic 组件,它通过约定插件声明项目类型,将这些类型分配给我们生产代码组件中的项目。

你如何定义这些依赖关系?有两件不同的事情要做:

  1. Make components (builds) known to each other. 这是通过将 includeBuild(…​) 语句添加到 settings.gradle(.kts) 来完成的。这是 not 在组件(项目)之间添加依赖关系。它只是让另一个组件知道一个组件的物理位置。从这个意义上说,它类似于用于发现二进制组件的仓库声明。有关如何包含构建的更多信息,请参阅 定义复合构建的部分

  2. Declare dependencies between (projects of) components. 这与 声明对二进制组件的依赖 类似,通过在 build.gradle(.kts) 文件的 dependencies { } 块中使用 GA(groupartifact )坐标:implementation("com.example.platform:product-platform")。或者,如果包含的组件提供了一个插件,您通过 ID 应用插件类似于从插件门户应用插件的方式:plugins { id("com.example.java-library") }

作为另一个例子,考虑我们的 server-application 组件的设置:

KotlinGroovy
server-application/settings.gradle.kts
// == Define locations for build logic ==
pluginManagement {
    repositories {
        gradlePluginPortal()
    }
    includeBuild("../build-logic")
}

// == Define locations for components ==
dependencyResolutionManagement {
    repositories {
        mavenCentral()
    }
}
includeBuild("../platforms")
includeBuild("../user-feature")
includeBuild("../admin-feature")

// == Define the inner structure of this component ==
rootProject.name = "server-application" // the component name
include("app")

我们看到settings.gradle(.kts)文件只定义了构建逻辑组件、其他生产代码组件和组件内部结构的位置。只有 app 项目中的 build.gradle(.kts) 文件然后通过应用约定插件和利用 dependencies 块来定义实际依赖项。

KotlinGroovy
server-application/app/build.gradle.kts
plugins {
    id("com.example.spring-boot-application")
}

group = "${group}.server-application"

dependencies {
    implementation("com.example.myproduct.user-feature:table")
    implementation("com.example.myproduct.admin-feature:config")

    implementation("org.apache.juneau:juneau-marshall")
}

你的软件型号

就是这样。本章通过示例概述了使用哪些技术将软件项目结构化为 Gradle 的组件。下载 完整样本 以了解更多详细信息。 下一章 涵盖了有关如何使用和发展此类项目结构的更多详细信息。 复合构建章节 为您提供了有关构建组合提供的功能的更多技术背景。

总而言之,如果您遵循本章的建议,您的设置应该清楚地分离以下关注点,以便为您的软件产品提供一个灵活而干净的模型:

  1. Write compact build.gradle(.kts) files. 虽然在传统的 Gradle 构建中,这些文件往往会增长并混合许多不同的问题,但此处呈现的结构使这些文件保持紧凑。在大多数情况下,他们仅通过在 dependencies {} 块中应用单个约定插件和依赖项来声明项目类型。它们可能包含最少的特定于项目的配置,但这些配置应尽可能保持最少。这也使得构建更少地依赖于 Gradle 的 DSL:如果您将构建逻辑放入约定插件中,您可以根据需要直接用 Java 编写它。

  2. Isolate cross-cutting technical concerns into project types. 出于技术动机的构建配置通常会贯穿整个软件架构。项目是“Java 库”还是“Kotlin 库”可能完全独立于它在组件层次结构中的位置。如果您使用约定插件,这允许您在一个中心位置隔离此类项目类型的定义,同时仍然在需要的地方重用它们。 (与所谓的 跨项目配置 相比,这是一个巨大的优势,它在旧的 Gradle 版本中很流行,但绑定到项目的层次结构。)

  3. Declare the origins of components in a central place. 在此结构中,构建可以找到其他组件的位置,无论它们是 位于二进制仓库中 还是 与其他 Gradle 构建一样在本地可用 ,都在 settings.gradle(.kts) 文件中集中定义。这使得更改组件的原点和 从二进制文件移动到组件的源代码版本 变得容易。请注意每个组件的每个 settings.gradle(.kts) 中的 有不同的策略来避免重复此信息

  4. Declare platforms in a central place. 示例中的平台组件是可选的。你可以在没有这些的情况下做一些事情,例如通过直接在你的约定插件中声明 依赖约束 。但是,平台 是一个很好的选择,可以确保在中央位置定义软件运行环境的所有边界。