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,我们按照模块以及是前台还是后台管理功能进行了细粒度的拆分,每个文件中我们可以维护paths
和schemas
下的内容,然后我们提供了一个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呢。大家加油!