0
点赞
收藏
分享

微信扫一扫

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


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


​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<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​​的方法。

再回到 ​​ApiDocumentationScanner​​​的 ​​scan​​​方法,看 ​​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​


@Override
public 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​​​转化为 ​​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<String, Model> modelMap) {
Map<String, Model> dependencies = modelProvider.dependencies(modelContext);
for (Model each : dependencies.values()) {
mergeModelMap(modelMap, each);
}
}


​CachingModelProvider​​​的​​dependencies​​​依赖的是​​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​​方法获取


@VisibleForTesting
List<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​​​就是​​enum​​​的​​value​​​,可以自定义,还可以加入​​description​​.

具体实现可以通过重写​​ApiModelPropertyPropertyBuilder​​​的​​apply​​实现。

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

原作者:无名鼠辈


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


举报

相关推荐

0 条评论