📦 本文基于的完整项目源码:https://github.com/meirongdev/shop

🏷️ 当前文章对应的代码版本:main

背景#

在一个典型的微服务架构中,每个服务都独立维护自己的 API 文档。随着服务数量增长,前端开发者和第三方集成方常常面临一个痛点:

我需要去哪里找某个接口的文档?

传统的解决方案是让开发者记住每个服务的地址,或者维护一个手动的聚合页面。但这些方案都存在明显的问题:容易过时、维护成本高、无法与 CI/CD 集成。

本文以一个云原生电商平台的实际案例,记录我们当前如何通过 Spring Cloud Gateway MVC + SpringDoc 维护一套相对省事的 OpenAPI 聚合方案。


架构概览#

我们的平台采用 Gateway + Thin BFF + Domain Service 三层架构:

Client
  └→ api-gateway:8080 (Spring Cloud Gateway MVC, JWT validation, rate limiting)
       ├→ /auth/**             → auth-server
       ├→ /buyer/**            → buyer-portal (Kotlin + Thymeleaf SSR)
       ├→ /seller/**           → seller-app (KMP WASM SPA)
       ├→ /api/buyer/**        → buyer-bff (aggregates domain services)
       ├→ /api/seller/**       → seller-bff (aggregates domain services)
       ├→ /api/loyalty/**      → loyalty-service
       ├→ /api/activity/**     → activity-service
       ├→ /api/webhook/**      → webhook-service
       └→ /api/subscription/** → subscription-service

共包含:

  • 1 个 API Gateway(路由聚合 + 安全网关)
  • 2 个 BFF 服务(Buyer / Seller Backend-for-Frontend)
  • 11+ 个 Domain Service(Profile、Promotion、Wallet、Order、Search…)

每个服务都独立部署,拥有自己的 /v3/api-docs 端点。Gateway 负责将所有 API 规范统一暴露给 Swagger UI。


实现方案:职责分工#

OpenAPI 聚合文档的核心思路是各服务独立生成规范,Gateway 统一聚合展示。下面以 buyer-bff 为例,分别说明下游服务和 Gateway 需要做什么。

一、下游服务(BFF / MS)需要做什么#

下游服务只需完成三件事:引入依赖、声明 OpenAPI 元信息、确保 /v3/api-docs 可访问。

1. 引入 SpringDoc 依赖#

pom.xml 中添加:

<!-- services/buyer-bff/pom.xml -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>

版本号由父 POM 统一管理(Spring Boot BOM 继承)。

2. 声明 OpenAPI 元信息#

创建一个 @Configuration 类,定义 OpenAPI Bean,描述服务的基本信息和安全方案:

// services/buyer-bff/src/main/java/.../config/OpenApiConfig.java
@Configuration(proxyBeanMethods = false)
public class OpenApiConfig {

    private static final String BEARER_AUTH = "BearerAuth";

    @Bean
    public OpenAPI serviceOpenApi() {
        return new OpenAPI()
                .info(new Info()
                        .title("Buyer BFF API")
                        .description("买家端聚合服务接口,面向 Buyer Portal / App 的 BFF 层。")
                        .version("v1"))
                // 声明安全方案:Swagger UI 会提示用户输入 JWT
                .addSecurityItem(new SecurityRequirement().addList(BEARER_AUTH))
                .components(new Components()
                        .addSecuritySchemes(BEARER_AUTH, new SecurityScheme()
                                .type(SecurityScheme.Type.HTTP)
                                .scheme("bearer")
                                .bearerFormat("JWT")
                                .description("通过认证中心获取 JWT 后填入此处。")));
    }
}

关键点

  • .addSecurityItem() 让 Swagger UI 的每个接口右上角显示 🔒 锁图标,提醒用户需要认证
  • .components().addSecuritySchemes() 定义认证格式,Swagger UI 的 Authorize 按钮会根据此配置弹出正确的输入框
  • proxyBeanMethods = false 通常有助于减少不必要的配置类代理开销

3. 无需额外 application.yml 配置#

按我这次在仓库里的做法,SpringDoc 默认生效,/v3/api-docs 端点自动可用,不需要额外再写 SpringDoc 配置。

验证方式:直接访问该服务的 /v3/api-docs,应返回 OpenAPI 3.0 JSON 规范文档。

总结:下游服务侧通常只需要 1 个依赖 + 1 个配置类,代码量也比较可控。

二、API Gateway 需要做什么#

Gateway 侧需要完成四件事:引入依赖、配置文档路由、配置 Swagger UI 分组、修复静态资源路由。

1. 引入 SpringDoc 依赖#

<!-- services/api-gateway/pom.xml -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>

2. 声明 Gateway 自身的 OpenAPI 元信息#

// services/api-gateway/src/main/java/.../config/OpenApiConfig.java
@Configuration(proxyBeanMethods = false)
public class OpenApiConfig {

    @Bean
    public OpenAPI gatewayOpenApi() {
        return new OpenAPI()
                .info(new Info()
                        .title("Shop Platform API Portal")
                        .description("微服务聚合 API 门户,通过网关统一访问各服务 OpenAPI 文档。")
                        .version("v1")
                        .contact(new Contact().name("Meirong Dev Team")));
    }
}

3. 配置文档路由转发#

application.yml 中,为每个下游服务配置两条规则:

a) 路由转发 — 将 Gateway 的 /v3/api-docs/{service} 请求代理到对应下游服务的 /v3/api-docs

spring:
  cloud:
    gateway:
      server:
        webmvc:
          routes:
            - id: api-docs-buyer-bff
              uri: ${BUYER_BFF_URI:http://buyer-bff:8080}
              predicates:
                - Path=/v3/api-docs/buyer
              filters:
                - RewritePath=/v3/api-docs/buyer, /v3/api-docs

            - id: api-docs-activity
              uri: ${ACTIVITY_SERVICE_URI:http://activity-service:8080}
              predicates:
                - Path=/v3/api-docs/activity
              filters:
                - RewritePath=/v3/api-docs/activity, /v3/api-docs
            # ... 每个服务一条路由

b) Swagger UI 分组 — 将各服务文档地址注册到下拉选择器:

springdoc:
  swagger-ui:
    enabled: true
    path: /swagger-ui.html
    urls:
      - name: "[BFF] Buyer"
        url: /v3/api-docs/buyer
      - name: "[BFF] Seller"
        url: /v3/api-docs/seller
      - name: "[MS] Activity"
        url: /v3/api-docs/activity
      # ... 每个服务一行
    urls-primary-name: "[BFF] Buyer"
    persist-authorization: true

4. 修复 Spring Cloud Gateway MVC 的静态资源路由#

这是最容易踩坑的地方。Spring Cloud Gateway MVC 默认将所有请求当作路由处理,导致 Swagger UI 的静态资源(JS/CSS)被当作 API 请求转发,返回 404。

需要手动注册 WebMvcConfigurer,让 Spring MVC 直接处理 Swagger UI 静态资源:

// services/api-gateway/src/main/java/.../config/OpenApiConfig.java
@Bean
public WebMvcConfigurer swaggerUiResourceHandler() {
    return new WebMvcConfigurer() {
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/swagger-ui/**")
                    .addResourceLocations("classpath:/META-INF/resources/webjars/swagger-ui/");
            registry.addResourceHandler("/webjars/**")
                    .addResourceLocations("classpath:/META-INF/resources/webjars/");
        }
    };
}

如果不加这段代码,访问 /swagger-ui.html 会返回 404,因为请求被 Gateway 路由拦截并尝试转发到一个不存在的路由。

三、新增一个服务的完整步骤#

当你的团队新增一个微服务时,接入 OpenAPI 聚合文档只需:

步骤 操作位置 具体操作
1 新服务 pom.xml 添加 springdoc-openapi-starter-webmvc-ui 依赖
2 新服务 Java 代码 创建 OpenApiConfig.java,声明 OpenAPI Bean
3 Gateway application.yml 添加一条 api-docs-{service} 路由
4 Gateway application.yml springdoc.swagger-ui.urls 中添加一行

步骤 1-2 是每个服务的职责,步骤 3-4 是 Gateway 的职责。是否需要重启,取决于你的配置刷新方式;至少改动面本身是比较收敛的。

三、请求数据流#

当用户在 Swagger UI 中选择 [MS] Activity 时,完整的请求链路如下:

浏览器
  │
  ├ ① 加载页面: GET /swagger-ui.html
  │     └→ Gateway 本地静态资源(不转发)
  │
  ├ ② 获取文档列表: GET /v3/api-docs/activity
  │     └→ Gateway 匹配路由 api-docs-activity
  │          └→ RewritePath → GET http://activity-service:8080/v3/api-docs
  │               └→ activity-service 返回 OpenAPI 3.0 JSON
  │                    └→ Gateway 透传回浏览器 → Swagger UI 渲染文档
  │
  └ ③ 用户调试接口: POST /api/activity/checkin
        └→ Gateway 匹配业务路由 activity-api
             └→ StripPrefix → POST http://activity-service:8080/activity/checkin
                  └→ activity-service 处理请求
                       └→ Gateway 透传回浏览器

关键设计

  • 步骤 ① 的静态资源由 Gateway 本地处理(不转发)
  • 步骤 ② 的文档获取由 Gateway 代理转发到下游服务
  • 步骤 ③ 的 API 调用由 Gateway 正常路由到下游服务
  • 三者路径空间互不冲突,各司其职

四、分组命名约定#

我们采用 [前缀] 服务名 的命名方式,在下拉菜单中清晰地区分不同层级:

前缀 含义 示例
[BFF] Backend-for-Frontend 聚合层 [BFF] Buyer, [BFF] Seller
[MS] Microservice 领域服务 [MS] Order, [MS] Wallet
无前缀 网关自身 Gateway Self

这种命名方式能让新加入团队的开发者更快识别服务的职责层级。


多环境管理:SpringDoc 开关与生产环境安全#

在生产环境中直接暴露 Swagger UI,通常不是我想保留的默认状态。这里采用的做法是通过环境变量控制开关,配合 Kustomize / Helm 按环境注入,实现非生产环境可用、生产环境默认关闭。

一、默认关闭的 application.yml 配置#

springdoc:
  api-docs:
    enabled: ${SPRINGDOC_ENABLED:false}
  swagger-ui:
    enabled: ${SPRINGDOC_ENABLED:false}
    path: /swagger-ui.html
    urls:
      - name: "[BFF] Buyer"
        url: /v3/api-docs/buyer
      # ... 其他服务
    urls-primary-name: "[BFF] Buyer"

关键设计${SPRINGDOC_ENABLED:false} 默认值为 false,即 不设置环境变量时,Swagger UI 和文档 API 默认都不可访问

二、非生产环境开启:Kustomize 注入#

# overlays/dev/kustomization.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-gateway
spec:
  template:
    spec:
      containers:
        - name: api-gateway
          env:
            - name: SPRINGDOC_ENABLED
              value: "true"

生产环境(overlays/prod/不设置该环境变量,或者显式设为 "false"

三、本地开发环境#

通过 Makefile 或 docker-compose 注入:

export SPRINGDOC_ENABLED=true

四、效果对照#

环境 SPRINGDOC_ENABLED Swagger UI /v3/api-docs
Dev / Stg true ✅ 可访问 ✅ 可访问
Prod false(默认) ❌ 404 ❌ 404

五、进阶:生产保留文档 API 但禁用 UI#

某些团队在生产环境仍然需要 /v3/api-docs 供 contract testing(Pact / Specmatic)或 API 治理工具使用,但需要禁止人类通过浏览器访问。此时可以分离两个开关:

springdoc:
  api-docs:
    enabled: true                          # 文档 API 始终可用
  swagger-ui:
    enabled: ${SWAGGER_UI_ENABLED:true}    # UI 可通过环境变量关闭

生产环境设置 SWAGGER_UI_ENABLED=false

  • /v3/api-docs/* ✅ 仍可访问(供自动化 CI/CD 流程使用)
  • /swagger-ui.html ❌ 返回 404(禁止人类通过页面浏览)

这种分离模式兼顾了安全合规和自动化需求。


使用效果#

统一入口#

开发者和测试人员只需访问一个地址:

http://127.0.0.1:18080/swagger-ui.html

通过右上角的 Select a definition 下拉菜单切换不同的服务,即可查看和测试对应的 API。

在线调试#

Swagger UI 支持在线测试接口:

  1. 选择目标服务
  2. 点击 🔓 Authorize 按钮,输入 JWT token(Bearer <token>
  3. 展开接口,点击 Try it out
  4. 填写参数,点击 Execute
  5. 查看响应体和生成的 curl 命令

这种能力在开发和联调阶段会方便不少,至少能减少在 Swagger UI 和 Postman 等工具之间来回切换。

获取 Token 的快速认证流程#

# 获取 JWT token
curl -X POST http://127.0.0.1:18080/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"buyer.demo","password":"password"}'

# Response: {"data":{"token":"eyJhbGci..."}}

复制 token 值,在 Swagger UI 中粘贴即可。由于配置了 persist-authorization: true,token 在页面刷新后依然有效。


优势总结#

维度 传统方案 本方案
文档一致性 手动维护,较容易漂移 文档跟代码一起变更,漂移更少
查找成本 需要记住各服务地址 统一入口,下拉切换
测试效率 通常还要切换外部工具 可直接在线试调用
运维成本 常常需要单独维护文档站 复用 Gateway 入口,额外组件更少
扩展性 新增服务时要额外同步文档页 新增服务时补路由和分组即可

未来演进方向#

  • 自动化注册:服务启动时自动向 Gateway 注册自己的 OpenAPI 端点(类似服务发现)
  • 版本管理:同一服务的多版本 API 并行展示(v1 / v2)
  • 权限控制:按用户角色过滤可见的 API 分组
  • contract testing 集成:将 OpenAPI 规范直接对接到 Pact / Specmatic 等 contract testing 框架
  • OpenAPI 3.1 升级:支持 JSON Schema 2020-12 等最新规范

小结#

通过 Spring Cloud Gateway MVC + SpringDoc 的组合,我们把 OpenAPI 聚合入口收敛到了 Gateway。至少在当前项目里,这减少了文档查找和联调切换成本,也为后续的 contract testing、版本管理和权限控制预留了位置。

如果你的团队也在用 Spring 生态构建微服务,这个思路可以作为一个可参考的起点。


技术栈版本:Java 25 · Spring Boot 3.5.11 · Spring Cloud 2025.0.1 · SpringDoc 2.x

💻 完整项目:https://github.com/meirongdev/shop