Spring Cloud Contract 1.0.0.M1 发布

工程 | Marcin Grzejszczak | 2016 年 7 月 25 日 | ...

我谨代表 Spring Cloud 团队宣布新的 Spring Cloud 项目 Spring Cloud Contract 发布 1.0.0.M1 版本。您可以从 Spring 的里程碑仓库获取,或者更好的方式是 - 前往 start.spring.io 并从那里选择。

Spring Cloud Contract

微服务方法有很多好处,但也带来了复杂性。这是分布式系统工作的必然结果:随着复杂性的增加,问题也随之增加。在本文中,我们将展示如何使用消费者驱动的契约方法来测试微服务并创建更好的 API。为了使微服务测试更容易,我们很高兴地推出 Spring Cloud 项目家族中的一个新项目 - Spring Cloud Contract。该项目为 Spring 应用程序中的消费者驱动的契约和服务模式提供支持,涵盖了编写测试、将其发布为资产、以及断言生产者和消费者遵守契约(对于 HTTP 和基于消息的交互)的一系列选项。

本文是最近关于如何使用数据库进行零停机部署的另一篇文章的补充。

服务提供者与消费者

在深入细节之前,让我们先回顾一些理论。分布式系统相关最大的挑战之一是节点之间传递的消息结构的约定(“消息”指任何格式良好、非流式的数据,这适用于传统的 HTTP API 以及基于事件的微服务)。当我们考虑消息结构时,会产生以下几个问题:

  • 消费者如何知道生产者已经更改了其 API?

  • 生产者方如何知道是否会破坏消费者?

  • 如果消费者使用桩(stubs)进行测试,谁应该创建这些桩?

  • 如何确保桩的质量?

解决这些问题的一种方法是引入“契约(contract)”的概念。契约是提供者和消费者之间关于他们通信应该是什么样子的约定。问题仍然在于谁应该驱动 API 的变更、契约应该存储在哪里以及契约应该包含什么。

在这篇博客文章中,我们将介绍名为“消费者驱动的契约(Consumer Driven Contracts)”的方法,以及新的 Spring Cloud 项目 Spring Cloud Contract Verifier,该项目以前称为 Accurest,由 Codearte 公司托管。

与此同时,在一家公司里……

让我们想象一下以下场景:

生产者团队完成了冲刺,并通过引入新版本的 API 更改了其应用程序。由于时间紧张,没有与该 API 的消费者进行协商。消费者在其测试中模拟(mock)了生产者端的所有集成,并且由于没有人通知他们任何变更,他们也没有更新这些模拟。这就是为什么所有单元测试和集成测试仍然通过,但端到端测试却惨败的原因。当消费者注意到生产者端 API 已更改时,他们不得不投入大量时间来调整其生产代码和测试代码,以发送和接收新的所需数据。消费者团队认为(由于时间紧张和更改的复杂性)这几乎是不可能的,因此他们开始向生产者团队提交问题,要求调整其 API。生产者团队回复说“没时间”,并表示他们可以“与他们的产品负责人谈谈,以便将该需求放入他们的待办事项列表中”。

如果你看看这两个团队的回顾,你会看到以下内容:

对于消费者方

  • 引入了破坏性变更,但没有人通知我们

  • 生产者团队没有就他们的 API 更改进行协商 - 新的 API 无法使用

  • 我们的集成测试没有发现生产者端 API 的变化

  • 在调整他们的 API 方面,我们完全被生产者团队忽视了

对于生产者方

  • 每个人都生我们的气,但我们必须交付商业价值

  • 当我们更改 API 时,我们无法更新每个消费者团队的测试

是不是听起来很耳熟?别担心,有一些方法可以改变这种做法,让每个人都不那么烦恼。

什么是消费者驱动的契约?

上述场景中存在几个问题:

  • API 的更改未与消费者协商

  • 桩(stubbing)过程由消费者拥有,因此生产者端的更改不会反映出来

让我们关注第一个问题。你还记得测试驱动开发(TDD)方法吗?你先写一个测试来表达你的期望,然后编写实现代码让测试通过,最后重构使代码更好。 “红、绿、重构”——失败的测试、通过的测试、重构的代码。TDD 关注的是犯错误。与代码 API 应该如何设计的假设相关的错误。这是一个迭代过程,可以让你提高代码质量。开发者是代码 API 变化的驱动者。他是其使用者,他知道想要实现什么,因此他会修改 API 直到满意为止。现在,让我们想象一下我们将这种方法应用到 API 设计层面...

由于消费者是使用 API 的人,他们应该成为 API 变化的驱动者。与 TDD 的主要区别在于,这里有两个团队参与该过程——消费者和生产者。这就是你可以从消费者驱动的契约(CDC)方法中受益的地方。其特点包括:

  • 生产者 API 由消费者与生产者团队共同设计(沟通至关重要!)

  • 契约被记录下来,必须同时满足双方的需求

  • 工作可以解耦——一旦契约被记录下来,两个团队就可以独立工作

  • 生产者方拥有契约(这是具体人员明确定义的责任)

Spring Cloud 团队希望有一个工具能让我们能够:

  • 以易读但灵活的方式定义契约

  • 让契约展示一些用例,而不仅仅是呈现消息结构

  • 生成测试以自动验证生产者端是否遵守契约

  • 从契约中自动生成桩(stubs),以便消费者可以重用

  • 使这种方法适用于 HTTP 和基于消息的通信

Spring Cloud Contract Verifier 通过提供自动化解决方案来解决这个问题,以确保创建的契约及其桩的质量和可靠性。它包含以下主要功能:

  • 库的核心部分提供了契约的概念

  • Verifier 由生产者使用(通常通过构建插件)

  • Spring Cloud Contract Verifier Maven / Gradle 插件提供了将契约转换为测试和 WireMock 桩的功能(WireMock 是一个 HTTP 服务器桩

  • Spring Cloud Contract Stub Runner 允许消费者自动下载上游生产者的桩,并在集成测试中启动内存中的 HTTP 桩服务器

  • Spring Cloud Contract Stub Runner 也允许消费者(通过 Spring Integration、Spring Cloud Stream 或 Apache Camel)发送和接收契约中描述的消息

让我们来看一个关于 HTTP 通信如何使用该工具的分步示例。

使用 Spring Cloud Contract Verifier 进行 CDC

让我们以欺诈检测和贷款发放流程为例。业务场景是:我们想向人们发放贷款,但不希望他们偷走我们的钱。我们系统当前的实现是向所有人发放贷款。

假设贷款发放(Loan Issuance)欺诈检测(Fraud Detection)服务器的客户端。在当前的冲刺中,我们需要开发一个新功能——如果客户想借太多钱,我们就将他标记为欺诈。

技术说明 - 欺诈检测的 artifact id 为 http-server,贷款发放的 artifact id 为 http-client,它们都属于 group id com.example

社交说明 - 消费者和生产者开发团队在整个过程中需要直接沟通和讨论变更。CDC 的核心是沟通。

The 生产者代码在此可用消费者代码在此可用

消费者方(贷款发放)

作为贷款发放服务(欺诈检测服务的消费者)的开发者

通过为你的功能编写测试来开始 TDD

@Test
public void shouldBeRejectedDueToAbnormalLoanAmount() {
	// given:
	LoanApplication application = new LoanApplication(new Client("1234567890"),
			99999);
	// when:
	LoanApplicationResult loanApplication = sut.loanApplication(application);
	// then:
	assertThat(loanApplication.getLoanApplicationStatus())
			.isEqualTo(LoanApplicationStatus.LOAN_APPLICATION_REJECTED);
	assertThat(loanApplication.getRejectionReason()).isEqualTo("Amount too high");
}

我们刚刚为我们的新功能编写了一个测试。如果收到一笔大额贷款申请,我们应该拒绝该贷款申请并附上一些描述。

编写缺失的实现

在某个时候,你需要向欺诈检测服务发送请求。假设我们想发送包含客户 ID 和他想从我们这里借款金额的请求。我们想通过 PUT 方法将其发送到 /fraudcheck URL。

ResponseEntity<FraudServiceResponse> response =
		restTemplate.exchange("http://localhost:" + port + "/fraudcheck", HttpMethod.PUT,
				new HttpEntity<>(request, httpHeaders),
				FraudServiceResponse.class);

为了简单起见,我们硬编码了欺诈检测服务的端口为 8080,而我们的应用程序运行在 8090 端口。

如果我们运行编写的测试,它显然会失败,因为在端口 8080 上没有运行任何服务。

在本地克隆欺诈检测服务仓库

我们将开始试用生产者端。这就是为什么我们首先需要克隆它。

git clone https://your-git-server.com/server.git local-http-server-repo

在欺诈检测服务仓库中本地定义契约

作为消费者,我们需要明确定义我们想要实现什么。我们需要制定我们的期望。这就是为什么我们编写以下契约。

package contracts

org.springframework.cloud.contract.spec.Contract.make {
			request { // (1)
				method 'PUT' // (2)
				url '/fraudcheck' // (3)
				body([ // (4)
					clientId: value(consumer(regex('[0-9]{10}'))),
					loanAmount: 99999
					])
				headers { // (5)
					header('Content-Type', 'application/vnd.fraud.v1+json')
				}
			}
			response { // (6)
				status 200 // (7)
				body([ // (8)
					fraudCheckStatus: "FRAUD",
					rejectionReason: "Amount too high"
				])
				headers { // (9)
					 header('Content-Type': value(
							 producer(regex('application/vnd.fraud.v1.json.*')),
							 consumer('application/vnd.fraud.v1+json'))
					 )
				}
			}
}

/*
Since we don't want to force on the user to hardcode values of fields that are dynamic
(timestamps, database ids etc.), one can provide parametrize those entries by using the
`value(consumer(...), producer(...))` method. That way what's present in the `consumer`
section will end up in the produced stub. What's there in the `producer` will end up in the
autogenerated test. If you provide only the regular expression side without the concrete
value then Spring Cloud Contract will generate one for you.

From the Consumer perspective, when shooting a request in the integration test:

(1) - If the consumer sends a request
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
 * has a field `clientId` that matches a regular expression `[0-9]{10}`
 * has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/vnd.fraud.v1+json`
(6) - then the response will be sent with
(7) - status equal `200`
(8) - and JSON body equal to
 { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` equal to `application/vnd.fraud.v1+json`

From the Producer perspective, in the autogenerated producer-side test:

(1) - A request will be sent to the producer
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
 * has a field `clientId` that will have a generated value that matches a regular expression `[0-9]{10}`
 * has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/vnd.fraud.v1+json`
(6) - then the test will assert if the response has been sent with
(7) - status equal `200`
(8) - and JSON body equal to
 { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` matching `application/vnd.fraud.v1+json.*`
 */

该契约使用静态类型的 Groovy DSL 编写。您可能想知道那些 value(consumer(…​), producer(…​)) 部分是什么。通过使用这种表示法,Spring Cloud Contract 允许您定义 JSON / URL 等动态部分。对于标识符或时间戳,您不想硬编码一个值。您希望允许不同范围的值。这就是为什么对于消费者端,您可以设置匹配这些值的正则表达式。您可以通过 map 表示法或带有插值的 String 来提供 body。请查阅文档了解更多信息。 我们强烈建议使用 map 表示法!

前述契约是双方之间的协议,表明:

  • 如果发送的 HTTP 请求具有:

    • 端点 /fraudcheck 上的 PUT 方法

    • JSON body 中 clientId 匹配正则表达式 [0-9]{10} 并且 loanAmount 等于 99999

    • 以及 Content-Type 请求头等于 application/vnd.fraud.v1+json

  • 那么将向消费者发送一个 HTTP 响应,该响应:

    • 状态码为 200

    • 包含 JSON body,其中 fraudCheckStatus 字段值为 FRAUDrejectionReason 字段值为 Amount too high

    • 以及一个 Content-Type 响应头,其值为 application/vnd.fraud.v1+json

一旦我们准备好在集成测试中实际检查 API,我们只需要在本地安装桩

将 Spring Cloud Contract Verifier 插件添加到服务器端

我们可以添加 Maven 或 Gradle 插件 - 在此示例中,我们将展示如何添加 Maven。首先,我们需要添加 Spring Cloud Contract BOM。

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

接下来,Spring Cloud Contract Verifier 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>com.example.fraud.MvcTest</baseClassForTests>
	</configuration>
</plugin>

添加插件后,我们获得了 Spring Cloud Contract Verifier 的功能,它可以根据提供的契约:

  • 生成并运行测试

  • 生成并安装桩

我们不希望生成测试,因为作为消费者,我们只想使用桩。这就是为什么我们需要跳过测试生成和执行。当我们执行时:

cd local-http-server-repo
./mvnw clean install -DskipTests

在日志中,我们会看到类似这样的内容:

[INFO] --- spring-cloud-contract-maven-plugin:1.0.0.BUILD-SNAPSHOT:generateStubs (default-generateStubs) @ http-server ---
[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar
[INFO]
[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ http-server ---
[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:1.4.0.BUILD-SNAPSHOT:repackage (default) @ http-server ---
[INFO]
[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ http-server ---
[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.jar
[INFO] Installing /some/path/http-server/pom.xml to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.pom
[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar

这行非常重要:

[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar

它确认 http-server 的桩已经安装在本地仓库中。

运行集成测试

为了利用 Spring Cloud Contract Stub Runner 的自动桩下载功能,你需要在消费者方项目(贷款申请服务)中进行以下操作。

添加 Spring Cloud Contract BOM

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

添加 Spring Cloud Contract Stub Runner 的依赖

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

提供 Stub Runner 下载协作者桩的 group id 和 artifact id。由于您正在离线使用协作者,因此还需要提供离线工作开关(可选步骤)。

stubrunner:
  work-offline: true
  stubs.ids: 'com.example:http-server:+:stubs:8080'

使用 @AutoConfigureStubRunner 注解你的测试类

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureStubRunner
public class LoanApplicationServiceTests {

现在,如果你运行测试,你会看到类似这样的内容:

2016-07-19 14:22:25.403  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Desired version is + - will try to resolve the latest version
2016-07-19 14:22:25.438  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved version is 0.0.1-SNAPSHOT
2016-07-19 14:22:25.439  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolving artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT using remote repositories []
2016-07-19 14:22:25.451  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar
2016-07-19 14:22:25.465  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacking stub from JAR [URI: file:/path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar]
2016-07-19 14:22:25.475  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacked file to [/var/folders/0p/xwq47sq106x1_g3dtv6qfm940000gq/T/contracts100276532569594265]
2016-07-19 14:22:27.737  INFO 41050 --- [           main] o.s.c.c.stubrunner.StubRunnerExecutor    : All stubs are now running RunningStubs [namesAndPorts={com.example:http-server:0.0.1-SNAPSHOT:stubs=8080}]

这意味着 Stub Runner 找到了你的桩,并为 group id 为 com.example、artifact id 为 http-server、版本为 0.0.1-SNAPSHOT、分类器为 stubs 的应用程序在端口 8080 上启动了一个服务器。

提交 PR

到目前为止我们所做的是一个迭代过程。我们可以试用契约,在本地安装,并在消费者端工作,直到我们对契约满意为止。

一旦我们对结果满意并且测试通过,就向生产者端提交 PR。至此,消费者端的工作就完成了。

生产者方(欺诈检测服务器)

作为欺诈检测服务器(贷款发放服务的消费者)的开发者

初始实现

提醒一下,您可以在这里看到初始实现:

@RequestMapping(
		value = "/fraudcheck",
		method = PUT,
		consumes = FRAUD_SERVICE_JSON_VERSION_1,
		produces = FRAUD_SERVICE_JSON_VERSION_1)
public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);

接管 PR

git checkout -b contract-change-pr master
git pull https://your-git-server.com/server-side-fork.git contract-change-pr

你必须添加自动生成的测试所需的依赖

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

在 Maven 插件的配置中,我们传递了 baseClassForTests 属性:

<plugin>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
	<version>${spring-cloud-contract.version}</version>
	<extensions>true</extensions>
	<configuration>
		<baseClassForTests>com.example.fraud.MvcTest</baseClassForTests>
	</configuration>
</plugin>

这是因为所有生成的测试都会继承该类。在那里你可以设置你的 Spring Context 或任何必需的东西。在我们的例子中,我们使用 Rest Assured MVC 来启动生产者端的 FraudDetectionController

package com.example.fraud;

import com.example.fraud.FraudDetectionController;
import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc;

import org.junit.Before;

public class MvcTest {

	@Before
	public void setup() {
		RestAssuredMockMvc.standaloneSetup(new FraudDetectionController());
	}

	public void assertThatRejectionReasonIsNull(Object rejectionReason) {
		assert rejectionReason == null;
	}
}

现在,如果你运行 ./mvnw clean install,你会得到类似这样的结果:

Results :

Tests in error:
  ContractVerifierTest.validate_shouldMarkClientAsFraud:32 » IllegalState Parsed...

那是因为你有一个新的契约,从中生成了一个测试,并且由于你尚未实现该功能,所以该测试失败了。自动生成的测试看起来像这样:

@Test
public void validate_shouldMarkClientAsFraud() throws Exception {
    // given:
        MockMvcRequestSpecification request = given()
                .header("Content-Type", "application/vnd.fraud.v1+json")
                .body("{\"clientId\":\"1234567890\",\"loanAmount\":99999}");

    // when:
        ResponseOptions response = given().spec(request)
                .put("/fraudcheck");

    // then:
        assertThat(response.statusCode()).isEqualTo(200);
        assertThat(response.header("Content-Type")).matches("application/vnd.fraud.v1.json.*");
    // and:
        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
        assertThatJson(parsedJson).field("fraudCheckStatus").matches("[A-Z]{5}");
        assertThatJson(parsedJson).field("rejectionReason").isEqualTo("Amount too high");
}

正如你所见,契约中所有存在于 value(consumer(…​), producer(…​)) 块中的 producer() 部分都被注入到了测试中。

这里需要注意的是,在生产者端,我们也正在进行 TDD。我们以测试的形式拥有期望。这个测试向我们自己的应用程序发送一个请求,其 URL、请求头和 body 都定义在契约中。它还期望响应中包含非常精确定义的值。换句话说,你拥有的是“红、绿、重构”中的“红”部分。现在是时候将“红”变成“绿”了。

编写缺失的实现

现在我们知道预期的输入和输出是什么了,让我们编写缺失的实现。

@RequestMapping(
		value = "/fraudcheck",
		method = PUT,
		consumes = FRAUD_SERVICE_JSON_VERSION_1,
		produces = FRAUD_SERVICE_JSON_VERSION_1)
public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
if (amountGreaterThanThreshold(fraudCheck)) {
	return new FraudCheckResult(FraudCheckStatus.FRAUD, AMOUNT_TOO_HIGH);
}
return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);
}

如果我们再次执行 ./mvnw clean install,测试将通过。由于 Spring Cloud Contract Verifier 插件将测试添加到 generated-test-sources 中,你实际上可以从你的 IDE 运行这些测试。

部署你的应用程序

完成工作后,就可以部署你的更改了。首先合并分支:

git checkout master
git merge --no-ff contract-change-pr
git push origin master

然后我们假设你的 CI 会运行类似 ./mvnw clean deploy 的命令,这将发布应用程序和桩制品。

消费者方(贷款发放)最后一步

作为贷款发放服务(欺诈检测服务的消费者)的开发者

合并分支到 master

git checkout master
git merge --no-ff contract-change-pr

在线工作

现在你可以禁用 Spring Cloud Contract Stub Runner 的离线工作,并提供你的桩所在的仓库位置。此时,生产者端的桩将自动从 Nexus / Artifactory 下载。

stubrunner.stubs:
  ids: 'com.example:http-server:+:stubs:8080'
  repositoryRoot: http://repo.spring.io/libs-snapshot

大功告成!

总结

在这个例子中,您可以看到如何使用 Spring Cloud Contract Verifier 来实践消费者驱动的契约方法。通过这种方式,我们实现了:

  • 一个适合消费者和生产者的 API

  • 易读且已针对生产者进行测试的契约

  • 可由所有消费者在其集成测试中使用的经过验证的桩

  • 消费者端工具,可自动下载最新的桩并为您设置桩环境

拓展阅读

订阅 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您快速提升。

了解更多

获取支持

Tanzu Spring 在一项简单的订阅中提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将到来的活动

查看 Spring 社区所有即将到来的活动。

查看全部