如何使用通用仓库来存储契约,而不是将其与生产者一起存放?

另一种存储契约的方式是将其保存在通用位置,而不是与生产者一起存放。这种情况可能与安全问题有关(例如消费者无法克隆生产者的代码)。此外,如果您将契约保存在一个位置,那么作为生产者,您会知道有多少消费者,以及您本地的更改可能会破坏哪些消费者。

仓库结构

假设我们有一个坐标为 com.example:server 的生产者,以及三个消费者:client1client2client3。那么,在存储通用契约的仓库中,您可以有以下设置(您可以在 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) 下,您可以看到三个消费者的期望(client1client2client3)。期望是标准的 Groovy DSL 契约文件,如本文档中描述的那样。这个仓库必须生成一个 JAR 文件,该文件与仓库的内容一一对应。

以下示例展示了 server 文件夹内的 pom.xml 文件

除了 Spring Cloud Contract Maven Plugin,没有其他依赖。这些 pom.xml 文件是消费者端运行 mvn clean install -DskipTests 所必需的,以便在本地安装生产者项目的存根。

根文件夹中的 pom.xml 文件可能如下所示

它使用 assembly plugin 来构建包含所有契约的 JAR 文件。以下示例展示了这种设置

工作流程

该工作流程假设 Spring Cloud Contract 已在消费者端和生产者端都已设置完成。包含契约的通用仓库中也已进行了正确的插件设置。CI 任务被设置为构建包含所有契约的通用仓库制品,并将其上传到 Nexus 或 Artifactory。以下图片展示了此工作流程的 UML

how-to-common-repo

消费者

当消费者希望离线处理契约时,不必克隆生产者的代码,而是克隆通用仓库,进入所需的生产者文件夹(例如 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 下载 groupidcom.example.standaloneartifactidcontracts 的 JAR。然后将其解压到本地临时文件夹中,并选取 com/example/server 中存在的契约作为用于生成测试和存根的契约。由于这种约定,生产者团队在进行一些不兼容更改时,可以知道哪些消费者团队受到了影响。

其余流程保持不变。

如何按主题而不是按生产者定义消息契约?

为了避免在通用仓库中出现消息契约重复(当多个生产者向同一个主题写入消息时),我们可以创建一个结构:REST 契约按生产者存放在文件夹中,而消息契约按主题存放在文件夹中。

对于 Maven 项目

为了能够在生产者端工作,我们应该指定一个包含模式,以便根据我们感兴趣的消息主题过滤通用仓库的 jar 文件。Spring Cloud Contract Maven 插件的 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 项目

  1. 为通用仓库依赖项添加自定义配置,如下所示

    ext {
        contractsGroupId = "com.example"
        contractsArtifactId = "common-repo"
        contractsVersion = "1.2.3"
    }
    
    configurations {
        contracts {
            transitive = false
        }
    }
  2. 将通用仓库依赖项添加到您的 classpath 中,如下所示

    dependencies {
        contracts "${contractsGroupId}:${contractsArtifactId}:${contractsVersion}"
        testCompile "${contractsGroupId}:${contractsArtifactId}:${contractsVersion}"
    }
  3. 将依赖项下载到合适的文件夹中,如下所示

    task getContracts(type: Copy) {
        from configurations.contracts
        into new File(project.buildDir, "downloadedContracts")
    }
  4. 解压 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
    }
  5. 清理未使用的契约,如下所示

    task deleteUnwantedContracts(type: Delete) {
        delete fileTree(dir: "${buildDir}/unpackedContracts",
            include: "**/*",
            excludes: [
                "**/${project.name}/**"",
                "**/${first-topic}/**",
                "**/${second-topic}/**"])
    }
  6. 创建任务依赖,如下所示

    unzipContracts.dependsOn("getContracts")
    deleteUnwantedContracts.dependsOn("unzipContracts")
    build.dependsOn("deleteUnwantedContracts")
  7. 通过设置 contractsDslDir 属性,指定包含契约的目录来配置插件,如下所示

    contracts {
        contractsDslDir = new File("${buildDir}/unpackedContracts")
    }