SwaggarカスタムModel、Enum(Spring Foxソース分析)

29977 ワード

Springfoxソース分析-カスタムModel、Enum
まずSprigfoxとSwaggerの関係を言います。
Swaggerは規範です。
springfox-swagerはSpring生態系に基づくこの規範の実現である。
スプリングfox-swaguer-uiはswaguer-uiのパッケージで、Springのサービスを利用できるようにしています。
仕事中はSwaggar Jsonに基づいていくつか処理をする必要があるため、しかしSwaggar Jsonのフォーマットはそんなに需要を満たすのではありません。
本論文のsprigfox-swagerバージョン番号: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(false, true)) {
        //    DocumentationPlugin   
        List plugins = pluginOrdering()
            .sortedCopy(documentationPluginsManager.documentationPlugins());
        for (DocumentationPlugin each : plugins) {
            //      
            DocumentationType documentationType = each.getDocumentationType();
            if (each.isEnabled()) {
                //          
                scanDocumentation(buildContext(each));
            } 
        }
    }
}
buildContextメソッドを呼び出して、Docetオブジェクトを介してDcumentain Contextオブジェクトを作成します。
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);
}
handlerProvidersRequestHandlerProviderインターフェースであり、実装クラスは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を与える必要がある。DefaultConfigurationcreate方法を見ています。
@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は1つの実装クラス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));
}
scanApiDocumentationScannerクラスに位置している。
public Documentation scan(DocumentationContext context) {
  ApiListingReferenceScanResult result = apiListingReferenceScanner.scan(context);
  ...
  Multimap apiListings = apiListingScanner.scan(listingContext);
  ...
apiListingReferenceScanner.scanApiListingReferenceScannerクラスに位置しています。
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));
}
これまですべてのインターフェースを取得し、パケット化してきましたが、ArayListMultimapはguavaの方法です。ApiDocumentationScannerscanに戻る方法は、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には、2つの実装クラスがあり、ドキュメントタイプによって取得される。
  • Operation ModelsProvidePlugin:処理リターンタイプ、パラメータタイプなどの
  • SwaggarrOperation ModelsProvider: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);
          }
      }
    }
    
    解決されました。フィルタリングは2つしか処理されません。@RequestBody@ReuqestPartによって注釈表示されていますが、GET方法のパラメータはこの2つの注釈を使用することができません。(もちろん仕様から言えば、GET法もこのパラメータをすべきではない)。OperationModelsProviderPluginの他の実施形態SwaggerOperationModelsProviderは、主に@ApiOperationを使用した場合のマスタ属性値と@ApiResponseの応答状態コードに関する型番を収集し、詳細なリストを省略する。apiModelReader.readmodelContextsModelに変換されたmodelProvider.modelFor()ModelProviderによって実現され、次の問題は詳細に説明される。
    じゃ、どうやってこの問題を解決しますか?
  • は、DocketadditionalModels方法を用いて、構成クラスにTypeResolver
  • を注入する。
    return new Docket(DocumentationType.SWAGGER_2)
    .additionalModels(typeResolver.resolve(xxx))
    ...
    
  • .swaggar-boot strap-uiのような第三者クラスの工具類を利用しています。
  • は、カスタムコレクタを追加するOperationModelsProviderPluginapply方法を書き換える。または直接書き換えてもいいです。たとえば
    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"
                  ...
            ]}
    
    一般的なエニュメレーション使用は、collectParameters、すなわちMINUTE(1,“ ”)codeを含む。
    しかし、実際のnameの値は2つのうちの1つになります。また、以下のような重用可能な外部参照は生成されません。
    "schema":{
             "$ref":"#/definitions/xxxForm"
    }
    
    注意:再利用可能な問題は、構成によりenumで処理されてもよい。3.0+の値を強制的にenumまたはcodeに設定する必要がある場合、またはより多くのコンテンツを拡張する必要がある場合、nameクラスはいつ処理されるかを見に来てください。
    前の問題の最後にenumと述べたが、apiModelReader.readからmodelContextsに変換されたModel方法はmodelProvider.modelFor()によって実現されたものであり、実はModelProvider`はインターフェースであり、2つの実施形態がある:
  • Default Model Provider:デフォルトでは、毎回model Conteetをmodel
  • に変換します。
  • CachingModelProvider:gavaキャッシュカードを宣言しました。まずキャッシュから取って、ない場合は初期化プロセッサを呼び出して、モデルに変換してキャッシュに入れます。
  • ModelProviderの構成方法ではApiModelReaderを使用することが指定されていますが、初めてキャッシュにはないので、CachingModelProviderに下ります。
    private void populateDependencies(ModelContext modelContext, Map modelMap) {
      Map dependencies = modelProvider.dependencies(modelContext);
      for (Model each : dependencies.values()) {
        mergeModelMap(modelMap, each);
      }
    }
    
    populateDependenciesCachingModelProviderdependenciesに依存している。
    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;
    }
    
    DefaultModelProviderと上記のサブは、デフォルトでキャッシュされ、インターフェースが交替される。
    public Set dependentModels(ModelContext modelContext) {
      return from(resolvedDependencies(modelContext))
          .filter(ignorableTypes(modelContext))
          .filter(not(baseTypes(modelContext)))
          .toSet();
    }
    
    後は二つの濾過です。dependencyProvider.dependentModelsを見ます
    private List resolvedDependencies(ModelContext modelContext) {
      ...
      List dependencies = newArrayList(resolvedTypeParameters(modelContext, resolvedType));
      dependencies.addAll(resolvedArrayElementType(modelContext, resolvedType));
      dependencies.addAll(resolvedPropertiesAndFields(modelContext, resolvedType));
      ...
    }
    
    ここは全部構造拡張タイプのresolvedDependenciesです。ResolvedTypeという名前があります。名前を見たらそれです。中に入ります。
    private List resolvedPropertiesAndFields(ModelContext modelContext, ResolvedType resolvedType) {
      ...
      List properties = newArrayList();
      for (ModelProperty property : nonTrivialProperties(modelContext, resolvedType)) {
        ...
        properties.addAll(maybeFromCollectionElementType(modelContext, property));
        properties.addAll(maybeFromMapValueType(modelContext, property));
        properties.addAll(maybeFromRegularType(modelContext, property));
      }}
    
    resolvedPropertiesAndFields、つまり対象の内部属性が表すModelを見ました。
    private FluentIterable nonTrivialProperties(ModelContext modelContext, ResolvedType resolvedType) {
      return from(propertiesFor(modelContext, resolvedType))
          .filter(not(baseProperty(modelContext)));
    }
    
    その後はModelPropertyです。
    private List<ModelProperty> propertiesFor(ModelContext modelContext, ResolvedType resolvedType) {
      return propertiesProvider.propertiesFor(resolvedType, modelContext);
    }
    
    このnonTrivialPropertiesはまだ一cacheのdefaultの策略で、直接に実現を見ます。
    public List propertiesFor(ResolvedType type, ModelContext givenContext) {
      ...
      for (Map.Entry each : propertyLookup.entrySet()) {
        BeanPropertyDefinition jacksonProperty = each.getValue();
        Optional annotatedMember
            = Optional.fromNullable(safeGetPrimaryMember(jacksonProperty));
        if (annotatedMember.isPresent()) {
          properties.addAll(candidateProperties(type, annotatedMember.get(), jacksonProperty, givenContext));
        }
      }...
    }
    
    propertiesForpropertiesProvider.propertiesFor方法で取得することが見られます。
    @VisibleForTesting
    List candidateProperties(
        ResolvedType type,
        AnnotatedMember member,
        BeanPropertyDefinition jacksonProperty,
        ModelContext givenContext) {
      List properties = newArrayList();
      if (member instanceof AnnotatedMethod) {
        properties.addAll(findAccessorMethod(type, member)
            .transform(propertyFromBean(givenContext, jacksonProperty))
            .or(new ArrayList()));
      } else if (member instanceof AnnotatedField) {
        properties.addAll(findField(type, jacksonProperty.getInternalName())
            .transform(propertyFromField(givenContext, jacksonProperty))
            .or(new ArrayList()));
      } else if (member instanceof AnnotatedParameter) {
        ModelContext modelContext = ModelContext.fromParent(givenContext, type);
        properties.addAll(fromFactoryMethod(type, jacksonProperty, (AnnotatedParameter) member, modelContext));
      }
     ...
    }
    
    ここでは、クラスメンバーのタイプをListに基づいて判断し、異なる処理を行う。enumはcandidatePropertiesを使用しています。
      ...
      public List apply(ResolvedMethod input) {
        ResolvedType type = paramOrReturnType(typeResolver, input);
        if (!givenContext.canIgnore(type)) {
          if (shouldUnwrap(input)) {
              return propertiesFor(type, fromParent(givenContext, type));
          }
          return newArrayList(beanModelProperty(input, jacksonProperty, givenContext));
        }...
        }};
    
    次にAnnotatedMemberです。
    private ModelProperty beanModelProperty(
        ...
      return schemaPluginsManager.property(
          new ModelPropertyContext(propertyBuilder,
              jacksonProperty,
              typeResolver,
              ...
    
    最後にpropertyFromBeanを呼び出しました。
    public ModelProperty property(ModelPropertyContext context) {
      //          ModelPropertyBuilderPlugin
      for (ModelPropertyBuilderPlugin enricher : propertyEnrichers.getPluginsFor(context.getDocumentationType())) {
        enricher.apply(context);
      }
      return context.getBuilder().build();
    }
    
    beanModelPropertyは一つのインターフェースであり、一つのインプリメンテーションクラスschemaPluginsManager.propertyを参照してください。
    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());
      }
    }
    
    コメントがあるかどうかを判断して、具体的な配置を設定することができます。ModelPropertyBuilderPluginは、ApiModelPropertyPropertyBuilderによって示されるタイプであり、固定可能である。typeenumallowableValuesであり、カスタムすることができ、enumに加入することもできる。
    具体的な実施は、書き換えvaluedescriptionによって実施されてもよい。
    ここに来て、二つの問題が解決されます。Springfoxのロード過程も基本的に紹介しました。