4. 服务契约与接口设计:WSDL与RESTful API设计、服务契约优先原则、版本管理策略

服务契约,说白了就是服务提供方和消费方之间的一纸「合同」。合同上写清楚了:你能给我什么、我需要给你什么、咱们怎么通信。没有这份合同,服务之间的调用就是一场混乱。

我见过太多项目,上来就写代码,接口说改就改,结果联调时鸡飞狗跳。嗯,这一章我们就来聊聊怎么把这份「合同」写好、管好。

4.1 服务契约优先原则

什么叫契约优先?就是先定义接口,再写实现代码。这个顺序不能乱。

核心思想:接口是合同,实现是履约。合同没签好,履约就是瞎忙活。

我个人习惯的做法是:

  1. 先定义数据结构——请求和响应的XML/JSON Schema
  2. 再定义操作——服务提供哪些方法、URL路径
  3. 然后生成代码骨架——用工具从契约生成服务端和客户端代码
  4. 最后填充业务逻辑

这样做的好处很明显:

  • 服务提供方和消费方可以并行开发
  • 接口变更在早期就能被发现
  • 生成的代码类型安全,减少运行时错误

注意:契约优先不是「写完契约就不改了」。契约也需要迭代,但每次变更都要走版本管理流程。

4.2 WSDL与SOAP接口设计

WSDL(Web Services Description Language)是SOAP服务的契约描述语言。它用XML描述了服务的所有细节:

  • types:数据类型定义(XML Schema)
  • message:消息结构
  • portType:操作列表(相当于接口)
  • binding:绑定协议(SOAP over HTTP等)
  • service:服务地址

一个典型的WSDL片段长这样:

<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
             xmlns:xsd="http://www.w3.org/2001/XMLSchema"
             targetNamespace="http://example.com/order">
  <types>
    <xsd:schema>
      <xsd:element name="CreateOrderRequest">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="customerId" type="xsd:string"/>
            <xsd:element name="items" type="tns:ItemList"/>
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
    </xsd:schema>
  </types>
  <portType name="OrderPortType">
    <operation name="createOrder">
      <input message="tns:CreateOrderRequest"/>
      <output message="tns:CreateOrderResponse"/>
    </operation>
  </portType>
</definitions>

我在项目中遇到过一个问题:WSDL文件太长了,动辄上千行。后来我们学会了拆分——把公共的数据类型定义放在独立的XSD文件中,用<xsd:import>引入。这样WSDL本身只描述接口,数据模型可以复用。

经验之谈:WSDL设计时,尽量使用文档/字面量(document/literal)风格,避免RPC/编码(RPC/encoded)风格。前者更灵活,后者已经过时了。

4.3 RESTful API设计

RESTful API现在更流行。它的契约不是WSDL,而是OpenAPI规范(以前叫Swagger)。

设计RESTful API,我遵循几个原则:

  • 资源导向——URL表示资源,不是操作。比如/orders,不是/createOrder
  • HTTP动词表达操作——GET查、POST增、PUT全量更新、PATCH部分更新、DELETE删
  • 状态码语义化——200成功、201创建成功、400参数错误、404不存在、500服务器错误
  • 版本号放在URL或Header中——比如/v1/orders或Accept: application/vnd.myapp.v1+json

一个OpenAPI 3.0的示例:

openapi: 3.0.0
info:
  title: 订单服务
  version: 1.0.0
paths:
  /orders:
    get:
      summary: 查询订单列表
      parameters:
        - name: status
          in: query
          schema:
            type: string
      responses:
        '200':
          description: 成功返回订单列表
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Order'
    post:
      summary: 创建订单
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
      responses:
        '201':
          description: 订单创建成功

关键区别:SOAP/WSDL面向操作,REST/OpenAPI面向资源。选哪个取决于你的场景——企业级集成、事务性要求高的场景用SOAP;互联网应用、移动端场景用REST。

4.4 版本管理策略

服务一旦上线,接口就会面临变更。怎么管?我总结了几种策略:

策略 实现方式 适用场景 缺点
URL路径版本 /v1/orders vs /v2/orders 简单直观,客户端容易理解 URL膨胀,长期维护成本高
请求Header版本 Accept: application/vnd.myapp.v1+json URL干净,适合网关路由 调试不方便,需要工具查看Header
查询参数版本 /orders?version=1 临时过渡,快速验证 容易混淆,不推荐长期使用
语义化版本 通过兼容性规则(向后兼容/不兼容) 内部服务,团队可控 需要严格的兼容性检查工具

我个人比较推荐URL路径版本加语义化版本的组合。对外暴露的接口用URL版本,清晰明了;内部服务之间用语义化版本,配合兼容性检查工具自动验证。

避坑指南:我曾经在一个项目里,团队为了省事,直接修改了现有接口的字段,没有升级版本号。结果消费方没同步更新,线上数据全部解析失败。那次事故之后,我们定了一条铁律:任何不兼容的变更都必须升级主版本号。

4.5 契约设计的核心流程

把上面的知识点串起来,契约设计的完整流程是这样的:

服务契约设计核心流程 1. 需求分析 2. 定义数据结构 3. 定义接口操作 WSDL / SOAP OpenAPI / REST 5. 生成代码骨架 版本管理贯穿始终:每次变更都要评估兼容性,决定是否升级版本号

4.6 契约的兼容性检查

契约设计好了,怎么保证变更不会破坏现有消费方?你需要做兼容性检查。

对于WSDL/SOAP:

  • 向后兼容:新增可选字段、新增操作是兼容的;删除字段、修改字段类型是不兼容的
  • 向前兼容:消费方用新契约调用老服务,通常更难保证

对于REST/OpenAPI:

  • 兼容的变更:新增端点、新增可选参数、新增响应字段
  • 不兼容的变更:删除端点、修改参数类型、删除响应字段、修改URL路径

工具推荐:对于OpenAPI,可以用openapi-diff或spectral做自动化兼容性检查。我习惯在CI/CD流水线里加一步——每次PR合并前自动检查契约变更是否兼容,不兼容的变更必须升级主版本号。

4.7 小结

服务契约是SOA的基石。契约优先原则让你在写代码之前就想清楚接口;WSDL和OpenAPI是两种主流的契约描述语言,各有适用场景;版本管理策略决定了你的服务能活多久。

嗯,这一章的内容就到这里。记住一句话:契约定得好,联调没烦恼。