swagger3新玩法

swagger工具的出现很好的解决了开发Java后台接口的文档化需求。它为前后端分离的基于API的对接提供了很好的保障,后台开发了哪些接口,出入参结构是怎样的,入参的约束,描述性的信息等等,都一目了然,我们还可以依赖它来做在线调试。所以说,对于前后端分离的项目,在开发和测试阶段,swagger在线文档可以很好的统一大家对Rest API的认识,尽管它的展示风格不是那么符合国人的审美。

关于swagger2

还记得当年前后端分离在企业开发中变得流行起来时,前后端开发人员在一起做一个新项目,在并行开发的情况下,最大的沟通问题就是在双方之间反复确认一些变化的出入参。因为接口是由后台开发提供的,根据需求和产品原型来梳理和定义API接口的重任便落在了后台开发人员肩上。准确来说,swagger在线文档并不等于API接口文档,因为swagger2的规范还没有形成,它更多是站在不成文的实现角度给开发人员提供了swagger注解API,这无疑增加了接口开发人员的负担,除了要应对变化无常的业务需求,还要手动写swagger注解来生成相关的在线文档,而这并不能第一时间成为各方认可的API文档标准。

openApi规范

从swagger3开始,swagger官方做出了权威的一举,制定了接口对接的统一规范------OpenAPI Specification(OAS)。这样swagger3的文档注解也相应的在命名和功能上做了调整,其实swagger3的注解现在以变成了一个中间产物,开发人员无须再以它为主导再去做繁杂的代码注释工作。新版的idea工具中包含了对openApi可视化的插件支持,如:

甚至,swagger官方贴心的考虑到手写swagger3注解的繁重任务,而推出了相关的生成器------Swagger Codegen。基于openApi的yaml文件就可以生成包含swagger注解的代码。因为它是一个规范,基于它的实现可以是各种后台编程语言,因此该生成器同时也是跨语言的,对于Java代码而言,我们自然期望生成包含spring mvc框架的api接口类文件。

swagger生成器

关于swagger生成器的使用以及功能的扩展,可以参考小卷的技术专栏《spring boot小卷生鲜电商项目实战》中介绍swagger生成器的笔记。这目前是一个单体的电商应用,考虑到模块多了,openApi的yaml文件会显得比较臃肿,好在openApi规范中可以采用$ref语法以json文档内容的节点定位形式来访问yaml文档中(或者外部文档)某一部分,这样我们自然就能够实现将一个很臃肿的openApi的yaml文件,把它拆分为按模块划分的多个小yaml文件。

这里的API接口和数据模型schema,我们按照模块以及是前台还是后台管理功能进行了细粒度的拆分,每个文件中我们可以维护pathsschemas下的内容,然后我们提供了一个template.ftl文件,借助freemarker模板帮我们生成整合后的内容,实际生成的临时文件的内容如下:

yaml 复制代码
openapi: 3.0.1
info:
  ...
paths:
  ...
  /mall-api/admin/categories:
    $ref: "./module/category-admin.yaml#/paths/~1mall-api~1admin~1categories"
  /mall-api/admin/categories/move:
    $ref: "./module/category-admin.yaml#/paths/~1mall-api~1admin~1categories~1move"
  /mall-api/admin/categories/{id}:
    $ref: "./module/category-admin.yaml#/paths/~1mall-api~1admin~1categories~1{id}"
  /mall-api/admin/product/updateSellStatus:
    $ref: "./module/product-admin.yaml#/paths/~1mall-api~1admin~1product~1updateSellStatus"
  /mall-api/admin/products:
    $ref: "./module/product-admin.yaml#/paths/~1mall-api~1admin~1products"
  /mall-api/portal/products:
    $ref: "./module/product.yaml#/paths/~1mall-api~1portal~1products"
  /mall-api/portal/product-detail/{id}:
    $ref: "./module/product.yaml#/paths/~1mall-api~1portal~1product-detail~1{id}"
  /mall-api/portal/product-latest/{categoryId}:
    $ref: "./module/product.yaml#/paths/~1mall-api~1portal~1product-latest~1{categoryId}"
components:
  schemas:
    CategoryItem:
      $ref: "./module/category.yaml#/components/schemas/CategoryItem"
    UserItem:
      $ref: "./module/user-admin.yaml#/components/schemas/UserItem"
    Token:
      $ref: "./module/user.yaml#/components/schemas/Token"
    UserInfo:
      $ref: "./module/user.yaml#/components/schemas/UserInfo"
    UserRegister:
      $ref: "./module/user.yaml#/components/schemas/UserRegister"
    PageQuery:
      $ref: "./module/common.yaml#/components/schemas/PageQuery"
    CategoryCondition:
      $ref: "./module/category-admin.yaml#/components/schemas/CategoryCondition"
    ...

以上的内容我们都不用关心,只需要关注各部分的yaml文件即可。

借助于swagger-codegen相关插件以及我们优化后的整合方案,我们就可以基于这些拆分出来的openApi子文件来一键生成后台代码:

生成的内容包含了我们已经实现的扩展功能:

  • 内置校验注解、自定义注解
  • 对同一DTO模型的新增、更新api的分组校验支持
  • 返回结果为通用的分页结果类型
  • 文件上传对Multipart类型的支持
  • 针对表单对象形式提交的swagger表单域校验支持
  • 其他

以上扩展功能我们对Swagger-Codegen的源码进行相关的改进以支持这些扩展,具体的改进请参考小卷的技术专栏《spring boot小卷生鲜电商项目实战》中相关笔记,这里不再赘述。

如前所述,开发人员开发后台Web层的工作从传统的Controller开发,包含了请求映射、参数(DTO)定义、各种注解(映射、校验、swagger),变成了api的声明与实现分离的模式,即只需要开发人员(前端甚至非开发人员)维护下openApi的yaml文件定义,然后交由代码生成器自动生成相关后台代码,后台开发人员只要写一个实现生成api接口的Controller组件即可。

示例

下面我们做一下演示。以后台的商品分类API开发为例,我们只需要先定义下相关的yaml文件内容:

yaml 复制代码
openapi: 3.0.1
info:
  title: 小卷生鲜OpenApi
  description: 小卷生鲜在线API文档
  version: 1.0.0
  termsOfService: https://edu.xiaojuan.com
  contact:
    name: xiaojuan
    url: https://edu.xiaojuan.com
    email: xiaojuan@162.com
servers:
  - url: 'http://localhost:8080'
    description: dev
paths:
  /mall-api/admin/categories:
    get:
      tags:
        - categoryAdmin
      summary: 查询分类列表
      description: 分页查询分类列表
      operationId: listCategories
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/CategoryCondition'
      x-pageResult: true
      responses:
        200:
          description: 查询成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CategoryInfo'
    post:
      tags:
        - categoryAdmin
      summary: 新增分类
      description: 新增一个分类
      operationId: addCategory
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CategoryAdd'
      responses:
        200:
          description: 新增成功
          content:
            application/json:
              schema:
                type: integer
                format: int64
    put:
      tags:
        - categoryAdmin
      summary: 更新分类
      description: 更新分类信息
      operationId: updateCategory
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CategoryUpdate'
      responses:
        200:
          description: 更新成功
        500:
          description: 更新失败
          x-errCode: '10001:参数校验失败(分类名称已存在、分类层级最多3级)'
  /mall-api/admin/categories/move:
    post:
      tags:
        - categoryAdmin
      summary: 分类拖拽
      description: 对分类进行拖拽调整分类或排序
      operationId: moveCategories
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CategoryMove'
      responses:
        200:
          description: 移动成功
  /mall-api/admin/categories/{id}:
    delete:
      tags:
        - categoryAdmin
      summary: 删除分类
      description: 按照id删除分类
      operationId: deleteCategory
      parameters:
        - name: id
          in: path
          description: 分类id
          schema:
            type: integer
            format: int64
      responses:
        200:
          description: 删除成功
components:
  schemas:
    CategoryCondition:
      description: 分类分页查询条件DTO类
      type: object
      allOf:
        - $ref: './common.yaml#/components/schemas/PageQuery'
      properties:
        name:
          description: 分类名称(模糊匹配)
          type: string
        pid:
          description: 分类父级id
          type: integer
          format: int64
    CategoryInfo:
      description: 分类信息DTO类
      type: object
      properties:
        id:
          description: 分类id
          type: integer
          format: int64
        pid:
          description: 父级分类id
          type: integer
          format: int64
        name:
          description: 分类名称
          type: string
        level:
          description: 分类层级
          type: integer
        orderNum:
          description: 排序
          type: integer
        createTime:
          description: 创建时间
          type: string
          format: date
        updateTime:
          description: 更新时间
          type: string
          format: date
    CategoryAdd:
      description: 分类新增DTO类
      type: object
      properties:
        name:
          description: 分类名称
          type: string
          x-validation: "@NotNull(message = MSG_CATEGORY_NAME_REQUIRED) @Size(min = 2, max = 5, message = MSG_CATEGORY_NAME_LENGTH_RANGE)"
        pid:
          description: 父级分类
          type: integer
          format: int64
          default: 0
    CategoryUpdate:
      description: 分类更新DTO类
      type: object
      properties:
        id:
          description: 分类id
          type: integer
          format: int64
          x-validation: "@NotNull(message = MSG_CATEGORY_ID_REQUIRED)"
        name:
          description: 分类名称
          type: string
    CategoryMove:
      description: 分类移动DTO类
      type: object
      properties:
        rootId:
          description: 要移动的分类id
          type: integer
          format: int64
          x-validation: "@NotNull(message = MSG_DRAG_CATEGORY_ID_REQUIRED)"
        targetId:
          description: 目标分类id
          type: integer
          format: int64
          x-validation: "@NotNull(message = MSG_DROP_TARGET_ID_REQUIRED)"
        type:
          description: 放置类型 0-之前 1-之中 2-之后
          type: integer
          x-validation: "@NotNull(message = MSG_DROP_TYPE_REQUIRED)"

然后执行生成器生成相关代码,最后写一个Controller来实现生成的接口即可:

java 复制代码
package com.xiaojuan.boot.web.controller.admin;

import ...

@RequiredArgsConstructor
@RestController
public class CategoryAdminController implements CategoryAdminApi {

    private final CategoryService categoryService;

    @SneakyThrows
    @Override
    public Long addCategory(CategoryAddDTO categoryAddDTO) {
        return categoryService.addCategory(categoryAddDTO);
    }

    @SneakyThrows
    @Override
    public void updateCategory(CategoryUpdateDTO updateDTO) {
        categoryService.updateCategory(updateDTO);
    }

    @Override
    public void deleteCategory(Long id) {
        categoryService.deleteCategory(id);
    }

    @SneakyThrows
    @Override
    public void moveCategories(CategoryMoveDTO body) {
        categoryService.moveCategories(body);
    }

    @Override
    public PageResultDTO<CategoryInfoDTO> listCategories(CategoryConditionDTO body) {
        return categoryService.listCategoriesByPage(body);
    }
}

内容看起来非常简洁,这就是接口与实现分离的好处,繁杂的注解声明由生成器帮我们在接口中做好了。

玩法升级

现在我们的工作重心就在openApi文档的声明和维护上,因为有了它,我们的后台代码也有了,swagger在线文档自然也有了,当然swagger在线文档显得不那么重要了,因为我们完全可以基于openApi的yaml内容,将其转换为我们想要的数据结构,持久化到数据库中,然后写自己的web端来维护这些数据,以方便非开发人员来维护在线API文档,可以将原始openApi的yaml文件经我们的数据结构导出为静态的html文档、markdown文件等形式,亦或者反向到处页面维护后的yaml格式,以便由swagger代码生成器来重新生成和同步与最新文档一致的后台代码。

这种不依赖于swagger注解的运行时元数据的形式,可以很方便的针对微服务分布式系统下各个微服务的API进行集中维护管理,可提供es搜索能力、针对指定环境的在线调试能力等等。

这就是我们所憧憬的玩法,相信也是不难实现的!

以上是给目前使用基于openAPI的swagger组件的小伙伴们提供的一点思路,也许朝着这个思路一点点去实现,就能实现一个企业内部真正所需要的"open"式API呢。大家加油!

相关推荐
IT界的奇葩14 小时前
Springfox、Swagger 和 Springdoc
swagger
前端搬砖小助手6 天前
开源 LLM 网关:APIPark 能做什么?
人工智能·开源·api·llm网关·api开放平台·api门户
一条晒干的咸魚8 天前
【Web前端】实现基于 Promise 的 API:alarm API
开发语言·前端·javascript·api·promise
Ttang2313 天前
后端——接口文档(API)
后端·api
Lojarro14 天前
接口文档的编写
api
Dynadot_tech15 天前
使用API有效率地管理Dynadot域名,列表形式查看账户whois联系人信息
网络·api·域名注册·dynadot
API小知识16 天前
Python中哪个框架最适合做API?
前端·后端·api
幂简集成16 天前
如何一步步获得文心一言API密钥
dubbo·api·文心一言
小小小妮子~21 天前
API:连接数字世界的桥梁
api