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()
方法.
@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);}
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
方法:
@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
的方法。
再回到 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
@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
转化为 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
方法获取
@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
就是enum
的value
,可以自定义,还可以加入description
.
具体实现可以通过重写ApiModelPropertyPropertyBuilder
的apply
实现。
到这里,两个问题都得到解决。Springfox的加载过程也基本介绍了一遍。
原作者:无名鼠辈
原文链接:“SpringFox源码解析"
原出处:博客园
侵删
标签:modelContext,...,return,ModelContext,天天,context,each,原理,Swagger 来源: https://blog.51cto.com/15050718/2621731