转载

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 方法。用代码体现如下图

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

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

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

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

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

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

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

"paths": {
    "/users/{userId}": {
        "get": {
            ....

资源名都是 /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

@RestController
@RequestMapping("/users")
public class UserController {
 
    @GetMapping(value = "/{userId}")
    public Map<String, Object> getUserInfo(@PathVariable("userId") Integer userId) {
        return ImmutableMap.of("UserId", userId, "Source", "DB");
    }
 
    @GetMapping(value = "/{userId$1}", params = {"source=file"})
    public Map<String, Object> getUserInfoFromFile(@PathVariable("userId$1") Integer userId) {
        return ImmutableMap.of("UserId", userId, "Source", "File");
    }
}

CloudUserController

@RestController
@RequestMapping("/users")
public class CloudUserController {
 
    @GetMapping(value = "/{userId$2}", params = {"source=cloud"})
    public Map<String, Object> getUserInfoFromCloud(@PathVariable("userId$2") Integer userId) {
        return ImmutableMap.of("UserId", userId, "Source", "Cloud");
    }
}

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

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

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

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

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

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

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

进一步探讨

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

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

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

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

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

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

/v2/api-docs 根据 groupdocumentationCache 中取出 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

  protected Map<String, Path> mapApiListings(Multimap<String, ApiListing> apiListings) {
    Map<String, Path> paths = newTreeMap();
    for (ApiListing each : apiListings.values()) {
      for (ApiDescription api : each.getApis()) {
        paths.put(api.getPath(), mapOperations(api, Optional.fromNullable(paths.get(api.getPath()))));
      }
    }
    return paths;
  }

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

  private Path mapOperations(ApiDescription api, Optional<Path> existingPath) {
    Path path = existingPath.or(new Path());
    for (springfox.documentation.service.Operation each : nullToEmptyList(api.getOperations())) {
      Operation operation = mapOperation(each);
      path.set(each.getMethod().toString().toLowerCase(), operation);
    }
    return path;
  }

以上的高亮行, 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://unmi.cc/springfox-single-resource-operation-multiple-methods-swagger-documentation/
正文到此结束
Loading...