专栏名称: Java知音
专注于Java,推送技术文章,热门开源项目等。致力打造一个有实用,有情怀的Java技术公众号!
目录
相关文章推荐
王者荣耀  ·  国色锦绣新版本2月13日开启!【西施-续相思 ... ·  7 小时前  
叶子猪游戏网  ·  三法都能秒伤百万!这枚大话2水炮为何如此暴力 ·  3 天前  
叶子猪游戏网  ·  把梦幻西游当单机玩,这个天科五庄真有想法! ·  3 天前  
51好读  ›  专栏  ›  Java知音

Swagger天天用,但它背后的实现原理很多人都不知道!

Java知音  · 公众号  ·  · 2020-12-16 09:45

正文

来源:http://r6d.cn/SVuZ

先说一说Springfox和Swagger的关系

Swagger 是一种规范。

springfox-swagger 是基于 Spring 生态系统的该规范的实现。

springfox-swagger-ui 是对 swagger-ui 的封装,使得其可以使用 Spring 的服务。

由于工作中遇到需要基于 Swagger Json 做一些处理,但 Swagger Json 的格式不是那么满足需求。

本文springfox-swagger版本号: 2.6.0

本文从问题出发,探索涉及的源码。

1. GET 方法的参数对象

第一个问题,当方法是GET请求,但参数是一个自定义 Object,在展示时(生成的JSON)是不包括本 Object 描述的。所以,就要看看什么时候会生成这些 Model 的描述。

万事有始有终,SpringFox始就在: springfox.documentation.spring.web.plugins 下的 DocumentationPluginsBootstrapper

该类实现了 SmartLifecycle 接口,实现此接口且通过 @Component 注入到容器的bean, 容器初始化后会执行 start() 方法.

@Component
public class DocumentationPluginsBootstrapper implements SmartLifecycle {

接着看 start 方法

@Override
public void start() {
    if (initialized.compareAndSet(falsetrue)) {
        // 拿到 DocumentationPlugin 插件
        List plugins = pluginOrdering()
            .sortedCopy(documentationPluginsManager.documentationPlugins());
        for (DocumentationPlugin each : plugins) {
            //获取文档类型
            DocumentationType documentationType = each.getDocumentationType();
            if (each.isEnabled()) {
                // 启用则扫描生成文档
                scanDocumentation(buildContext(each));
            } 
        }
    }
}

调用了 buildContext 方法, 通过 Docket 对象创建 DocumentaionContext 对象

private DocumentationContext buildContext(DocumentationPlugin each) {
    return each.configure(this.defaultContextBuilder(each));
}

再往下走

private DocumentationContextBuilder defaultContextBuilder(DocumentationPlugin each) {
    DocumentationType documentationType = each.getDocumentationType();
    // 获取所有的RequestHnadler
    List requestHandlers = FluentIterable.from(this.handlerProviders).transformAndConcat(this.handlers()).toList();
    return this.documentationPluginsManager.createContextBuilder(documentationType, this.defaultConfiguration).requestHandlers(requestHandlers);
}

handlerProviders RequestHandlerProvider 接口,实现类是 WebMvcRequestHandlerProvider ,其中 requestHandlers 方法会接收Spring中的所有请求映射。

接着看 DocumentationContextBuilder 的构造过程: documentationPluginsManager.createContextBuilder

public DocumentationContextBuilder createContextBuilder(DocumentationType documentationType,
                                                        DefaultConfiguration defaultConfiguration)
 
{
  return defaultsProviders.getPluginFor(documentationType, defaultConfiguration)
      .create(documentationType)
      .withResourceGroupingStrategy(resourceGroupingStrategy(documentationType));
}

defaultsProviders 是也是一个插件接口 DefaultsProviderPlugin ,只有一个实现类 DefaultConfiguration ,不过该类未使用 @Compoent 注解,所以需要给一个替换值 defaultConfiguration ,也就是 DefaultConfiguration 。在看 DefaultConfiguration create 方法:

@Override
public DocumentationContextBuilder create(DocumentationType documentationType) {
  return new DocumentationContextBuilder(documentationType)
          .operationOrdering(defaults.operationOrdering())
          .apiDescriptionOrdering(defaults.apiDescriptionOrdering())
          .apiListingReferenceOrdering(defaults.apiListingReferenceOrdering())
          .additionalIgnorableTypes(defaults.defaultIgnorableParameterTypes())
          .rules(defaults.defaultRules(typeResolver))
          .defaultResponseMessages(defaults.defaultResponseMessages())
          .pathProvider(new  RelativePathProvider(servletContext))
          .typeResolver(typeResolver)
          .enableUrlTemplating(false)
          .selector(ApiSelector.DEFAULT);
}

这里在给 DocumentationContextBuilder 设置相关参数,至此拿到了 DocumentationContextBuilder

回到上面提到的 buildContext 方法, defaultContextBuilder 方法执行完毕,接下来是 configure

return each.configure(this.defaultContextBuilder(each));

DocumentationPlugin 只有一个实现类 Docket ,到这里就有点熟悉了。 Docket 对象是我们开发人员在外部通过 @Bean 来创建的,而外部赋值的对象值,最终都会整合到 DocumentationContext 。这里的 config 就是在二次赋值。可以看一下一般自己定义的 Docket 对象。

public class SwaggerConfig {
    ...
    @Bean
    public Docket docket() {
        ...
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName(SWAGGER_GROUP)
                .apiInfo(new ApiInfoBuilder().title("xx").version("1.0.0").build())
                ......
                .select()
                .apis(basePackage("xxx"))
                .paths(PathSelectors.any())
                .build();
    }
}

到这里实际只设置了默认的参数。但接口,定义,模型等关键信息等都未初始化。

回到最初 start() , 看看 scanDocumentation(buildContext(each)) scanDocumentation

private void scanDocumentation(DocumentationContext context) {
  scanned.addDocumentation(resourceListing.scan(context));
}

其中 scan 位于 ApiDocumentationScanner

public Documentation scan(DocumentationContext context) {
  ApiListingReferenceScanResult result = apiListingReferenceScanner.scan(context);
  ...
  Multimap apiListings = apiListingScanner.scan(listingContext);
  ...

apiListingReferenceScanner.scan 位于 ApiListingReferenceScanner

public ApiListingReferenceScanResult scan(DocumentationContext context) {
  ...
  // 接口选择器 在构建Docket时通过.select()默认配置 
  ApiSelector selector = context.getApiSelector();
  // 根据package路径(一般)或注解区分, 过滤筛选掉不符规则的 RequestHandler 接口
  Iterable matchingHandlers = from(context.getRequestHandlers())
      .filter(selector.getRequestHandlerSelector());
  for (RequestHandler handler : matchingHandlers) {
    // 接口分组 resourceGroup = Controller,RequestMapping = method
    ResourceGroup resourceGroup = new ResourceGroup(handler.groupName(),
        handler.declaringClass(), 0);
    RequestMappingContext requestMappingContext
        = new RequestMappingContext(context, handler);
    resourceGroupRequestMappings.put(resourceGroup, requestMappingContext);
  }
  return new ApiListingReferenceScanResult(asMap(resourceGroupRequestMappings));
}

到这已经拿到了所有接口并进行了分组,其中ArrayListMultimap是 guava 的方法。

再回到 ApiDocumentationScanner scan 方法,看 apiListingScanner.scan

public Multimap scan(ApiListingScanningContext context) {
  ...
  for (ResourceGroup resourceGroup : sortedByName(requestMappingsByResourceGroup.keySet())) {
    ...
    for (RequestMappingContext each : sortedByMethods(requestMappingsByResourceGroup.get(resourceGroup))) {
      // 循环Controller下的所有接口的实例对象, 拿到该接口的所有Model
      models.putAll(apiModelReader.read(each.withKnownModels(models)));
      apiDescriptions.addAll(apiDescriptionReader.read(each));
    }

each.withKnownModels 是复制对象,主要看 apiModelReader.read ,读取该接口的 Model 信息。

public Map read(RequestMappingContext context) {
 // 忽略的class
  Set ignorableTypes = newHashSet(context.getIgnorableParameterTypes());
  Set modelContexts = pluginsManager.modelContexts(context);
  Map modelMap = newHashMap(context.getModelMap());
  for (ModelContext each : modelContexts) {
    markIgnorablesAsHasSeen(typeResolver, ignorableTypes, each);
    Optional pModel = modelProvider.modelFor(each);
    if (pModel.isPresent()) {
      mergeModelMap(modelMap, pModel.get());
    } else {
    }
    populateDependencies(each, modelMap);
  }
  return  modelMap;
}

就是从 modelContexts 转化为 Model ,看看 pluginsManager.modelContexts ,怎么取 modelContexts

public Set modelContexts(RequestMappingContext context) {
  DocumentationType documentationType = context.getDocumentationContext().getDocumentationType();
  // 构建接口的ModelContext集合
  for (OperationModelsProviderPlugin each : operationModelsProviders.getPluginsFor(documentationType)) {
    each.apply(context);
  }
  return context.operationModelsBuilder().build();
}

OperationModelsProviderPlugin 有两个实现类,通过文档类型来获取。

  • OperationModelsProviderPlugin:处理返回类型,参数类型等

  • SwaggerOperationModelsProvider:swagger注解提供的值类型, @ApiResponse @ApiOperation

先看 OperationModelsProviderPlugin

@Override
public void apply(RequestMappingContext context) {
  // 收集返回类型
  collectFromReturnType(context);
  // 收集参数类型
  collectParameters(context);
  // 收集接口型号
  collectGlobalModels(context);
}

到了这,本问题( GET 方法的请求Object不描述)的答案就要呼之欲出了。来看 collectParameters

private void collectParameters(RequestMappingContext context) {
  // 获取所有类型
  List parameterTypes = context.getParameters();
  for (ResolvedMethodParameter parameterType : parameterTypes) {
    // 过滤  
    if (parameterType.hasParameterAnnotation(RequestBody.class)
          || parameterType.hasParameterAnnotation(RequestPart.class)) 
{
        ResolvedType modelType = context.alternateFor(parameterType.getParameterType());
        context.operationModelsBuilder().addInputParam(modelType);
      }
  }
}

破案了,可以看到过滤时只会处理两种:通过 @RequestBody @ReuqestPart 注解标注的, 而GET方法的参数是不可以使用这两个注解的。(当然从规范来说,GET方法也不应该这种参数)。

至于 OperationModelsProviderPlugin 的另一个实现类 SwaggerOperationModelsProvider 主要是收集使用 @ApiOperation 时主句属性值和 @ApiResponse 响应状态码涉及到的型号,不再详细列出。

apiModelReader.read 中的 modelContexts 转化为 Model modelProvider.modelFor() 是通过 ModelProvider 实现,下一个问题会详细阐述。

那么,如何解决这个问题:

1.使用 Docket additionalModels 方法,在配置类中注入 TypeResolver

return new Docket(DocumentationType.SWAGGER_2)
.additionalModels(typeResolver.resolve(xxx))
...

2.借助第三方类库 如swagger-bootstrap-ui的工具类(我没接,但可以..)

3.重写

重写 OperationModelsProviderPlugin apply 方法,添加自定义收集器。或者直接重写 collectParameters 也行。比如

private void collectGetParameters(RequestMappingContext context) {
       ...
       for (ResolvedMethodParameter parameterType : parameterTypes) {
           // 不存在@RequestBody注解
           if (!parameterType.hasParameterAnnotation(RequestBody.class)...) {
           ...
               if (xxx) {
                   ResolvedType modelType = context.alternateFor(parameterType.getParameterType());
                   context.operationModelsBuilder().addInputParam(modelType);
               }
           } ...
       }}

问题解决。

2. Enum的描述格式

问题是对于枚举类,在生成的JSON文件中描述是在原参数对象中的如下格式:

   "xxx": {...}
   "periodUnit":{
      "type":"string",
      "enum":[
               "MINUTE",
               "HOUR"
               ...
         ]}

一般枚举使用会如 MINUTE(1,“分钟”) ,也就是包括了 code name 描述。

但实际 enum 的值会是二者之一。且不会生成如下的可重用的外部引用。

"schema":{
          "$ref":"#/definitions/xxxForm"
}

注意:可重用的问题在 3.0+ 可以通过配置处理。

如果需要强制将 enum 的值设为 code name ,或拓展更多的内容,就需要来看看, enum 类何时会被处理。

上一个问题的结尾说到 apiModelReader.read modelContexts 转化为 Model modelProvider.modelFor() 方法是通过 ModelProvider 实现,其实 ModelProvider`是接口,有两个实现类:

  • DefaultModelProvider:默认,每次都会将modelContext转换为model

  • CachingModelProvider:声明了guava缓存池,先从缓存池取,没有则调用初始化处理器,转换为模型,再放入缓存池。

ApiModelReader 的构造方法里指定了使用 CachingModelProvider ,不过第一次调用缓存里是没有的,所以往下走到 populateDependencies

private void populateDependencies(ModelContext modelContext, Map modelMap) {
  Map dependencies = modelProvider.dependencies(modelContext);
  for (Model each : dependencies.values()) {
    mergeModelMap(modelMap, each);
  }
}

CachingModelProvider dependencies 依赖的是 DefaultModelProvider

public Map dependencies(ModelContext modelContext) {
  return delegate.dependencies(modelContext);
}

所以看 DefaultModelProvider 中的实现

public Map dependencies(ModelContext modelContext) {
  Map models = newHashMap();
  for (ResolvedType resolvedType : dependencyProvider.dependentModels(modelContext)) {
    ModelContext parentContext = ModelContext.fromParent(modelContext, resolvedType);
    Optional model = modelFor(parentContext).or(mapModel(parentContext, resolvedType));
    if (model.isPresent()) {
      models.put(model.get().getName(), model.get());
    }
  }
  return models;
}

dependencyProvider.dependentModels 和上面一个路子,一默认一缓存,交替接口。

public Set dependentModels(ModelContext modelContext) {
  return from(resolvedDependencies(modelContext))
      .filter(ignorableTypes(modelContext))
      .filter(not(baseTypes(modelContext)))
      .toSet();
}

后面是两个过滤,暂且不提。看 resolvedDependencies

private List resolvedDependencies(ModelContext modelContext) {
  ...
  List dependencies = newArrayList(resolvedTypeParameters(modelContext, resolvedType));
  dependencies.addAll(resolvedArrayElementType(modelContext, resolvedType));
  dependencies.addAll(resolvedPropertiesAndFields(modelContext, resolvedType));
  ...
}

这里都是在构造拓展类型 ResolvedType ,有一个叫 resolvedPropertiesAndFields ,看名字就是它了,进去

private






请到「今天看啥」查看全文