如何使用合約的通用儲存庫,而不是將合約與生產者一起儲存?
另一種儲存合約的方式,而不是將它們與生產者放在一起,是將它們保存在一個通用位置。這種情況可能與安全性問題有關(消費者無法複製生產者的程式碼)。此外,如果您將合約保存在單一位置,那麼作為生產者,您會知道您有多少消費者,以及您的本地變更可能會破壞哪些消費者。
儲存庫結構
假設我們有一個生產者,其座標為 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
檔案
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>server</artifactId>
<version>0.0.1</version>
<name>Server Stubs</name>
<description>POM used to install locally stubs for consumer side</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.6</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
<spring-cloud-contract.version>4.1.4-SNAPSHOT</spring-cloud-contract.version>
<excludeBuildFolders>true</excludeBuildFolders>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<!-- By default it would search under src/test/resources/ -->
<contractsDirectory>${project.basedir}</contractsDirectory>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
除了 Spring Cloud Contract Maven Plugin 之外,沒有其他相依性。這些 pom.xml
檔案對於消費者端執行 mvn clean install -DskipTests
以在本機安裝生產者專案的 Stub 是必要的。
根資料夾中的 pom.xml
檔案可能如下所示
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.standalone</groupId>
<artifactId>contracts</artifactId>
<version>0.0.1</version>
<name>Contracts</name>
<description>Contains all the Spring Cloud Contracts, well, contracts. JAR used by the
producers to generate tests and stubs
</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>contracts</id>
<phase>prepare-package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<attach>true</attach>
<descriptor>${basedir}/src/assembly/contracts.xml</descriptor>
<!-- If you want an explicit classifier remove the following line -->
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
它使用 assembly plugin 來建置包含所有合約的 JAR。以下範例顯示了這樣的設定
<assembly xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 https://maven.apache.org/xsd/assembly-1.1.3.xsd">
<id>project</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>${project.basedir}</directory>
<outputDirectory>/</outputDirectory>
<useDefaultExcludes>true</useDefaultExcludes>
<excludes>
<exclude>**/${project.build.directory}/**</exclude>
<exclude>mvnw</exclude>
<exclude>mvnw.cmd</exclude>
<exclude>.mvn/**</exclude>
<exclude>src/**</exclude>
</excludes>
</fileSet>
</fileSets>
</assembly>
工作流程
工作流程假設 Spring Cloud Contract 在消費者端和生產者端都已設定。通用合約儲存庫中也存在正確的 plugin 設定。CI 工作設定為通用儲存庫,以建置所有合約的成品並將其上傳到 Nexus 或 Artifactory。下圖顯示了此工作流程的 UML

消費者
當消費者想要離線處理合約時,消費者團隊不是複製生產者程式碼,而是複製通用儲存庫,前往所需的生產者資料夾(例如,com/example/server
),並執行 mvn clean install -DskipTests
以在本機安裝從合約轉換而來的 Stub。
您需要在本機安裝 Maven。 |
生產者
作為生產者,您可以變更 Spring Cloud Contract Verifier 以提供 URL 和包含合約的 JAR 的相依性,如下所示
<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>
透過此設定,groupid
為 com.example.standalone
且 artifactid
為 contracts
的 JAR 會從 link/to/your/nexus/or/artifactory/or/sth
下載。然後,它會在本地臨時資料夾中解壓縮,並且 com/example/server
中存在的合約會被選取為用於產生測試和 Stub 的合約。由於此慣例,當進行一些不相容的變更時,生產者團隊可以知道哪些消費者團隊被破壞。
其餘流程看起來相同。
如何為每個主題而不是每個生產者定義訊息傳遞合約?
為了避免在通用儲存庫中重複訊息傳遞合約,當多個生產者向一個主題寫入訊息時,我們可以建立一個結構,其中 REST 合約放置在每個生產者的資料夾中,而訊息傳遞合約放置在每個主題的資料夾中。
對於 Maven 專案
為了使生產者端能夠工作,我們應該指定一個包含模式,用於按我們感興趣的訊息主題過濾通用儲存庫 jar 檔案。Maven Spring Cloud Contract plugin 的 includedFiles
屬性允許我們這樣做。此外,由於預設路徑是通用儲存庫 groupid/artifactid
,因此需要指定 contractsPath
。以下範例顯示了 Spring Cloud Contract 的 Maven plugin
<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 plugin 中的許多值都可以更改。我們包含它只是為了說明目的,而不是試圖提供一個「典型」範例。 |
對於 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
屬性,設定 plugin 以指定包含合約的目錄,如下所示contracts { contractsDslDir = new File("${buildDir}/unpackedContracts") }