Springfox 解决在单一资源操作多个方法实现时生成 Swagger 文档的问题

在命名本文的标题都敲打了几分钟时间,问题很简单,然而用简短的一个标题完全描述出来却有点费事。在 Spring MVC 项目结合 Springfox 来生成 Swagger API 文档时,如果一个资源操作因为请求参数的不同而映射到多个 controller 方法,那么 Swagger 可能只能生成某一个 API 条目,其余都被忽略。至于为什么说是 "可能", 可能正好未遵循命名规范而躲过了这一劫。由此引出

我们的问题

我们这里用了资源操作一词,它包含了两部分信息: 资源与操作,比如 /users/{userId} 是资源,而发生在其上的 HTTP 各种方法,如 POST, GET, PUT, DELETE 等就是操作。而 Spring MVC 中允许我们针对不同的查询参数把相同的资源操作映射到不同的 controller 方法上,也是为了保持逻辑上更为清晰。

比如下面的例子路由配置的例子

GET /users/{userId}     UserController.getUserInfo                                    //默认
GET /users/{userId}     UserController.getUserInfo                                   //当有 ?source=file 时
GET /users/{userId}     CloudUserController.getUserInfoFromCloud       //当有 ?source=cloud 时

看到上面资源与操作完全相同,仅仅因为 source 查询参数的不同而映射到三个 controller 方法。用代码体现如下图

假设该应用的 Web 上下文是 swagger-test, 那么启动应用后,通过 URL http://localhost:8080/swagger-test/swagger-ui.html 访问,并且把所有 controller 下的API 都展开(看 controller 后显示的向下箭头就代表着展开了所有的 API) 

从上图中看到,我们定义了三个 GET /users/{userId} 的 API, Swagger 只显示了一个,CloudUserController 中什么也没有,Swagger 只会显示它找到的最后一个。

这是为什么呢?事情要从源头上找,也就是 swagger-ui.html 显示的内容来自于这里的 http://localhost:8080/swagger-test/v2/api-docs, 打开来看, 在 paths 下只有 /users/{userId} 一个对象

它组织 API 的方式是 资源/操作, 所以前面想要用参数来区分的三个 API,它们在 /v2/api-docs 都表示为

资源名都是 /users/{userId}, 操作也都是 get,如此 Swagger 在生成 JSON 文档时以上三个 API 使用相同的 JSON key, 造成相互覆盖,只有最后面那个 API 保留了下来。

解决办法

由 Swagger 组织 资源/操作 的方式受到启发,其实只要做点变通就能让 Swagger 生成所有定义的 API,如果阅读到这儿的读者大概也猜到了。对啦,就是修改路径中的变量名(path variable name),我们不能总是用 {userId}, 比如另两个改成 {user-id}, {user_id}, 这种做法更为混乱,那么下一个怎么办呢?倒不如简单了事,用序号去区分,如 userId$1, userId$2, 再多都能应会。

实际上路径变量的改名是对 Swagger 的欺骗行为,因为本质上, 从 RESTful 资源的概念来讲 /users/{userId} 与 /users/{userId$1} 是没有区别。为了达到欺骗的效果,我们只能合理的不去遵循 Java 的变量命名规则了。前面只要区分出不同就行,也可以用 userId1, userId2 的方式,我之所以选用 userId$1, userId$2 是效仿了 Java 在对付匿名类生成 class 文件名的做法。

回到前面的例子,修改后的代码如下

UserController

CloudUserController

注意 @PathVariable 中的变量名也要作相应的修改。

重启服务,再次浏览 http://localhost:8080/swagger-test/swagger-ui.html, 是下面的情景

所有的 API 都展露无余,如果通过 swagger-ui.html 来直接对 API 进行测试的话,也都没问题,会命中各自对应的 controller 方法。

查看一下相应的 http://localhost:8080/swagger-test/api-docs

三个 API 由于路径中变量名的不同,它们有了各自独立的 JSON key, 才能在一个地方被平行的容得下。

进一步探讨

目前我们是已知有三个 /users/{userId} API 的 controller 方法实现,假如一个团队中其他成员又用一个不同的请求参数,或其他的方式对 /users/{userId} 又加了一个新的实现方法,会造成某一个 API 在  Swagger 文档中缺失。因此我们最好能有一种机制让 Swagger 生成 /v2/api-docs 文档中发现有相同的资源/操作发生时给予警示。

http://localhost:8080/swagger-test/v2/api-docs 文档的生成是由 @EnableSwagger2 开启的,应该从它入手。看了下源代码,这里先列一个线索

Spring MVC 的所有 API 会在 Spring 启动的时候被扫描并分组存入到 DocumentationCache 缓存中去,就是上面的 scanned 变量。这里面会保存所有 API 条目,不会按 资源/操作 进行去重,进到下一步

然后在访问 http://localhost:8080/swagger-test/v2/api-docs 会进入到 springfox.documentation.swagger2.web.Swagger2Controller 的 getDocumentation(@RequestParam(value = "group", required = false) String swaggerGroup, HttpServletRequest servletRequest) 方法。

/v2/api-docs 根据 group 从 documentationCache 中取出 Spring 启动时扫描到的所有 API, 此时取到的 documentation 仍然是包含所有 API (含重复的 资源/操作)。关键代码出现在上面的高亮行

Swagger swagger = mapper.mapDocumentation(documentation);

mapper 的实现类是 ServiceModelToSwagger2MapperImpl, 它的方法 mapDocumentation(Documentation from)  中的行

swagger.setPaths(mapApiListings(from.getApiListings()));

将会把重复的资源/操作 过滤掉,mapApiListings(Multimap<String, ApiListing> apilistings) 的实现在抽象类 ServiceModelToSwagger2Mapper 中

上面代码高亮行 api.getPath() 是 key, 对应的 Path 包括所有允许的操作,按顺序是 get, head, post, put, delete, options, 和 patch.

以上的高亮行,path.set(...) 设置某个路径的允许的操作,从这个点上可以发出警告。如果在 path.set("get", operation) 前,我们检测到 path.get("get") 不为 null 时说明有重复的 资源/操作 定义,给出警告信息。

具体的做法参考,ServiceModelToSwagger2MapperImpl 是一个用 @Component 定义的 Spring Bean, 我们可以创建它的子类,并声明为 @Primary, 从而替换掉 Swagger2Controller 中的 ServiceModelToSwagger2Mapper 依赖。然后把前面的两个方法

protected Map<String, Path> mapApiListings(Multimap<String, ApiListing> apiListings)
private Path mapOperations(ApiDescription api, Optional<Path> existingPath)

置换掉就行了。

本文链接 https://yanbin.blog/springfox-single-resource-operation-multiple-methods-swagger-documentation/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

4 Comments
Inline Feedbacks
View all comments
xkcoding
4 years ago
Reply to  anonymous

+1,通过配置 new Docket().enableUrlTemplating(true) 解决,感谢