4、服务契约设计:WSDL与OpenAPI、接口版本控制策略、契约优先设计

说到服务契约,我脑子里第一个蹦出来的画面,是十年前一个深夜的线上事故。那时候我们团队刚搞SOA,服务之间调用全靠口头约定——"你返回JSON,字段名叫user_name"。结果某天上游服务改了字段名,下游直接崩了。嗯,从那以后,我成了契约设计的坚定信徒。

说白了,服务契约就是服务之间的"合同"。你承诺提供什么接口,我承诺怎么调用。没有这份合同,微服务就是一盘散沙。

4.1 WSDL与OpenAPI:两种契约语言

先聊聊WSDL。这东西是SOAP时代的产物,我刚开始接触时觉得它又臭又长。一个简单的接口,WSDL能写几百行XML。但不得不承认,它很严谨。

WSDL的核心结构:

  • types:定义数据类型,通常内嵌XSD
  • message:定义请求和响应的消息格式
  • portType:定义操作(相当于接口方法)
  • binding:绑定到具体协议(SOAP/HTTP)
  • service:指定服务地址

举个例子,一个查询用户信息的WSDL片段长这样:

<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
  <wsdl:types>
    <xsd:schema>
      <xsd:complexType name="UserRequest">
        <xsd:sequence>
          <xsd:element name="userId" type="xsd:string"/>
        </xsd:sequence>
      </xsd:complexType>
    </xsd:schema>
  </wsdl:types>
  <wsdl:message name="getUserRequest">
    <wsdl:part name="parameters" element="tns:UserRequest"/>
  </wsdl:message>
  <wsdl:portType name="UserService">
    <wsdl:operation name="getUser">
      <wsdl:input message="tns:getUserRequest"/>
      <wsdl:output message="tns:getUserResponse"/>
    </wsdl:operation>
  </wsdl:portType>
</wsdl:definitions>

我个人习惯,在遗留系统迁移时才会碰WSDL。新项目?我建议直接用OpenAPI。

OpenAPI(以前叫Swagger)是RESTful世界的契约标准。它用JSON或YAML描述接口,可读性强得多。你想想看,一个查询用户的OpenAPI定义:

openapi: 3.0.0
info:
  title: 用户服务
  version: 1.0.0
paths:
  /users/{userId}:
    get:
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string

是不是清爽多了?我在项目中遇到过很多团队,拿着OpenAPI文档直接生成客户端代码,效率翻倍。

我的建议:新服务用OpenAPI 3.0,老系统如果还在用SOAP,别急着重构,先做好WSDL版本管理。等业务稳定了再考虑迁移。

4.2 接口版本控制策略

接口版本控制,说白了就是"改接口不炸下游"。我见过最惨的案例:某团队直接改了线上接口的字段类型,导致十几个调用方同时报错。那个晚上,运维同学的电话被打爆了。

常见的版本控制策略有三种:

策略 实现方式 优点 缺点
URI路径版本 /api/v1/users, /api/v2/users 简单直观,容易路由 URL会变长,语义不够优雅
请求头版本 Accept: application/vnd.myapp.v1+json URL干净,符合RESTful风格 调试时不太直观
查询参数版本 /api/users?version=1 实现简单 容易被缓存污染

我个人习惯用URI路径版本。为什么?因为运维同学看日志时一眼就能看出是哪个版本的接口出了问题。我曾经在排障时,就因为请求头版本号没打出来,多花了两个小时定位问题。

避坑指南:我曾经见过一个团队,版本号从v1直接跳到v5,中间版本全废弃了。结果新来的同事看到v5,以为前面四个版本都有用,花了一周去研究根本不存在的v2接口。版本号要连续,废弃的版本要明确标记为deprecated。

还有一个细节:什么时候该升版本?我定了个简单规则:

  • 向后兼容的变更(加字段、加可选参数):小版本升级,不改路径
  • 不兼容的变更(删字段、改类型、改语义):必须升大版本

你想想看,如果每次加个字段都要升版本,那版本号很快就到v100了。不现实。

4.3 契约优先设计

契约优先设计,英文叫Contract-First。什么意思呢?就是先定义接口契约,再写实现代码。

很多团队习惯反过来——先写代码,再生成文档。结果呢?文档永远滞后,接口改了文档没改。我在项目中遇到过最夸张的一次:文档上写着返回三个字段,实际代码返回了八个字段。下游对接的同学对着文档调了三天,死活调不通。

契约优先的好处很明显:

  • 沟通成本低:前后端、服务之间,先对齐契约再开发
  • 自动化程度高:契约文件可以直接生成Mock服务、客户端代码
  • 变更可追溯:契约文件纳入版本控制,每次改动都有记录

具体怎么做?我建议的流程是这样的:

  1. 业务分析:搞清楚要提供什么能力
  2. 契约编写:用OpenAPI或WSDL定义接口
  3. 契约评审:相关团队一起过一遍,确认没问题
  4. Mock生成:根据契约生成Mock服务,让调用方先开发
  5. 实现开发:服务提供方按照契约写代码
  6. 契约测试:用契约文件做自动化测试,确保实现符合契约

关键点:契约文件是"真理之源"(Source of Truth)。任何接口变更,必须先改契约文件,再改代码。顺序反了,契约优先就成了一句空话。

我记得有一次,团队里一个小伙子急着上线,偷偷改了接口没更新OpenAPI文件。结果下游团队按照旧契约调了三天,最后发现是接口变了。那次之后,我们在CI流水线里加了个检查:如果契约文件和代码不一致,构建直接失败。

嗯,这里要注意:契约优先不是银弹。如果你的业务变化极快,一天改三次接口,那契约优先反而会成为负担。这种情况下,我建议先快速迭代,等接口稳定了再补契约。但话说回来,如果接口一天改三次,你确定你的架构没问题?

工具推荐:

  • OpenAPI:用Swagger Editor或Stoplight Studio编写
  • 契约测试:用Spring Cloud Contract或Pact
  • Mock服务:用WireMock或MockServer

最后总结一句:服务契约不是文档,是代码。把它当成一等公民对待,你的微服务架构会省心很多。