如何使用通用仓库来存储契约,而不是将它们与生产者一起存储?
存储合同的另一种方式,而不是与生产者一起存储,是将其保存在一个公共位置。这种情况可能与安全问题有关(消费者无法克隆生产者的代码)。此外,如果您将合同保存在一个位置,那么作为生产者,您将知道有多少消费者以及您的本地更改可能会破坏哪些消费者。
仓库结构
假设我们有一个坐标为 com.example:server 的生产者和三个消费者:client1、client2 和 client3。那么,在具有通用合同的仓库中,您可以进行以下设置(您可以在 Spring Cloud Contract 的仓库 samples/standalone/contracts 子文件夹中查看)。以下列表显示了这种结构
├── com
│ └── example
│ └── server
│ ├── client1
│ │ └── expectation.groovy
│ ├── client2
│ │ └── expectation.groovy
│ ├── client3
│ │ └── expectation.groovy
│ └── pom.xml
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
└── assembly
└── contracts.xml
在以斜杠分隔的 groupid/artifact id 文件夹 (com/example/server) 下,您有三个消费者 (client1、client2 和 client3) 的预期。预期是标准的 Groovy DSL 合同文件,如本文档所述。此仓库必须生成一个 JAR 文件,该文件与仓库的内容一对一映射。
以下示例显示了 server 文件夹中的 pom.xml 文件
除了 Spring Cloud Contract Maven 插件外,没有其他依赖项。这些 pom.xml 文件对于消费者端运行 mvn clean install -DskipTests 以本地安装生产者项目的存根是必要的。
根文件夹中的 pom.xml 文件可以如下所示
它使用 assembly 插件构建包含所有合同的 JAR。以下示例显示了这种设置
工作流
工作流假设 Spring Cloud Contract 在消费者和生产者端都已设置。在包含合同的通用仓库中也存在适当的插件设置。CI 作业已设置为通用仓库构建所有合同的构件并将其上传到 Nexus 或 Artifactory。下图显示了此工作流的 UML
消费者
当消费者想要脱机处理合同时,消费者团队无需克隆生产者代码,而是克隆通用仓库,进入所需的生产者文件夹(例如,com/example/server),并运行 mvn clean install -DskipTests 以本地安装从合同转换而来的存根。
| 您需要在本地安装 Maven。 |
生产者
作为生产者,您可以修改 Spring Cloud Contract Verifier 以提供包含合同的 JAR 的 URL 和依赖项,如下所示
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<configuration>
<contractsMode>REMOTE</contractsMode>
<contractsRepositoryUrl>
https://link/to/your/nexus/or/artifactory/or/sth
</contractsRepositoryUrl>
<contractDependency>
<groupId>com.example.standalone</groupId>
<artifactId>contracts</artifactId>
</contractDependency>
</configuration>
</plugin>
通过此设置,将从 link/to/your/nexus/or/artifactory/or/sth 下载 groupid 为 com.example.standalone 且 artifactid 为 contracts 的 JAR。然后将其解压到本地临时文件夹中,并选择 com/example/server 中存在的合同作为用于生成测试和存根的合同。由于此约定,生产者团队可以知道在进行一些不兼容更改时哪些消费者团队被破坏了。
其余流程看起来相同。
如何按主题而非按生产者定义消息合同?
为了避免在通用仓库中重复消息合同,当几个生产者向一个主题写入消息时,我们可以创建一个结构,其中 REST 合同按生产者放在文件夹中,消息合同按主题放在文件夹中。
对于 Maven 项目
为了在生产者端工作,我们应该指定一个包含模式,用于按我们感兴趣的消息主题过滤通用仓库 JAR 文件。Maven Spring Cloud Contract 插件的 includedFiles 属性允许我们这样做。此外,还需要指定 contractsPath,因为默认路径将是通用仓库的 groupid/artifactid。以下示例显示了 Spring Cloud Contract 的 Maven 插件
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<configuration>
<contractsMode>REMOTE</contractsMode>
<contractsRepositoryUrl>https://link/to/your/nexus/or/artifactory/or/sth</contractsRepositoryUrl>
<contractDependency>
<groupId>com.example</groupId>
<artifactId>common-repo-with-contracts</artifactId>
<version>+</version>
</contractDependency>
<contractsPath>/</contractsPath>
<baseClassMappings>
<baseClassMapping>
<contractPackageRegex>.*messaging.*</contractPackageRegex>
<baseClassFQN>com.example.services.MessagingBase</baseClassFQN>
</baseClassMapping>
<baseClassMapping>
<contractPackageRegex>.*rest.*</contractPackageRegex>
<baseClassFQN>com.example.services.TestBase</baseClassFQN>
</baseClassMapping>
</baseClassMappings>
<includedFiles>
<includedFile>**/${project.artifactId}/**</includedFile>
<includedFile>**/${first-topic}/**</includedFile>
<includedFile>**/${second-topic}/**</includedFile>
</includedFiles>
</configuration>
</plugin>
| 前面 Maven 插件中的许多值都可以更改。我们将其包含在内是为了说明目的,而不是试图提供一个“典型”示例。 |
对于 Gradle 项目
要使用 Gradle 项目
-
为通用仓库依赖项添加自定义配置,如下所示
ext { contractsGroupId = "com.example" contractsArtifactId = "common-repo" contractsVersion = "1.2.3" } configurations { contracts { transitive = false } } -
将通用仓库依赖项添加到您的类路径中,如下所示
dependencies { contracts "${contractsGroupId}:${contractsArtifactId}:${contractsVersion}" testCompile "${contractsGroupId}:${contractsArtifactId}:${contractsVersion}" } -
将依赖项下载到适当的文件夹,如下所示
task getContracts(type: Copy) { from configurations.contracts into new File(project.buildDir, "downloadedContracts") } -
解压缩 JAR,如下所示
task unzipContracts(type: Copy) { def zipFile = new File(project.buildDir, "downloadedContracts/${contractsArtifactId}-${contractsVersion}.jar") def outputDir = file("${buildDir}/unpackedContracts") from zipTree(zipFile) into outputDir } -
清理未使用的合同,如下所示
task deleteUnwantedContracts(type: Delete) { delete fileTree(dir: "${buildDir}/unpackedContracts", include: "**/*", excludes: [ "**/${project.name}/**"", "**/${first-topic}/**", "**/${second-topic}/**"]) } -
创建任务依赖项,如下所示
unzipContracts.dependsOn("getContracts") deleteUnwantedContracts.dependsOn("unzipContracts") build.dependsOn("deleteUnwantedContracts") -
通过设置
contractsDslDir属性配置插件,指定包含合同的目录,如下所示contracts { contractsDslDir = new File("${buildDir}/unpackedContracts") }