本指南将引导您完成创建一个带有合约桩的 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
-
进入
gs-contract-rest/initial
目录 -
跳转到 创建合约生产者服务。
完成后,您可以对照 gs-contract-rest/complete
中的代码检查您的成果。
使用 Gradle 构建
使用 Gradle 构建
首先,您需要设置一个基本的构建脚本。在使用 Spring 构建应用程序时,您可以使用任何喜欢的构建系统,但这里包含了使用 Gradle 和 Maven 所需的代码。如果您对两者都不熟悉,请参考 使用 Gradle 构建 Java 项目 或 使用 Maven 构建 Java 项目。
创建目录结构
在您选择的项目目录中,创建以下子目录结构;例如,在*nix系统上使用mkdir -p src/main/java/hello
命令:
└── 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 依赖项。您可以覆盖任何您希望的版本,但默认情况下会使用 Spring 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
脚本。该契约指定,如果有一个GET
请求访问URL /person/1
,则会在响应体中返回代表Person
实体的示例数据id=1
、name=foo
和surname=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 的 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
启动。
在浏览器中访问服务 http://localhost:8000/person/1
、http://localhost: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("http://localhost: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("http://localhost: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
启动。
在浏览器中访问该服务 http://localhost:9000/message/1
,http://localhost:9000/message/2
等。
总结
恭喜!您刚刚使用了 Spring 来使您的 REST 服务声明其契约,并使消费者服务与该契约保持一致。
另请参阅
以下指南也可能会有所帮助: