伍佰目录 短网址
  当前位置:海洋目录网 » 站长资讯 » 站长资讯 » 文章详细 订阅RssFeed

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

来源:本站原创 浏览:101次 时间:2023-04-29

先说一说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()方法.

@Componentpublic class DocumentationPluginsBootstrapper implements SmartLifecycle {

接着看 start 方法

@Overridepublic void start() {    if (initialized.compareAndSet(false, true)) {        // 拿到 DocumentationPlugin 插件        List<DocumentationPlugin> 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<RequestHandler> 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方法:

@Overridepublic 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<String, ApiListing> apiListings = apiListingScanner.scan(listingContext);  ...

apiListingReferenceScanner.scan位于 ApiListingReferenceScanner

public ApiListingReferenceScanResult scan(DocumentationContext context) {  ...  // 接口选择器 在构建Docket时通过.select()默认配置   ApiSelector selector = context.getApiSelector();  // 根据package路径(一般)或注解区分, 过滤筛选掉不符规则的 RequestHandler 接口  Iterable<RequestHandler> 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的方法。

再回到 ApiDocumentationScannerscan方法,看 apiListingScanner.scan

public Multimap<String, ApiListing> 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<String, Model> read(RequestMappingContext context) { // 忽略的class  Set<Class> ignorableTypes = newHashSet(context.getIgnorableParameterTypes());  Set<ModelContext> modelContexts = pluginsManager.modelContexts(context);  Map<String, Model> modelMap = newHashMap(context.getModelMap());  for (ModelContext each : modelContexts) {    markIgnorablesAsHasSeen(typeResolver, ignorableTypes, each);    Optional<Model> pModel = modelProvider.modelFor(each);    if (pModel.isPresent()) {      mergeModelMap(modelMap, pModel.get());    } else {    }    populateDependencies(each, modelMap);  }  return modelMap;}

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

public Set<ModelContext> 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

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

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

private void collectParameters(RequestMappingContext context) {  // 获取所有类型  List<ResolvedMethodParameter> 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转化为 ModelmodelProvider.modelFor()是通过ModelProvider实现,下一个问题会详细阐述。

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

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

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

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

3.重写

重写OperationModelsProviderPluginapply方法,添加自定义收集器。或者直接重写 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,“分钟”),也就是包括了codename描述。

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

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

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

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

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

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

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

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

CachingModelProviderdependencies依赖的是DefaultModelProvider

public Map<String, Model> dependencies(ModelContext modelContext) {  return delegate.dependencies(modelContext);}

所以看DefaultModelProvider中的实现

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

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

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

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

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

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

private List<ResolvedType> resolvedPropertiesAndFields(ModelContext modelContext, ResolvedType resolvedType) {  ...  List<ResolvedType> properties = newArrayList();  for (ModelProperty property : nonTrivialProperties(modelContext, resolvedType)) {    ...    properties.addAll(maybeFromCollectionElementType(modelContext, property));    properties.addAll(maybeFromMapValueType(modelContext, property));    properties.addAll(maybeFromRegularType(modelContext, property));  }}

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

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

之后是propertiesFor

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

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

public List<ModelProperty> propertiesFor(ResolvedType type, ModelContext givenContext) {  ...  for (Map.Entry<String, BeanPropertyDefinition> each : propertyLookup.entrySet()) {    BeanPropertyDefinition jacksonProperty = each.getValue();    Optional<AnnotatedMember> annotatedMember        = Optional.fromNullable(safeGetPrimaryMember(jacksonProperty));    if (annotatedMember.isPresent()) {      properties.addAll(candidateProperties(type, annotatedMember.get(), jacksonProperty, givenContext));    }  }...}

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

@VisibleForTestingList<ModelProperty> candidateProperties(    ResolvedType type,    AnnotatedMember member,    BeanPropertyDefinition jacksonProperty,    ModelContext givenContext) {  List<ModelProperty> properties = newArrayList();  if (member instanceof AnnotatedMethod) {    properties.addAll(findAccessorMethod(type, member)        .transform(propertyFromBean(givenContext, jacksonProperty))        .or(new ArrayList<ModelProperty>()));  } else if (member instanceof AnnotatedField) {    properties.addAll(findField(type, jacksonProperty.getInternalName())        .transform(propertyFromField(givenContext, jacksonProperty))        .or(new ArrayList<ModelProperty>()));  } else if (member instanceof AnnotatedParameter) {    ModelContext modelContext = ModelContext.fromParent(givenContext, type);    properties.addAll(fromFactoryMethod(type, jacksonProperty, (AnnotatedParameter) member, modelContext));  } ...}

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

 ...  public List<ModelProperty> 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));    }...    }};

接着是 beanModelProperty

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

最后调用了 schemaPluginsManager.property

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

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

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

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

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

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


原作者:无名鼠辈
原文链接:“SpringFox源码解析"
原出处:博客园
侵删


  推荐站点

  • At-lib分类目录At-lib分类目录

    At-lib网站分类目录汇集全国所有高质量网站,是中国权威的中文网站分类目录,给站长提供免费网址目录提交收录和推荐最新最全的优秀网站大全是名站导航之家

    www.at-lib.cn
  • 中国链接目录中国链接目录

    中国链接目录简称链接目录,是收录优秀网站和淘宝网店的网站分类目录,为您提供优质的网址导航服务,也是网店进行收录推广,站长免费推广网站、加快百度收录、增加友情链接和网站外链的平台。

    www.cnlink.org
  • 35目录网35目录网

    35目录免费收录各类优秀网站,全力打造互动式网站目录,提供网站分类目录检索,关键字搜索功能。欢迎您向35目录推荐、提交优秀网站。

    www.35mulu.com
  • 就要爱网站目录就要爱网站目录

    就要爱网站目录,按主题和类别列出网站。所有提交的网站都经过人工审查,确保质量和无垃圾邮件的结果。

    www.912219.com
  • 伍佰目录伍佰目录

    伍佰网站目录免费收录各类优秀网站,全力打造互动式网站目录,提供网站分类目录检索,关键字搜索功能。欢迎您向伍佰目录推荐、提交优秀网站。

    www.wbwb.net