微服务联调靠“玄学”?——Spring Cloud Contract 把接口争吵变成可执行契约!

举报
bug菌 发表于 2026/01/13 11:52:10 2026/01/13
【摘要】 🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。  本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。...

🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
  
本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】https://blog.csdn.net/weixin_43970743/article/details/151115907,你想学习的都被收集在内,快速投入学习!!两不误。
  
若还想学习更多,可直接前往《滚雪球学SpringBoot(全版本合集)》:https://blog.csdn.net/weixin_43970743/category_11599389.html,涵盖SpringBoot所有版本教学文章。

演示环境说明:

  • 开发工具:IDEA 2021.3
  • JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
  • Spring Boot版本:3.5.4(于25年7月24日发布)
  • Maven版本:3.8.2 (或更高)
  • Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
  • 操作系统:Windows 11

1)微服务测试痛点:为什么需要 CDC(Consumer-Driven Contracts)?

微服务一多,最容易把人逼疯的不是业务复杂,而是依赖方太多

  • A 服务(Consumer)依赖 B 服务(Producer)的 /users/{id}
  • B 改了字段名、状态码、响应结构……A 这边测试不一定立刻炸(尤其是 mock 写得“很乐观”的时候)
  • 真正炸往往发生在:联调、预发、甚至线上(那种“我明明没动你啊”最扎心🥲)

CDC 的核心思想很朴素:

由消费方提出期望(契约),生产方必须证明自己满足契约;同时,消费方使用由契约生成的 Stub 来隔离测试。

Spring Cloud Contract 就是把这件事工具化:你写契约(Groovy/YAML 等),它可以在生产者侧生成测试来验证实现,还能在消费者侧生成/运行 WireMock Stub用于隔离协作方。

2)契约文件怎么写:Groovy / YAML 两条路

官方文档明确:契约可以用 Groovy DSL 或 YAML 表达,并放在 contractsDslDir 指定目录下,默认是 src/test/resources/contracts
(写到这我忍不住吐槽一句:默认目录真的挺合理,别一上来就改路径改到自己都找不到🤣)

2.1 Groovy 契约示例(HTTP)

场景:GET /api/users/1 返回 200 和用户信息

src/test/resources/contracts/user/get_user_1.groovy

import org.springframework.cloud.contract.spec.Contract

Contract.make {
  description "should return user by id"
  request {
    method GET()
    url "/api/users/1"
  }
  response {
    status OK()
    headers {
      contentType(applicationJson())
    }
    body(
      id: 1,
      name: "Britney",
      level: "VIP"
    )
  }
}

Groovy DSL 的好处是表达力强、能写 matcher、还能做 DSL 扩展(官方也提供了 DSL customization 的机制)。

2.2 YAML 契约示例(更“配置化”)

src/test/resources/contracts/user/get_user_1.yml

description: should return user by id
request:
  method: GET
  url: /api/users/1
response:
  status: 200
  headers:
    Content-Type: application/json
  body:
    id: 1
    name: Britney
    level: VIP

如果你团队里不想引入 Groovy(或者有人看到 Groovy 就皱眉😅),YAML 很友好;但复杂匹配场景 Groovy 往往更顺手。

3)Producer 端:自动生成测试用例,验证接口实现(别“写了契约就当完成”)

Spring Cloud Contract 的基本生产者流程是:

  1. contracts 目录写契约
  2. 引入 Verifier 依赖 + Maven/Gradle 插件
  3. 生成测试并运行:只要实现不符合契约,构建直接失败

3.1 Maven 插件(典型配置思路)

官方文档对 Maven 项目接入有专门章节,并说明生成测试源目录有时需要加入 IDE classpath。

一个常见配置(示意):

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-contract-maven-plugin</artifactId>
      <version>${spring-cloud-contract.version}</version>
      <extensions>true</extensions>
      <configuration>
        <!-- 生成的测试会继承这个基类:你在这里准备 MockMvc/RestAssured 等 -->
        <baseClassForTests>com.example.contract.ContractBase</baseClassForTests>
      </configuration>
    </plugin>

    <!-- 可选:把 generated-test-sources 加入测试源码路径,IDE 才能识别 -->
    <plugin>
      <groupId>org.codehaus.mojo</groupId>
      <artifactId>build-helper-maven-plugin</artifactId>
      <executions>
        <execution>
          <id>add-test-source</id>
          <phase>generate-test-sources</phase>
          <goals><goal>add-test-source</goal></goals>
          <configuration>
            <sources>
              <source>${project.build.directory}/generated-test-sources/contracts/</source>
            </sources>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

生成测试源目录与 build-helper 的做法在官方 Maven 项目文档里有提到。

3.2 baseClassForTests:让“生成测试”知道怎么发请求

你需要写个基类,让生成的测试能跑(比如用 MockMvc):

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
public abstract class ContractBase {

  @Autowired MockMvc mockMvc;

  @BeforeEach
  void setup() {
    RestAssuredMockMvc.mockMvc(mockMvc);
  }
}

然后你在 Producer 里实现真实 Controller。只要响应和契约不一致,生成测试就会失败。这个机制本质上就是:契约在生产者侧被“自动变成验收测试”

4)Consumer 端:自动生成 Stub(WireMock),隔离依赖测试

消费者最怕两件事:

  • 依赖服务没起、网络不稳、环境不同步
  • 你写的 mock 跟真实接口偏得离谱(还自信满满😂)

Spring Cloud Contract 的 Stub Runner 就是为了解决“怎么把生产者生成的 stub 交付给消费者,并自动跑起来”的问题:它可以下载 stub jar 或从 classpath 取 stub,然后启动 WireMock 服务供消费者测试使用。

4.1 Stub 交付:stubs-jar(常见做法)

生产者构建时除了跑生成测试,还会生成一个包含 WireMock 映射的 stubs-jar(Maven 插件提供了相关目标/能力)。
然后把它发布到 Nexus/Artifactory(或内部仓库),消费者测试时直接拉下来用。

4.2 Consumer 测试中跑 Stub:Stub Runner + WireMock

消费者侧(JUnit)典型用法是加个注解,让 stub 自动拉起:

@SpringBootTest
@AutoConfigureStubRunner(
  ids = "com.example:producer-service:+:stubs:8085",
  stubsMode = StubRunnerProperties.StubsMode.REMOTE
)
class ConsumerIntegrationTest {

  @Test
  void should_call_producer_stub() {
    // 你的 consumer 代码把 producer baseUrl 指向 localhost:8085
    // 然后跑集成测试:完全不依赖真实 producer
  }
}
  • ids 指向生产者的 stub 坐标(groupId:artifactId:version:classifier:port)
  • Stub Runner 会为你启动 WireMock(HTTP stubs)
    这些能力在 Stub Runner Core 的官方说明中明确:下载/挑选 stub、启动 WireMock、提供 stub 定义。

小贴士:如果你不想连远程仓库,也可以用 LOCALCLASSPATH 模式(看你团队的 artifact 管理习惯)。

5)把流程串成一条“能落地”的 CDC 工作流(我个人最推荐的那种🙂)

给你一个实战型流程,按这个跑,团队争吵会少很多:

  1. Consumer 提需求:把对 Producer 的调用期望写成契约(字段、状态码、headers、示例 body)
  2. Producer 拉契约:在 Producer 构建里生成测试并跑,保证实现满足契约
  3. Producer 发布 stubs-jar:把 stub 当作“可复用资产”发布到仓库
  4. Consumer 用 Stub Runner 测试:消费者 CI 不依赖真实 Producer,稳定、可重复
  5. 契约变更走版本化:契约是接口演进的“事实标准”,别靠口头同步

这套流程真正的价值不是“我用了一个新工具”,而是:

把接口协作从“人肉联调”升级成“可执行契约 + 可交付 stub”。
这才是 CDC 的魂。

6)常见坑(我替你提前踩两脚😅)

坑 1:契约写得太“宽松”,等于没写

比如响应 body 只写了一个字段,其他字段不约束;或者状态码不严格。最后 Producer 随便改,Consumer 还是会炸。
建议:关键字段要约束,非关键字段可以宽松,但别“全宽松”。

坑 2:Producer 生成测试没跑 / CI 没卡口

契约测试的核心是“强制验证”。如果生成测试不进 CI,那契约就变成装饰品。
建议:把 generateTests + test 绑定到构建生命周期,让不满足契约的提交过不了。

坑 3:Consumer 用 stub,但 baseUrl 没切对

Stub Runner 把 WireMock 起在某个端口,你的 consumer client 还是指向真实环境,那当然“不生效”。
建议:测试 profile 下通过配置注入 producerUrl(比如 http://localhost:8085)。

收尾:契约测试到底值不值?我用一句话反问你🙂

如果你的团队每次联调都在吵:

  • “你怎么又改字段名?”
  • “我这边没问题啊,你清缓存了吗?”
  • “昨天还能跑,今天怎么挂了?”

那我想问你一句:

**你们到底是在开发功能,还是在反复对齐接口细节?**😄

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。

ps:本文涉及所有源代码,均已上传至Gitee:https://gitee.com/bugjun01/SpringBoot-demo 开源,供同学们一对一参考 Gitee传送门https://gitee.com/bugjun01/SpringBoot-demo,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗

🫵 Who am I?

我是 bug菌:

  • 热活跃于 CSDN:https://blog.csdn.net/weixin_43970743 | 掘金:https://juejin.cn/user/695333581765240 | InfoQ:https://www.infoq.cn/profile/4F581734D60B28/publish | 51CTO:https://blog.51cto.com/u_15700751 | 华为云:https://bbs.huaweicloud.com/community/usersnew/id_1582617489455371 | 阿里云:https://developer.aliyun.com/profile/uolxikq5k3gke | 腾讯云:https://cloud.tencent.com/developer/user/10216480/articles 等技术社区;
  • CSDN 博客之星 Top30、华为云多年度十佳博主&卓越贡献奖、掘金多年度人气作者 Top40;
  • 掘金、InfoQ、51CTO 等平台签约及优质作者;
  • 全网粉丝累计 30w+

更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看:https://bbs.csdn.net/topics/612438251 👈️
硬核技术公众号 「猿圈奇妙屋」https://bbs.csdn.net/topics/612438251 期待你的加入,一起进阶、一起打怪升级。

- End -

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。