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

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

Another way of storing contracts, rather than having them with the producer, is to keep them in a common place. This situation can be related to security issues (where the consumers cannot clone the producer’s code). Also, if you keep contracts in a single place, then you, as a producer, know how many consumers you have and which consumer you may break with your local changes.

Repo Structure

Предположим, у нас есть изготовитель с координатами com.example:server и три потребителя: client1, client2 и client3. Тогда в репозитории с общими контрактами у вас может быть следующая настройка (которую вы можете проверить в подпапке Spring Cloud Contract’s repository samples/standalone/contracts). Следующий список представляет такую структуру:

Assume that we have a producer with coordinates of com.example:server and three consumers: client1, client2, and client3. Then, in the repository with common contracts, you could have the following setup (which you can check out in the Spring Cloud Contract’s repository samples/standalone/contracts subfolder). The following listing shows such a structure:

├── 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 文件。

Under the slash-delimited groupid/artifact id folder (com/example/server), you have expectations of the three consumers (client1, client2, and client3). Expectations are the standard Groovy DSL contract files, as described throughout this documentation. This repository has to produce a JAR file that maps one-to-one to the contents of the repository.

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

The following example shows a pom.xml file inside the server folder:

Unresolved directive in how-to-common-repo-with-contracts.adoc - include::{samples_path}/standalone/contracts/com/example/server/pom.xml[]

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

There are no dependencies other than the Spring Cloud Contract Maven Plugin. Those pom.xml files are necessary for the consumer side to run mvn clean install -DskipTests to locally install the stubs of the producer project.

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

The pom.xml file in the root folder can look like the following:

Unresolved directive in how-to-common-repo-with-contracts.adoc - include::{samples_path}/standalone/contracts/pom.xml[]

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

It uses the assembly plugin to build the JAR with all the contracts. The following example shows such a setup:

Unresolved directive in how-to-common-repo-with-contracts.adoc - include::{samples_path}/standalone/contracts/src/assembly/contracts.xml[]

Workflow

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

The workflow assumes that Spring Cloud Contract is set up both on the consumer and on the producer side. There is also the proper plugin setup in the common repository with contracts. The CI jobs are set for a common repository to build an artifact of all contracts and upload it to Nexus or Artifactory. The following image shows the UML for this workflow:

"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 以本地安装由合约转换的存根。

When the consumer wants to work on the contracts offline, instead of cloning the producer code, the consumer team clones the common repository, goes to the required producer’s folder (for example, com/example/server) and runs mvn clean install -DskipTests to locally install the stubs converted from the contracts.

您需要具备 Maven installed locally

You need to have Maven installed locally.

Producer

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

As a producer, you can alter the Spring Cloud Contract Verifier to provide the URL and the dependency of the JAR that contains the contracts, as follows:

Unresolved directive in how-to-common-repo-with-contracts.adoc - include::{tools_path}/spring-cloud-contract-maven-plugin/src/test/projects/basic-remote-contracts/pom-with-repo.xml[]

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

With this setup, the JAR with a groupid of com.example.standalone and an artifactid of contracts is downloaded from https://link/to/your/nexus/or/artifactory/or/sth. It is then unpacked in a local temporary folder, and the contracts present in com/example/server are picked as the ones used to generate the tests and the stubs. Due to this convention, the producer team can know which consumer teams are broken when some incompatible changes are made.

其余流程看起来相同。

The rest of the flow looks the same.

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

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

To avoid messaging contracts duplication in the common repository, when a few producers write messages to one topic, we could create a structure in which the REST contracts are placed in a folder per producer and messaging contracts are placed in the folder per topic.

For Maven Projects

为了继续在生产者方工作,我们应该指定一个包含模式来通过我们感兴趣的消息主题过滤公共仓库 Jar 文件。Maven Spring Cloud Contract 插件的 includedFiles 属性支持我们这样做。另外,需要指定 contractsPath,因为默认路径会是公共仓库的 GroupId/ArtifactId。以下示例显示了 Spring Cloud Contract 的 Maven 插件:

To make it possible to work on the producer side, we should specify an inclusion pattern for filtering common repository jar files by messaging topics we are interested in. The includedFiles property of the Maven Spring Cloud Contract plugin lets us do so. Also, contractsPath need to be specified, since the default path would be the common repository groupid/artifactid. The following example shows a Maven plugin for Spring Cloud Contract:

<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`"示例。

Many of the values in the preceding Maven plugin can be changed. We included it for illustration purposes rather than trying to provide a “typical” example.

For Gradle Projects

使用 Gradle 项目:

To work with a Gradle project:

  1. Add a custom configuration for the common repository dependency, as follows:[source, groovy]

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

configurations {
    contracts {
        transitive = false
    }
}
  1. Add the common repository dependency to your classpath, as follows:[source, groovy]

dependencies {
    contracts "${contractsGroupId}:${contractsArtifactId}:${contractsVersion}"
    testCompile "${contractsGroupId}:${contractsArtifactId}:${contractsVersion}"
}
  1. Download the dependency to an appropriate folder, as follows:[source, groovy]

task getContracts(type: Copy) {
    from configurations.contracts
    into new File(project.buildDir, "downloadedContracts")
}
  1. Unzip the JAR, as follows:[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. Cleanup unused contracts, as follows:[source, groovy]

task deleteUnwantedContracts(type: Delete) {
    delete fileTree(dir: "${buildDir}/unpackedContracts",
        include: "**/*",
        excludes: [
            "**/${project.name}/**"",
            "**/${first-topic}/**",
            "**/${second-topic}/**"])
}
  1. Create task dependencies, as follows:[source, groovy]

unzipContracts.dependsOn("getContracts")
deleteUnwantedContracts.dependsOn("unzipContracts")
build.dependsOn("deleteUnwantedContracts")
  1. Configure the plugin by specifying the directory that contains the contracts, by setting the contractsDslDir property, as follows:[source, groovy]

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