Spring Boot 与 Docker
观察 GraphQL 的实际运行

本指南将引导您完成创建一个带有合约桩的 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 构建文件

以下是初始的 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 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=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 的 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/1http://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/1http://localhost:9000/message/2 等。

总结

恭喜!您刚刚使用了 Spring 来使您的 REST 服务声明其契约,并使消费者服务与该契约保持一致。

另请参阅

以下指南也可能会有所帮助:

本页目录