Swagger 自定义Model、Enum(SpringFox源码分析)

时间:2024-10-12 11:54:57
Springfox源码分析-自定义Model、Enum

先说一说SpringfoxSwagger的关系

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始就在: 下的 DocumentationPluginsBootstrapper

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

@Component
public class DocumentationPluginsBootstrapper implements SmartLifecycle {
  • 1
  • 2

接着看 start 方法

@Override
public void start() {
    if ((false, true)) {
        // 拿到 DocumentationPlugin 插件
        List<DocumentationPlugin> plugins = pluginOrdering()
            .sortedCopy(());
        for (DocumentationPlugin each : plugins) {
            //获取文档类型
            DocumentationType documentationType = ();
            if (()) {
                // 启用则扫描生成文档
                scanDocumentation(buildContext(each));
            } 
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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

private DocumentationContext buildContext(DocumentationPlugin each) {
    return ((each));
}
  • 1
  • 2
  • 3

再往下走

private DocumentationContextBuilder defaultContextBuilder(DocumentationPlugin each) {
    DocumentationType documentationType = ();
    // 获取所有的RequestHnadler
    List<RequestHandler> requestHandlers = ().transformAndConcat(()).toList();
    return (documentationType, ).requestHandlers(requestHandlers);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

接着看 DocumentationContextBuilder的构造过程:

public DocumentationContextBuilder createContextBuilder(DocumentationType documentationType,
                                                        DefaultConfiguration defaultConfiguration) {
  return defaultsProviders.getPluginFor(documentationType, defaultConfiguration)
      .create(documentationType)
      .withResourceGroupingStrategy(resourceGroupingStrategy(documentationType));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

@Override
public DocumentationContextBuilder create(DocumentationType documentationType) {
  return new DocumentationContextBuilder(documentationType)
          .operationOrdering(())
          .apiDescriptionOrdering(())
          .apiListingReferenceOrdering(())
          .additionalIgnorableTypes(())
          .rules((typeResolver))
          .defaultResponseMessages(())
          .pathProvider(new RelativePathProvider(servletContext))
          .typeResolver(typeResolver)
          .enableUrlTemplating(false)
          .selector();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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

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

return ((each));
  • 1

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(())
                .build();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

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

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

private void scanDocumentation(DocumentationContext context) {
  ((context));
}
  • 1
  • 2
  • 3

其中 scan 位于 ApiDocumentationScanner

public Documentation scan(DocumentationContext context) {
  ApiListingReferenceScanResult result = (context);
  ...
  Multimap<String, ApiListing> apiListings = (listingContext);
  ...
  • 1
  • 2
  • 3
  • 4
  • 5

位于 ApiListingReferenceScanner

public ApiListingReferenceScanResult scan(DocumentationContext context) {
  ...
  // 接口选择器 在构建Docket时通过.select()默认配置 
  ApiSelector selector = ();
  // 根据package路径(一般)或注解区分, 过滤筛选掉不符规则的 RequestHandler 接口
  Iterable<RequestHandler> matchingHandlers = from(())
      .filter(());
  for (RequestHandler handler : matchingHandlers) {
    // 接口分组 resourceGroup = Controller,RequestMapping = method
    ResourceGroup resourceGroup = new ResourceGroup((),
        (), 0);
    RequestMappingContext requestMappingContext
        = new RequestMappingContext(context, handler);
    (resourceGroup, requestMappingContext);
  }
  return new ApiListingReferenceScanResult(asMap(resourceGroupRequestMappings));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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

再回到 ApiDocumentationScannerscan方法,看

public Multimap<String, ApiListing> scan(ApiListingScanningContext context) {
  ...
  for (ResourceGroup resourceGroup : sortedByName(())) {
    ...
    for (RequestMappingContext each : sortedByMethods((resourceGroup))) {
      // 循环Controller下的所有接口的实例对象, 拿到该接口的所有Model
      (((models)));
      ((each));
    }
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

是复制对象,主要看,读取该接口的 Model 信息。

public Map<String, Model> read(RequestMappingContext context) {
	// 忽略的class
  Set<Class> ignorableTypes = newHashSet(());
  Set<ModelContext> modelContexts = (context);
  Map<String, Model> modelMap = newHashMap(());
  for (ModelContext each : modelContexts) {
    markIgnorablesAsHasSeen(typeResolver, ignorableTypes, each);
    Optional<Model> pModel = (each);
    if (()) {
      mergeModelMap(modelMap, ());
    } else {
    }
    populateDependencies(each, modelMap);
  }
  return modelMap;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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

public Set<ModelContext> modelContexts(RequestMappingContext context) {
  DocumentationType documentationType = ().getDocumentationType();
  // 构建接口的ModelContext集合
  for (OperationModelsProviderPlugin each : (documentationType)) {
    (context);
  }
  return ().build();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

  • OperationModelsProviderPlugin:处理返回类型,参数类型等
  • SwaggerOperationModelsProvider:swagger注解提供的值类型,@ApiResponse@ApiOperation

先看OperationModelsProviderPlugin

@Override
public void apply(RequestMappingContext context) {
  // 收集返回类型
  collectFromReturnType(context);
  // 收集参数类型
  collectParameters(context);
  // 收集接口型号
  collectGlobalModels(context);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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

private void collectParameters(RequestMappingContext context) {
  // 获取所有类型
  List<ResolvedMethodParameter> parameterTypes = ();
  for (ResolvedMethodParameter parameterType : parameterTypes) {
    // 过滤  
    if (()
          || ()) {
        ResolvedType modelType = (());
        ().addInputParam(modelType);
      }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

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

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

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

  1. 使用 DocketadditionalModels方法,在配置类中注入 TypeResolver
return new Docket(DocumentationType.SWAGGER_2)
.additionalModels((xxx))
...
  • 1
  • 2
  • 3
  1. 借助第三方类库 如swagger-bootstrap-ui的工具类(我没接,但可以…)

  2. 重写

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

    private void collectGetParameters(RequestMappingContext context) {
        ...
        for (ResolvedMethodParameter parameterType : parameterTypes) {
            // 不存在@RequestBody注解
            if (!()...) {
       					...
                if (xxx) {
                    ResolvedType modelType = (());
                    ().addInputParam(modelType);
                }
            } ...
        }}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

问题解决。

2. Enum的描述格式

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

  "xxx": {...}
  "periodUnit":{
     "type":"string",
     "enum":[
              "MINUTE",
              "HOUR"
              ...
        ]}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

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

"schema":{
         "$ref":"#/definitions/xxxForm"
}
  • 1
  • 2
  • 3

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

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

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

  • DefaultModelProvider:默认,每次都会将modelContext转换为model
  • CachingModelProvider:声明了guava缓存池,先从缓存池取,没有则调用初始化处理器,转换为模型,再放入缓存池。

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

private void populateDependencies(ModelContext modelContext, Map<String, Model> modelMap) {
  Map<String, Model> dependencies = (modelContext);
  for (Model each : ()) {
    mergeModelMap(modelMap, each);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

CachingModelProviderdependencies依赖的是DefaultModelProvider

public Map<String, Model> dependencies(ModelContext modelContext) {
  return (modelContext);
}
  • 1
  • 2
  • 3

所以看DefaultModelProvider中的实现

public Map<String, Model> dependencies(ModelContext modelContext) {
  Map<String, Model> models = newHashMap();
  for (ResolvedType resolvedType : (modelContext)) {
    ModelContext parentContext = (modelContext, resolvedType);
    Optional<Model> model = modelFor(parentContext).or(mapModel(parentContext, resolvedType));
    if (()) {
      (().getName(), ());
    }
  }
  return models;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

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

public Set<ResolvedType> dependentModels(ModelContext modelContext) {
  return from(resolvedDependencies(modelContext))
      .filter(ignorableTypes(modelContext))
      .filter(not(baseTypes(modelContext)))
      .toSet();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

private List<ResolvedType> resolvedDependencies(ModelContext modelContext) {
  ...
  List<ResolvedType> dependencies = newArrayList(resolvedTypeParameters(modelContext, resolvedType));
  (resolvedArrayElementType(modelContext, resolvedType));
  (resolvedPropertiesAndFields(modelContext, resolvedType));
  ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

private List<ResolvedType> resolvedPropertiesAndFields(ModelContext modelContext, ResolvedType resolvedType) {
  ...
  List<ResolvedType> properties = newArrayList();
  for (ModelProperty property : nonTrivialProperties(modelContext, resolvedType)) {
    ...
    (maybeFromCollectionElementType(modelContext, property));
    (maybeFromMapValueType(modelContext, property));
    (maybeFromRegularType(modelContext, property));
  }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

看到ModelProperty,也就是对象内部属性代表的Model了,那就看nonTrivialProperties方法

private FluentIterable<ModelProperty> nonTrivialProperties(ModelContext modelContext, ResolvedType resolvedType) {
  return from(propertiesFor(modelContext, resolvedType))
      .filter(not(baseProperty(modelContext)));
}
  • 1
  • 2
  • 3
  • 4

之后是propertiesFor

private List<ModelProperty> propertiesFor(ModelContext modelContext, ResolvedType resolvedType) {
  return propertiesProvider.propertiesFor(resolvedType, modelContext);
}
  • 1
  • 2
  • 3

这个仍是一cache一default的策略,直接看实现

public List<ModelProperty> propertiesFor(ResolvedType type, ModelContext givenContext) {
  ...
  for (<String, BeanPropertyDefinition> each : ()) {
    BeanPropertyDefinition jacksonProperty = ();
    Optional<AnnotatedMember> annotatedMember
        = (safeGetPrimaryMember(jacksonProperty));
    if (()) {
      (candidateProperties(type, (), jacksonProperty, givenContext));
    }
  }...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

可以看到 List<ModelProperty>通过 candidateProperties方法获取

@VisibleForTesting
List<ModelProperty> candidateProperties(
    ResolvedType type,
    AnnotatedMember member,
    BeanPropertyDefinition jacksonProperty,
    ModelContext givenContext) {
  List<ModelProperty> properties = newArrayList();
  if (member instanceof AnnotatedMethod) {
    (findAccessorMethod(type, member)
        .transform(propertyFromBean(givenContext, jacksonProperty))
        .or(new ArrayList<ModelProperty>()));
  } else if (member instanceof AnnotatedField) {
    (findField(type, ())
        .transform(propertyFromField(givenContext, jacksonProperty))
        .or(new ArrayList<ModelProperty>()));
  } else if (member instanceof AnnotatedParameter) {
    ModelContext modelContext = (givenContext, type);
    (fromFactoryMethod(type, jacksonProperty, (AnnotatedParameter) member, modelContext));
  }
 ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

这里根据 AnnotatedMember判断类成员的类型,进行不同的处理。enum使用的是 propertyFromBean

  ...
  public List<ModelProperty> apply(ResolvedMethod input) {
    ResolvedType type = paramOrReturnType(typeResolver, input);
    if (!(type)) {
      if (shouldUnwrap(input)) {
          return propertiesFor(type, fromParent(givenContext, type));
      }
      return newArrayList(beanModelProperty(input, jacksonProperty, givenContext));
    }...
    }};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

接着是 beanModelProperty

private ModelProperty beanModelProperty(
    ...
  return schemaPluginsManager.property(
      new ModelPropertyContext(propertyBuilder,
          jacksonProperty,
          typeResolver,
          ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

最后调用了

public ModelProperty property(ModelPropertyContext context) {
  // 根据文档类型取出 ModelPropertyBuilderPlugin
  for (ModelPropertyBuilderPlugin enricher : propertyEnrichers.getPluginsFor(context.getDocumentationType())) {
    enricher.apply(context);
  }
  return context.getBuilder().build();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

public void apply(ModelPropertyContext context) {
  // 取出元素的注解
  Optional<ApiModelProperty> 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())
        .isHidden(annotation.transform(toHidden()).or(false))
        .type(annotation.transform(toType(context.getResolver())).orNull())
        .position(annotation.transform(toPosition()).or(0))
        .example(annotation.transform(toExample()).orNull());
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

可以看到通过判断是否存在注解,再设置具体的配置。

其中type就是enum展示的类型了,可以固定。allowableValues就是enumvalue,可以自定义,还可以加入description.

具体实现可以通过重写ApiModelPropertyPropertyBuilderapply实现。

到这里,两个问题都得到解决。Springfox的加载过程也基本介绍了一遍。