└── src └── main └── java └── hello
消费者驱动契约
本指南将引导你完成使用其契约存根创建 Spring REST 应用程序并在另一个 Spring 应用程序中使用契约的过程。 Spring Cloud 契约项目
你将构建的内容
你将设置两个微服务,一个提供其契约,另一个使用此契约以确保与契约提供者服务的集成符合规范。如果将来生产者服务的契约发生更改,则消费者服务的测试将失败,从而发现潜在的不兼容性。
你需要的内容
-
大约 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.1.2'
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.1.2' }
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.1.2</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.1.2</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
请求,则在内容类型为 application/json
的响应正文中将返回表示 Person
实体的示例数据 id=1
、name=foo
和 surname=bee
。
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 的人的 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 服务只返回一条消息,其中包含查询到的 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://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 服务声明其合同,并且消费者服务与该合同保持一致。