消費者驅動契約

本指南將引導您建立 Spring REST 應用程式及其契約存根,並在另一個 Spring 應用程式中消費該契約。Spring Cloud Contract 專案

您將構建什麼

您將設定兩個微服務,一個提供其契約,另一個消費該契約,以確保與契約提供者服務的整合符合規範。如果將來生產者服務的契約發生變化,那麼消費者服務的測試將失敗,從而捕獲潛在的不相容性。

您需要什麼

如何完成本指南

與大多數 Spring 入門指南一樣,您可以從頭開始並完成每個步驟,也可以跳過您已熟悉的基本設定步驟。無論哪種方式,您最終都會得到可工作的程式碼。

從頭開始,請繼續閱讀使用 Gradle 構建

跳過基礎知識,請執行以下操作

完成時,您可以將結果與gs-contract-rest/complete中的程式碼進行核對。

使用 Gradle 構建

使用 Gradle 構建

首先,您需要設定一個基本的構建指令碼。在使用 Spring 構建應用程式時,您可以使用任何您喜歡的構建系統,但此處包含了使用GradleMaven所需的程式碼。如果您對兩者都不熟悉,請參考使用 Gradle 構建 Java 專案使用 Maven 構建 Java 專案

建立目錄結構

在您選擇的專案目錄中,建立以下子目錄結構;例如,在 *nix 系統上使用mkdir -p src/main/java/hello

└── src
    └── main
        └── java
            └── hello

建立 Gradle 構建檔案

contract-rest-service/build.gradle

buildscript {
  ext {
    springBootVersion = '3.3.0'
    verifierVersion = '4.0.4'
  }
  repositories { mavenCentral() }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${verifierVersion}"
  }
}

apply plugin: 'groovy'
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'spring-cloud-contract'


bootJar {
  archiveFileName = 'contract-rest-service'
  version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 17
targetCompatibility = 17

repositories { mavenCentral() }

dependencies {
  implementation('org.springframework.boot:spring-boot-starter-web')
  testImplementation('org.springframework.boot:spring-boot-starter-test')
  testImplementation('org.springframework.cloud:spring-cloud-starter-contract-verifier')
}

dependencyManagement {
  imports {
    mavenBom "org.springframework.cloud:spring-cloud-dependencies:2022.0.4"
  }
}

contracts {
  // To have the same contract folder as Maven. In Gradle would default to
  // src/contractTest/resources/contracts
  contractsDslDir = file("src/test/resources/contracts")
  packageWithBaseClasses = 'hello'
  baseClassMappings {
    baseClassMapping(".*hello.*", "hello.BaseClass")
  }
}

contractTest {
  useJUnitPlatform()
}

contract-rest-client/build.gradle

buildscript {
  ext { springBootVersion = '3.3.0' }
  repositories { mavenCentral() }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

bootJar {
  archiveFileName = 'contract-rest-client'
  version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 17
targetCompatibility = 17

repositories { mavenCentral() }

dependencies {
  implementation('org.springframework.boot:spring-boot-starter-web')
  testImplementation('org.springframework.boot:spring-boot-starter-test')
  testImplementation('org.springframework.cloud:spring-cloud-starter-contract-stub-runner')
}

dependencyManagement {
  imports {
    mavenBom "org.springframework.cloud:spring-cloud-dependencies:2022.0.4"
  }
}

eclipse {
  classpath {
    containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
    containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17'
  }
}

test {
  useJUnitPlatform()
}

Spring Boot gradle 外掛提供了許多便捷的功能

  • 它收集類路徑上的所有 jar 並構建一個可執行的“über-jar”,這使得執行和傳輸您的服務更加方便。

  • 它搜尋public static void main()方法以將其標記為可執行類。

  • 它提供了一個內建的依賴解析器,將版本號設定為匹配Spring Boot 依賴項。您可以覆蓋任何您想要的版本,但它會預設為 Boot 選擇的一組版本。

使用 Maven 構建

使用 Maven 構建

首先,您需要設定一個基本的構建指令碼。在使用 Spring 構建應用程式時,您可以使用任何您喜歡的構建系統,但此處包含了使用Maven所需的程式碼。如果您不熟悉 Maven,請參考使用 Maven 構建 Java 專案

建立目錄結構

在您選擇的專案目錄中,建立以下子目錄結構;例如,在 *nix 系統上使用mkdir -p src/main/java/hello

└── src
    └── main
        └── java
            └── hello

為了讓您快速入門,這裡是伺服器和客戶端應用程式的完整配置

contract-rest-service/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="https://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>contract-rest-service</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.3.0</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>17</java.version>
		<spring-cloud.version>2022.0.4</spring-cloud.version>
		<spring-cloud-contract.version>4.0.4</spring-cloud-contract.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>

            <!--
			<plugin>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-contract-maven-plugin</artifactId>
				<version>${spring-cloud-contract.version}</version>
				<extensions>true</extensions>
				<configuration>
					<baseClassForTests>hello.BaseClass</baseClassForTests>
				</configuration>
			</plugin>
            -->

		</plugins>
	</build>


</project>

contract-rest-client/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="https://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>contract-rest-client</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.3.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>17</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
			<scope>test</scope>
		</dependency>

	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>2022.0.4</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>


</project>

Spring Boot Maven 外掛提供了許多便捷的功能

  • 它收集類路徑上的所有 jar 並構建一個可執行的“über-jar”,這使得執行和傳輸您的服務更加方便。

  • 它搜尋public static void main()方法以將其標記為可執行類。

  • 它提供了一個內建的依賴解析器,將版本號設定為匹配Spring Boot 依賴項。您可以覆蓋任何您想要的版本,但它會預設為 Boot 選擇的一組版本。

使用您的 IDE 構建

使用您的 IDE 構建

建立契約生產者服務

您首先需要建立生成契約的服務。這是一個常規的 Spring Boot 應用程式,提供一個非常簡單的 REST 服務。該 REST 服務只返回一個 JSON 格式的Person物件。

contract-rest-service/src/main/java/hello/ContractRestServiceApplication.java

package hello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ContractRestServiceApplication {

  public static void main(String[] args) {
    SpringApplication.run(ContractRestServiceApplication.class, args);
  }
}

建立 REST 服務的契約

REST 服務的契約可以定義為.groovy指令碼。該契約指定如果對 URL /person/1發出GET請求,則響應體中將返回表示Person實體的示例資料id=1name=foosurname=bee,內容型別為application/json

contract-rest-service/src/test/resources/contracts/hello/find_person_by_id.groovy

import org.springframework.cloud.contract.spec.Contract

Contract.make {
  description "should return person by id=1"

  request {
    url "/person/1"
    method GET()
  }

  response {
    status OK()
    headers {
      contentType applicationJson()
    }
    body (
      id: 1,
      name: "foo",
      surname: "bee"
    )
  }
}

test階段,將為 groovy 檔案中指定的契約自動建立測試類。這分別透過 Gradle 中的構建外掛org.springframework.cloud:spring-cloud-contract-gradle-plugin或 Maven 中的org.springframework.cloud:spring-cloud-contract-maven-plugin完成。自動生成的測試 Java 類將擴充套件hello.BaseClass

要在 Maven 中包含該外掛,您需要新增以下內容

	<plugin>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-contract-maven-plugin</artifactId>
		<version>${spring-cloud-contract.version}</version>
		<extensions>true</extensions>
		<configuration>
			<baseClassForTests>hello.BaseClass</baseClassForTests>
		</configuration>
	</plugin>

為了執行測試,您還需要在測試範圍內包含org.springframework.cloud:spring-cloud-starter-contract-verifier依賴項。

最後,建立測試的基類

contract-rest-service/src/test/java/hello/BaseClass.java

package hello;

import org.junit.jupiter.api.BeforeEach;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;

import io.restassured.module.mockmvc.RestAssuredMockMvc;

@SpringBootTest(classes = ContractRestServiceApplication.class)
public abstract class BaseClass {

  @Autowired PersonRestController personRestController;

  @MockBean PersonService personService;

  @BeforeEach public void setup() {
    RestAssuredMockMvc.standaloneSetup(personRestController);

    Mockito.when(personService.findPersonById(1L))
        .thenReturn(new Person(1L, "foo", "bee"));
  }

}

在此步驟中,當執行測試時,測試結果應為綠色,表明 REST 控制器與契約一致,並且您有一個功能齊全的服務。

檢查簡單的 Person 查詢業務邏輯

模型類Person.java contract-rest-service/src/main/java/hello/Person.java

package hello;

class Person {

  Person(Long id, String name, String surname) {
    this.id = id;
    this.name = name;
    this.surname = surname;
  }

  private Long id;

  private String name;

  private String surname;

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getSurname() {
    return surname;
  }

  public void setSurname(String surname) {
    this.surname = surname;
  }
}

服務 bean PersonService.java,它只是在記憶體中填充一些 Person 實體,並在請求時返回一個。contract-rest-service/src/main/java/hello/PersonService.java

package hello;

import java.util.HashMap;
import java.util.Map;

import org.springframework.stereotype.Service;

@Service
class PersonService {

  private final Map<Long, Person> personMap;
  
  public PersonService() {
    personMap = new HashMap<>();
    personMap.put(1L, new Person(1L, "Richard", "Gere"));
    personMap.put(2L, new Person(2L, "Emma", "Choplin"));
    personMap.put(3L, new Person(3L, "Anna", "Carolina"));
  }
  
  Person findPersonById(Long id) {
    return personMap.get(id);
  }
}

RestController bean PersonRestController.java,它在收到帶有 id 的 Person 的 REST 請求時呼叫PersonService bean。contract-rest-service/src/main/java/hello/PersonRestController.java

package hello;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
class PersonRestController {

  private final PersonService personService;

  public PersonRestController(PersonService personService) {
    this.personService = personService;
  }

  @GetMapping("/person/{id}")
  public Person findPersonById(@PathVariable("id") Long id) {
    return personService.findPersonById(id);
  }
}

測試 contract-rest-service 應用程式

ContractRestServiceApplication.java類作為 Java 應用程式或 Spring Boot 應用程式執行。服務應在埠8000啟動。

在瀏覽器中訪問服務https://:8000/person/1https://:8000/person/2等。

建立契約消費者服務

契約生產者服務準備就緒後,我們現在需要建立消費所提供契約的客戶端應用程式。這是一個常規的 Spring Boot 應用程式,提供一個非常簡單的 REST 服務。該 REST 服務只返回一條帶有查詢到的 Person 姓名的訊息,例如Hello Anna

contract-rest-client/src/main/java/hello/ContractRestClientApplication.java

package hello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class ContractRestClientApplication {

  public static void main(String[] args) {
    SpringApplication.run(ContractRestClientApplication.class, args);
  }
}

@RestController
class MessageRestController {

  private final RestTemplate restTemplate;

  MessageRestController(RestTemplateBuilder restTemplateBuilder) {
    this.restTemplate = restTemplateBuilder.build();
  }

  @RequestMapping("/message/{personId}")
  String getMessage(@PathVariable("personId") Long personId) {
    Person person = this.restTemplate.getForObject("https://:8000/person/{personId}", Person.class, personId);
    return "Hello " + person.getName();
  }

}

建立契約測試

生產者提供的契約應作為簡單的 Spring 測試進行消費。

contract-rest-client/src/test/java/hello/ContractRestClientApplicationTest.java

package hello;

import org.assertj.core.api.BDDAssertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

@SpringBootTest
@AutoConfigureStubRunner(
    ids = "com.example:contract-rest-service:0.0.1-SNAPSHOT:stubs:8100",
    stubsMode = StubRunnerProperties.StubsMode.LOCAL
)
public class ContractRestClientApplicationTest {

  @Test
  public void get_person_from_service_contract() {
    // given:
    RestTemplate restTemplate = new RestTemplate();
    
    // when:
    ResponseEntity<Person> personResponseEntity = restTemplate.getForEntity("https://:8100/person/1", Person.class);

    // then:
    BDDAssertions.then(personResponseEntity.getStatusCodeValue()).isEqualTo(200);
    BDDAssertions.then(personResponseEntity.getBody().getId()).isEqualTo(1l);
    BDDAssertions.then(personResponseEntity.getBody().getName()).isEqualTo("foo");
    BDDAssertions.then(personResponseEntity.getBody().getSurname()).isEqualTo("bee");
    
  }
}

此測試類將載入契約生產者服務的存根,並確保與服務的整合與契約一致。

如果消費者服務的測試與生產者的契約之間的通訊出現故障,測試將失敗,需要在進行新的生產更改之前修復問題。

測試 contract-rest-client 應用程式

ContractRestClientApplication.java類作為 Java 應用程式或 Spring Boot 應用程式執行。服務應在埠9000啟動。

在瀏覽器中訪問服務https://:9000/message/1https://:9000/message/2等。

總結

恭喜!您剛剛使用 Spring 使您的 REST 服務宣告其契約,並使消費者服務與此契約保持一致。

另請參閱

以下指南也可能有所幫助

想寫新指南或為現有指南做貢獻嗎?請檢視我們的貢獻指南

所有指南的程式碼均採用 ASLv2 許可,文字內容採用署名-禁止演繹知識共享許可

獲取程式碼