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服务、客户端代码
- 变更可追溯:契约文件纳入版本控制,每次改动都有记录
具体怎么做?我建议的流程是这样的:
- 业务分析:搞清楚要提供什么能力
- 契约编写:用OpenAPI或WSDL定义接口
- 契约评审:相关团队一起过一遍,确认没问题
- Mock生成:根据契约生成Mock服务,让调用方先开发
- 实现开发:服务提供方按照契约写代码
- 契约测试:用契约文件做自动化测试,确保实现符合契约
关键点:契约文件是"真理之源"(Source of Truth)。任何接口变更,必须先改契约文件,再改代码。顺序反了,契约优先就成了一句空话。
我记得有一次,团队里一个小伙子急着上线,偷偷改了接口没更新OpenAPI文件。结果下游团队按照旧契约调了三天,最后发现是接口变了。那次之后,我们在CI流水线里加了个检查:如果契约文件和代码不一致,构建直接失败。
嗯,这里要注意:契约优先不是银弹。如果你的业务变化极快,一天改三次接口,那契约优先反而会成为负担。这种情况下,我建议先快速迭代,等接口稳定了再补契约。但话说回来,如果接口一天改三次,你确定你的架构没问题?
工具推荐:
- OpenAPI:用Swagger Editor或Stoplight Studio编写
- 契约测试:用Spring Cloud Contract或Pact
- Mock服务:用WireMock或MockServer
最后总结一句:服务契约不是文档,是代码。把它当成一等公民对待,你的微服务架构会省心很多。