契约式编程:让微服务别“各说各话”

58 阅读3分钟

在微服务的江湖里,最怕的不是 bug,而是我说的你听不懂,你写的我调用不了
就像点外卖:我喊的是“加份辣椒”,送来的却是“多加一瓶可乐”。这就是契约不一致

于是,“契约式编程(Contract First)”横空出世。它的核心思想很简单:先立字为证,后各自表演


一、什么是契约式编程?

契约(Contract)就是一份协议。在微服务里,通常表现为:

  • 统一接口定义(Java Interface + SpringMVC 注解)
  • 服务提供方 Controller 实现接口
  • 服务调用方 Feign Client 继承接口

这样,接口就是“婚前协议”,大家必须遵守,谁违约编译器第一个跳出来打脸。

举个栗子 🌰

// 统一接口(契约)
@RequestMapping("/user")
public interface UserApi {

    @GetMapping("/{id}")
    UserDTO getUserById(@PathVariable("id") Long id);
}

服务提供方(Controller):

@RestController
public class UserController implements UserApi {

    @Override
    public UserDTO getUserById(Long id) {
        // 从数据库查用户
        return new UserDTO(id, "Neo");
    }
}

服务调用方(Feign Client):

@FeignClient(name = "user-service")
public interface UserFeignClient extends UserApi {}

此时,调用方只要注入 UserFeignClient,调用方法,就像调用本地接口一样丝滑。
大家共享一份契约,不会出现“你说 GET,我写 POST”的笑话。


二、为什么需要契约?

1. 避免重复劳动

过去的日子里,接口要写三份:

  • 提供方 Controller
  • 调用方 Feign Client
  • 文档(Swagger / Markdown)

契约式编程后:只写一份,三方通用。文档还能通过 OpenAPI 自动生成。

2. 保证一致性

接口改动了?编译直接报错。
就像结婚时签的合同:想悔婚?不好意思,法官(编译器)说不行。

3. 提升开发效率

减少沟通成本,接口即文档,前后端都能看得一清二楚。


三、契约式编程的坑

当然,江湖没有银弹,契约也不是万金油。

  1. 耦合性增强
    契约 jar 一改,所有依赖方都要升级。不然就编译红线。
    → 解决办法:版本化管理,用 api-1.0.0.jarapi-2.0.0.jar 做兼容。
  2. 接口演进难
    服务多了以后,契约 jar 的发布频率会飞起。
    → 解决办法:借助 Maven 私服 / Git submodule 管理版本。
  3. 灵活性下降
    有时候调用方并不需要 Controller 全部方法,但继承接口后就都带上了。
    → 解决办法:拆分更细粒度的接口(单一职责)。

四、最佳实践

  1. 独立契约模块
    单独抽一个 api 模块,供服务提供方和调用方依赖,避免循环依赖。

    user-api
    └── UserApi.java
    
    user-service
    └── 依赖 user-api
    
    order-service
    └── 依赖 user-api
    
  2. 统一发布到私服
    所有契约 jar 统一发布到 Maven 私服(如 Nexus/Artifactory),版本化管理。

  3. 结合 OpenAPI/Swagger
    契约定义好之后,用 SpringDoc/Knife4j 自动生成 API 文档,减少手工写文档。

  4. 配合测试契约(Contract Test)
    消费方编写基于契约的测试用例,保证提供方真的实现了契约,而不是“写了个摆设”。


五、总结

契约式编程其实就是:
👉 “先立契约,后各自实现”
👉 “一处定义,多处受益”

它解决的是微服务接口 重复定义、容易不一致 的老毛病。

当然,它带来了一些耦合和版本管理的挑战,但在一个团队内部,尤其是接口频繁变动、调用方众多的场景里,它绝对是提升效率的神器。


一句收尾

在微服务的江湖里,契约式编程就像是大家在开打之前,先立个帮规:
拳脚随意,但不得打脸
——接口写法随意,但契约必须一致。