如何使用合約的通用儲存庫,而不是將合約與生產者一起儲存?

另一種儲存合約的方式,而不是將它們與生產者放在一起,是將它們保存在一個通用位置。這種情況可能與安全性問題有關(消費者無法複製生產者的程式碼)。此外,如果您將合約保存在單一位置,那麼作為生產者,您會知道您有多少消費者,以及您的本地變更可能會破壞哪些消費者。

儲存庫結構

假設我們有一個生產者,其座標為 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 檔案

<?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

how-to-common-repo

消費者

當消費者想要離線處理合約時,消費者團隊不是複製生產者程式碼,而是複製通用儲存庫,前往所需的生產者資料夾(例如,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>

透過此設定,groupidcom.example.standaloneartifactidcontracts 的 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 專案

  1. 為通用儲存庫相依性新增自訂組態,如下所示

    ext {
        contractsGroupId = "com.example"
        contractsArtifactId = "common-repo"
        contractsVersion = "1.2.3"
    }
    
    configurations {
        contracts {
            transitive = false
        }
    }
  2. 將通用儲存庫相依性新增到您的類別路徑,如下所示

    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 屬性,設定 plugin 以指定包含合約的目錄,如下所示

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