How Can I use a Common Repository with Contracts Instead of Storing Them with the Producer?

除了让生产者拥有合约外,存储合约的另一种方法是将它们保存在一个公共位置。此情况可能与安全问题相关(消费者无法克隆生产者的代码)。此外,如果您将合约保存在一个地方,那么您作为一名生产者将知道您有多少消费者,以及您可能会通过本地更改破坏哪个消费者。

Repo Structure

Предположим, у нас есть изготовитель с координатами com.example:server и три потребителя: client1, client2 и client3. Тогда в репозитории с общими контрактами у вас может быть следующая настройка (которую вы можете проверить в подпапке Spring Cloud Contract’s repository 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/ArtifactId 文件夹 (com/example/server) 下,您有三个消费者的预期(client1client2client3)。期望是标准 Groovy DSL 合约文件,如本说明文档中所述。此仓库必须生成一个与仓库内容一一对应的 JAR 文件。

以下示例显示了 server 文件夹中一个 pom.xml 文件:

link:{samples_path}/standalone/contracts/com/example/server/pom.xml[role=include]

除了 Spring Cloud Contract Maven 插件外,没有其他依赖项。这些 pom.xml 文件对于消费者方本地安装生产者项目的存根至关重要,方法是运行 mvn clean install -DskipTests

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

link:{samples_path}/standalone/contracts/pom.xml[role=include]

它使用 assembly 插件构建包含所有合约的 JAR。以下示例显示了这种设置:

link:{samples_path}/standalone/contracts/src/assembly/contracts.xml[role=include]

Workflow

工作流假设生产者和消费者方都设置了 Spring Cloud Contract。在具有合约的公共仓库中还有正确的插件设置。CI 作业设置为公共仓库以构建所有合约的工件并将它上传到 Nexus 或 Artifactory。以下图像显示了此工作流的 UML:

"API Consumer"->"Common repo": create a folder \nfor producer [API Producer]
"API Consumer"->"Common repo": under [API Producer] create a folder \nfor consumer \n[API Consumer]
"API Consumer"->"Common repo": define contracts under \n[API Consumer] folder
"API Consumer"->"Common repo": install stubs of [API Producer]\nin local storage
"Common repo"->"Common Repo\nSCC Plugin": install stubs \nin local storage. \nDon't generate tests.
"Common Repo\nSCC Plugin"->"Local storage": install stubs
"Local storage"->"Common Repo\nSCC Plugin": stubs installed
"API Consumer"->"API Consumer": write a SCC Stub Runner \nbased contract test
"API Consumer"->"API Consumer\nSCC Stub Runner": fetch the stubs\n of [API Producer] \nfrom local storage
"API Consumer\nSCC Stub Runner"->"Local storage": test asks for [API Producer] stubs
"Local storage"->"API Consumer\nSCC Stub Runner": [API Producer] stubs found
"API Consumer\nSCC Stub Runner"->"API Consumer\nSCC Stub Runner": run in memory\n HTTP server stubs
"API Consumer\nSCC Stub Runner"->"API Consumer": HTTP server stubs running,\n ready for tests
"API Consumer"->"API Consumer\nSCC Stub Runner": send a request \nto the HTTP server stub
"API Consumer\nSCC Stub Runner"->"API Consumer": communication is correct. \nTests are passing
"API Consumer"->"Common repo": file pull request \nwith contracts
"API Producer"->"Common repo": take over \nthe pull request
"API Producer"->"Common repo": install the JAR \nwith all contracts\n in local storage
"Common repo"->"Local storage": install the JAR
"Local storage"->"Common repo": contracts JAR installed
"API Producer"->"Producer Build": run the build \nand fetch contracts from \nlocal storage
"Producer Build"->"Producer\nSCC Plugin": generate \ntests, stubs and stubs \nartifact (e.g. stubs-jar)
"Producer\nSCC Plugin"->"Local storage": fetch contract definitions for [API Prodcer]
"Local storage"->"Producer\nSCC Plugin": contracts fetched
"Producer\nSCC Plugin"->"Producer Build": tests and stubs created
"Producer Build"->"Nexus / Artifactory": upload contracts \nand stubs and the project arifact
"Producer Build"->"API Producer": Build successful
"API Producer"->"Common repo": merge the pull request
"Common repo"->"Nexus / Artifactory": upload the fresh JAR \nwith contract definitions
"API Producer"->"API Producer": start fetching contract definitions \nfrom Nexus / Artifactory

Consumer

当消费者希望脱机处理合约时,消费者团队会克隆公共仓库而不是克隆生产者代码,转至必需的生产者文件夹(例如,com/example/server),然后运行 mvn clean install -DskipTests 以本地安装由合约转换的存根。

您需要具备 Maven installed locally

Producer

作为生产者,您可以更改 Spring Cloud Contract Verifier 以提供包含合约的 JAR 的 URL 和依赖项,如下所示:

link:{tools_path}/spring-cloud-contract-maven-plugin/src/test/projects/basic-remote-contracts/pom-with-repo.xml[role=include]

通过此设置,从 https://link/to/your/nexus/or/artifactory/or/sth 下载 GroupIdcom.example.standaloneArtifactIdcontracts 的 JAR。然后它会在本地临时文件夹中解压,并选择 com/example/server 中存在的合约作为用于生成测试和存根的合约。由于此惯例,当做出一些不兼容的更改时,生产者团队可以知道哪些消费者团队会被破坏。

其余流程看起来相同。

How Can I Define Messaging Contracts per Topic Rather than per Producer?

为了避免在多个生产者向一个主题编写消息时在公共仓库中复制消息合约,我们可以创建一种结构,其中 REST 合约被放置在每个生产者的文件夹中,而消息合约被放置在每个主题的文件夹中。

For Maven Projects

为了继续在生产者方工作,我们应该指定一个包含模式来通过我们感兴趣的消息主题过滤公共仓库 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 插件中的许多值。出于说明目的,我们包含了它,而不是试图提供一个"`typical`"示例。

For Gradle Projects

使用 Gradle 项目:

  1. 如下添加通用存储库依赖项的自定义配置:[source, groovy]

ext {
    contractsGroupId = "com.example"
    contractsArtifactId = "common-repo"
    contractsVersion = "1.2.3"
}

configurations {
    contracts {
        transitive = false
    }
}
  1. 如下将通用存储库依赖项添加到你的类路径:[source, groovy]

dependencies {
    contracts "${contractsGroupId}:${contractsArtifactId}:${contractsVersion}"
    testCompile "${contractsGroupId}:${contractsArtifactId}:${contractsVersion}"
}
  1. 如下下载依赖项到相应文件夹:[source, groovy]

task getContracts(type: Copy) {
    from configurations.contracts
    into new File(project.buildDir, "downloadedContracts")
}
  1. 如下解压 JAR:[source, groovy]

task unzipContracts(type: Copy) {
    def zipFile = new File(project.buildDir, "downloadedContracts/${contractsArtifactId}-${contractsVersion}.jar")
    def outputDir = file("${buildDir}/unpackedContracts")

    from zipTree(zipFile)
    into outputDir
}
  1. 如下清理未使用的约定:[source, groovy]

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

unzipContracts.dependsOn("getContracts")
deleteUnwantedContracts.dependsOn("unzipContracts")
build.dependsOn("deleteUnwantedContracts")
  1. 通过指定包含约定的目录来配置插件,如下设置 contractsDslDir 属性:[source, groovy]

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