└── src └── main └── java └── hello
消費者驅動契約
本指南將引導您完成建立 Spring REST 應用程式及其契約存根,並在另一個 Spring 應用程式中使用該契約的流程。Spring Cloud Contract 專案
您將建構的內容
您將設定兩個微服務,一個提供其契約,另一個使用此契約,以確保與契約提供者服務的整合符合規範。 如果未來生產者服務的契約變更,則消費者服務的測試將會失敗,從而捕捉到潛在的不相容性。
您需要的東西
-
約 15 分鐘
-
您慣用的文字編輯器或 IDE
-
Java 17 或更高版本
-
您也可以將程式碼直接匯入到您的 IDE 中
如何完成本指南
如同大多數 Spring 入門指南,您可以從頭開始並完成每個步驟,或者您可以跳過您已熟悉的基本設定步驟。 無論哪種方式,您最終都會得到可運作的程式碼。
若要從頭開始,請繼續前往 使用 Gradle 建置。
若要跳過基礎知識,請執行以下操作
-
下載 並解壓縮本指南的原始碼儲存庫,或使用 Git 克隆它:
git clone https://github.com/spring-guides/gs-contract-rest.git
-
cd 進入
gs-contract-rest/initial
-
跳到 建立契約生產者服務。
當您完成時,您可以對照 gs-contract-rest/complete
中的程式碼檢查您的結果。
使用 Gradle 建置
使用 Gradle 建置
首先,您要設定一個基本的建置腳本。 您可以使用任何您喜歡的建置系統來建置 Spring 應用程式,但此處包含使用 Gradle 和 Maven 所需的程式碼。 如果您不熟悉其中任何一個,請參閱 使用 Gradle 建置 Java 專案 或 使用 Maven 建置 Java 專案。
建立目錄結構
在您選擇的專案目錄中,建立以下子目錄結構;例如,在 *nix 系統上使用 mkdir -p src/main/java/hello
建立 Gradle 建置檔案
以下是 初始 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 Tool Suite。
-
閱讀如何在 IntelliJ IDEA 中使用本指南。
建立契約生產者服務
您首先需要建立生產契約的服務。 這是一個常規的 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
請求,則範例資料 id=1
、name=foo
和 surname=bee
代表 Person
實體將在 content-type 為 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"));
}
}
在此步驟中,當執行測試時,測試結果應為 GREEN,表示 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;
}
}
Service 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 的人員的 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
啟動。
建立契約消費者服務
在契約生產者服務準備就緒後,現在我們需要建立用戶端應用程式,該應用程式使用提供的契約。 這是一個常規的 Spring Boot 應用程式,提供非常簡單的 REST 服務。 rest 服務僅傳回一則包含查詢人員姓名的訊息,例如 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://127.0.0.1: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://127.0.0.1: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
啟動。
摘要
恭喜! 您剛剛使用 Spring 使您的 REST 服務宣告其契約,並使消費者服務與此契約保持一致。