SpringFox 源码分析(及 Yapi 问题的另一种解决方案)

作者 | 李一帆

SpringFox 源码分析(及 Yapi 问题的另一种解决方案)_第1张图片

初级秃头后端工程师。

在做中台网关时需要基于 Swagger Json 生成契约文件和 SDK 。但生成的 Swagger Json 默认格式不是那么满足需求,就需要进行一些自定义,但要在哪里定义?

同时在使用 Yapi 时,也存在一些需要注意或修改的地方,这与需要“自定义”的地方部分是相同的。那么就带着问题,阅读源码找到原因。

本文springfox-swagger版本号:2.6.0

0. 写在前面

因贴出代码较多,较枯燥,先说结论: 

0.1 自定义对象使用 @ApiParam 无法显示属性描述

  • @ApiModel 和 @ApiModelProperty 都可用来标识对象内元素,只是 Springfox 处理时不会处理@ApiParam。

0.2 GET 请求的对象参数,不会生成对象的描述

  • 原因是 GET 请求原则上不应该使用应该配合 @RequestBody使用的对象参数,应该使用 POST 或 @ModelAttribute,但可用自定义强制处理。

  • List和Optional description 展示正常。

下面再看源码。

1. GET 请求的参数对象

第一个问题,当方法是GET请求,但参数是一个自定义对象,在展示时是不包括本对象描述的。如果去 Google,会告诉你根据HTTP规范或Restful风格”不建议”这么做,如下。

  • HTTP GET with request body

  • rfc3986

但就像建议我找个另一半而我却仍孤苦伶仃。

所以就需要看看什么时候会生成这些 Model 的描述。

1.1 start()

万事有始有终,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(false, true)) {
        // 拿到 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));
}

1.2 创建 DocumentationContextBuilder 对象

再往下走看  defaultContextBuilder方法 。

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 注解,所以需要通过  getPluginFor 给一个替换值 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

1.3 初始化 Docket

回到 1.1 中的buildContextdefaultContextBuilder 方法执行完毕,接下来是  each.configure

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

each 是 DocumentationPlugin 只有个实现类Docket,到这就有点熟悉了。Docket 对象是我们在外部用来创建的,而外部赋值的对象值,最终都会整合到 DocumentationContext。这里就是在二次赋值。可以看下一般定义的 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();
    }
}

外部创建时只设置了默认的参数。但接口,定义,模型等关键信息等都未初始化。

1.4. 扫描

再次回到最初的起点 start() , 看看 scanDocumentation(buildContext(each))的 scanDocumentation 。

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

其中  scan 位于  ApiDocumentationScanner类。有两个 scan ,分别来看。

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

第一个 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);

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

1.4.2

第二个 scan,  apiListingScanner.scan

public Multimap scan(ApiListingScanningContext context) {
    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 信息了。

1.5 读取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);
    // ModelContext 转 Model
    Optional pModel = modelProvider.modelFor(each);
    ...

先看 pluginsManager.modelContexts,怎么取的 modelContexts

public Set modelContexts(RequestMappingContext context) {
  // 构建接口的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());

破案了,可以看到过滤时只会处理两种:通过 @RequestBody 和 @ReuqestPart 注解标注的,而GET方法的参数是不使用这两个注解的,所以也就不被处理。

至于另一个实现类 SwaggerOperationModelsProvider 主要是收集使用 @ApiOperation 时主句属性值和@ApiResponse 响应状态码涉及到的型号,与本次无关就不再详细介绍。

而开头 read方法最后一行 modelContext 转化为  ModelmodelProvider.modelFor()是通过ModelProvider实现,下一问题会讲到它。

1.6. 解决问题

那么,如何解决这个问题。本文给出四个方法。

1.6.1 @ModelAttribute
  • 使用 @ModelAttribute,大体是可以的,但使用后首先传递的不是 json字符串对象, 请求路径会有变化。  Swagger UI展示时也不是一个对象而是一个个参数,如果想 基于 swagger json 生成契约代码、SDK等就不OK了,反向生成出来的就不是对象了。

  • 而且,会导致部分 List 加载失败,UI 丢失 List 元素或描述会出现问题。最后一个问题会谈到。

1.6.2 additionalModels

使用 Docket的 additionalModels 方法,在配置类中注入  TypeResolver 。直接将该 Model 强制加入,但不能一劳永逸。

return new Docket(DocumentationType.SWAGGER_2)
.additionalModels(typeResolver.resolve(xxx))
1.6.3 第三方

借助第三方类库 如swagger-bootstrap-ui的工具类。

1.6.4 重写

上面已经谈到了加载 Model 的逻辑,那么重写 OperationModelsProviderPlugin 的 apply 方法,添加自定义收集器,或者直接重写 collectParameters方法都可以。如新增自定义收集器:强制处理指定 Model

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. @ApiParam 不显示描述

上一个问题的结尾说到 apiModelReader.read的 modelProvider.modelFor() 方法。ModelProvider 是一个接口,有两个实现类:

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

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

2.1 model转换

在 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) {
  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());
    }

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

public Set dependentModels(ModelContext modelContext) {
  return from(resolvedDependencies(modelContext))

关注resolvedDependencies方法。

private List resolvedDependencies(ModelContext modelContext) {
  List dependencies = newArrayList(resolvedTypeParameters(modelContext, resolvedType));

这里都是在构造拓展类型  ResolvedType ,接着往下关注 resolvedPropertiesAndFields 方法。

private List resolvedPropertiesAndFields(ModelContext modelContext, ResolvedType resolvedType) {
  List properties = newArrayList();
  for (ModelProperty property : nonTrivialProperties(modelContext, resolvedType))

看到 ModelProperty ,也就是对象的属性,那就看 nonTrivialProperties 方法。

private FluentIterable nonTrivialProperties(ModelContext modelContext, ResolvedType resolvedType) {
  return from(propertiesFor(modelContext, resolvedType))
      .filter(not(baseProperty(modelContext)));

接着是 propertiesFor

private List propertiesFor(ModelContext modelContext, ResolvedType resolvedType) {
  return propertiesProvider.propertiesFor(resolvedType, modelContext);

这个propertiesProvider.propertiesFor 仍是一缓存一默认的策略,直接看实现。

public List propertiesFor(ResolvedType type, ModelContext givenContext) {
  for (Map.Entry each : propertyLookup.entrySet()) {
    properties.addAll(candidateProperties(type, annotatedMember.get(), jacksonProperty, givenContext));

2.2 判断元素注解类型

List =properties通过  candidateProperties 方法获取添加。

List candidateProperties( ResolvedType type, AnnotatedMember member...) {
  List properties = newArrayList();
  // 根据 元素注解进行不同处理
  if (member instanceof AnnotatedMethod) {
    properties.addAll(findAccessorMethod(type, member)
        .transform(propertyFromBean(givenContext, jacksonProperty))
        .or(new ArrayList()));
  } else if (member instanceof AnnotatedField) {
    ...
  } else if (member instanceof AnnotatedParameter) {
    ...
  }

根据  AnnotatedMember 判断类成员的类型,进行不同的处理。不过我们一般都会加入  @Data注解, getxxx方法导致我们基本都走到了第一个分支,调用了 propertyFromBean 方法。

return new Function>() {
@Override
  public List apply(ResolvedMethod input) {
  return newArrayList(beanModelProperty(input, jacksonProperty, givenContext));

2.3 加载Plugin

接着是 beanModelProperty

private ModelProperty beanModelProperty(
  return schemaPluginsManager.property(new ModelPropertyContext(propertyBuilder,...

最后调用了 schemaPluginsManager.property 方法。

public ModelProperty property(ModelPropertyContext context) {
  // 根据文档类型取出 ModelPropertyBuilderPlugin
  for (ModelPropertyBuilderPlugin enricher : propertyEnrichers.getPluginsFor(context.getDocumentationType())) {
    enricher.apply(context);
  }

ModelPropertyBuilderPlugin 是一个接口,看它的实现类 ApiModelPropertyPropertyBuilder

public void apply(ModelPropertyContext context) {
  // 取出元素的注解
  Optional annotation = Optional.absent();
  ...
  if (annotation.isPresent()) {
    context.getBuilder()
        .allowableValues(annotation.transform(toAllowableValues()).orNull())
        .required(annotation.transform(toIsRequired()).or(false))
        .readOnly(annotation.transform(toIsReadOnly()).or(false))
        .description(annotation.transform(toDescription()).orNull())
        ...
  }
}

终于找到问题的答案了,这个调用链有点长...可以看到是通过判断是否存在 @ApiModelProperty 注解,再进行赋值。所以 @ApiParam 不被处理。

2.4 解决问题

如果单纯想让其显示描述的话,重写 ApiModelPropertyPropertyBuilder 的 apply 方法判断 @ApiParam 不失为一个好办法,如下。

public class ModelPropertyPlugin implements ModelPropertyBuilderPlugin {
    @Override
    public void apply(ModelPropertyContext context) {
        Optional annotation = Optional.absent();
        if (context.getBeanPropertyDefinition().isPresent()) {
        annotation = annotation.or(findPropertyAnnotation(context.getBeanPropertyDefinition().get(), ApiParam.class));
        }
        ModelPropertyBuilder modelPropertyBuilder = context.getBuilder();
        if (annotation.isPresent()) {
           // 存在 @ApiParam注解。设置描述
            modelPropertyBuilder.description(annotation.get().value());
        }
    } ...

完事。

3. 对于Enum

在第一个问题说到 List 可能会丢失或描述有问题。其实,就对于单纯的 private Enum enum;,在 Swagegr UI 展示时你也不能保证它显示的是你定义 code还是 name。而对于嵌套数据结构,就更没法处理了。

3.1 解决

问题的原因是因为嵌套类型的 Enum取其中的namecode时和 Enum 不同,具体原因因篇幅就不再详细说明。可通过以下方式取出。

BeanPropertyDefinition propertyDefinition = context.getBeanPropertyDefinition().get();
ModelPropertyBuilder modelPropertyBuilder = context.getBuilder();

ResolvedType resolvedTypeObj = (ResolvedType) Objects.requireNonNull(type).get(modelPropertyBuilder);
            // 针对 Optional
            Class erasedType = resolvedTypeObj.getErasedType();
            // 针对 List
            Class bindingsType = erasedType;
            if (List.class.isAssignableFrom(erasedType)) {
                bindingsType = resolvedTypeObj.getTypeBindings().getBoundType(0).getErasedType();
            }
            Class fieldType = propertyDefinition.getField().getRawType();
            ...

这样,可以根据类型,取出 Enum 类的 name 和 code。对于嵌套类型也就可以自定义设置描述了。

4. 总结

基于三个问题分析,基本也将 SpringFox 的加载机制介绍了一部分。

全文完


以下文章您可能也会感兴趣:

  • 聊聊Hystrix 命令执行流程

  • Mysql redo log 漫游

  • RabbitMQ 如何保证消息可靠性

  • 从对称加密到非对称加密再到认证中心 -- https 的证书申请

  • 简单聊聊 TCP 的可靠性

  • 延时队列:基于 Redis 的实现

  • 你真的懂 Builder 设计模式吗?论如何实现真正安全的 Builder 模式

  • Actor 模型及 Akka 简介

  • 从零搭建一个基于 lstio 的服务网格

  • 容器管理利器:Web Terminal 简介

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 [email protected]

你可能感兴趣的:(SpringFox 源码分析(及 Yapi 问题的另一种解决方案))