作者 | 李一帆
初级秃头后端工程师。
在做中台网关时需要基于 Swagger Json 生成契约文件和 SDK 。但生成的 Swagger Json 默认格式不是那么满足需求,就需要进行一些自定义,但要在哪里定义?
同时在使用 Yapi 时,也存在一些需要注意或修改的地方,这与需要“自定义”的地方部分是相同的。那么就带着问题,阅读源码找到原因。
本文springfox-swagger版本号:
2.6.0
因贴出代码较多,较枯燥,先说结论:
@ApiModel 和 @ApiModelProperty 都可用来标识对象内元素,只是 Springfox 处理时不会处理@ApiParam。
原因是 GET 请求原则上不应该使用应该配合 @RequestBody使用的对象参数,应该使用 POST 或 @ModelAttribute,但可用自定义强制处理。
List和Optional description 展示正常。
下面再看源码。
第一个问题,当方法是GET请求,但参数是一个自定义对象,在展示时是不包括本对象描述的。如果去 Google,会告诉你根据HTTP规范或Restful风格”不建议”这么做,如下。
HTTP GET with request body
rfc3986
但就像建议我找个另一半而我却仍孤苦伶仃。
所以就需要看看什么时候会生成这些 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
方法,通过 Docket
对象创建 DocumentaionContext
对象。
private DocumentationContext buildContext(DocumentationPlugin each) {
return each.configure(this.defaultContextBuilder(each));
}
再往下走看 defaultContextBuilder
方法 。
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);
}
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
注解,所以需要通过 getPluginFor
给一个替换值 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
。
回到 1.1 中的buildContext
,defaultContextBuilder
方法执行完毕,接下来是 each.configure
。
return each.configure(this.defaultContextBuilder(each));
each
是 DocumentationPlugin
只有个实现类Docket
,到这就有点熟悉了。Docket
对象是我们在外部用来创建的,而外部赋值的对象值,最终都会整合到 DocumentationContext
。这里就是在二次赋值。可以看下一般定义的 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
类。有两个 scan
,分别来看。
public Documentation scan(DocumentationContext context) {
ApiListingReferenceScanResult result = apiListingReferenceScanner.scan(context);
...
Multimap apiListings = apiListingScanner.scan(listingContext);
第一个 apiListingReferenceScanner.scan
位于 ApiListingReferenceScanner
。
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);
这个方法拿到了所有接口信息并进行了分组,其中 ArrayListMultimap 是 guava
的方法。
第二个 scan
, apiListingScanner.scan
public Multimap scan(ApiListingScanningContext context) {
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);
// ModelContext 转 Model
Optional pModel = modelProvider.modelFor(each);
...
先看 pluginsManager.modelContexts
,怎么取的 modelContexts
。
public Set modelContexts(RequestMappingContext context) {
// 构建接口的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 parameterTypes = context.getParameters();
for (ResolvedMethodParameter parameterType : parameterTypes) {
// 过滤
if (parameterType.hasParameterAnnotation(RequestBody.class)
|| parameterType.hasParameterAnnotation(RequestPart.class)) {
ResolvedType modelType = context.alternateFor(parameterType.getParameterType());
破案了,可以看到过滤时只会处理两种:通过 @RequestBody
和 @ReuqestPart
注解标注的,而GET方法的参数是不使用这两个注解的,所以也就不被处理。
至于另一个实现类 SwaggerOperationModelsProvider
主要是收集使用 @ApiOperation
时主句属性值和@ApiResponse
响应状态码涉及到的型号,与本次无关就不再详细介绍。
而开头 read
方法最后一行 modelContext
转化为 Model
的modelProvider.modelFor()
是通过ModelProvider
实现,下一问题会讲到它。
那么,如何解决这个问题。本文给出四个方法。
使用 @ModelAttribute
,大体是可以的,但使用后首先传递的不是 json
字符串对象, 请求路径会有变化。 Swagger UI
展示时也不是一个对象而是一个个参数,如果想 基于 swagger json
生成契约代码、SDK等就不OK了,反向生成出来的就不是对象了。
而且,会导致部分 List
加载失败,UI 丢失 List
元素或描述会出现问题。最后一个问题会谈到。
使用 Docket
的 additionalModels
方法,在配置类中注入 TypeResolver
。直接将该 Model
强制加入,但不能一劳永逸。
return new Docket(DocumentationType.SWAGGER_2)
.additionalModels(typeResolver.resolve(xxx))
借助第三方类库 如swagger-bootstrap-ui的工具类。
上面已经谈到了加载 Model
的逻辑,那么重写 OperationModelsProviderPlugin
的 apply
方法,添加自定义收集器,或者直接重写 collectParameters
方法都可以。如新增自定义收集器:强制处理指定 Model
。
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);
}
} ...
上一个问题的结尾说到 apiModelReader.read
的 modelProvider.modelFor()
方法。ModelProvider
是一个接口,有两个实现类:
DefaultModelProvider:默认,每次都会将 modelContext 转换为 model。
CachingModelProvider:声明了 guava 缓存池,先从缓存池取,没有则调用初始化处理器,转换为模型,再放入缓存池。
在 ApiModelReader
的构造方法里指定使用 CachingModelProvider
,但第一次调用缓存里是没的,往下走到 populateDependencies
。
private void populateDependencies(ModelContext modelContext, Map modelMap) {
Map dependencies = modelProvider.dependencies(modelContext);
for (Model each : dependencies.values()) {
mergeModelMap(modelMap, each);
CachingModelProvider
的 dependencies
依赖的是 DefaultModelProvider
,等于又绕了回来。
public Map dependencies(ModelContext modelContext) {
return delegate.dependencies(modelContext);
所以还是看默认的 DefaultModelProvider
中的实现。
public Map dependencies(ModelContext modelContext) {
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());
}
dependencyProvider.dependentModels
和上面一个路子,一默认一缓存,交替接口。
public Set dependentModels(ModelContext modelContext) {
return from(resolvedDependencies(modelContext))
关注resolvedDependencies
方法。
private List resolvedDependencies(ModelContext modelContext) {
List dependencies = newArrayList(resolvedTypeParameters(modelContext, resolvedType));
这里都是在构造拓展类型 ResolvedType
,接着往下关注 resolvedPropertiesAndFields
方法。
private List resolvedPropertiesAndFields(ModelContext modelContext, ResolvedType resolvedType) {
List properties = newArrayList();
for (ModelProperty property : nonTrivialProperties(modelContext, resolvedType))
看到 ModelProperty
,也就是对象的属性,那就看 nonTrivialProperties
方法。
private FluentIterable nonTrivialProperties(ModelContext modelContext, ResolvedType resolvedType) {
return from(propertiesFor(modelContext, resolvedType))
.filter(not(baseProperty(modelContext)));
接着是 propertiesFor
。
private List propertiesFor(ModelContext modelContext, ResolvedType resolvedType) {
return propertiesProvider.propertiesFor(resolvedType, modelContext);
这个propertiesProvider.propertiesFor
仍是一缓存一默认的策略,直接看实现。
public List propertiesFor(ResolvedType type, ModelContext givenContext) {
for (Map.Entry each : propertyLookup.entrySet()) {
properties.addAll(candidateProperties(type, annotatedMember.get(), jacksonProperty, givenContext));
List
=properties
通过 candidateProperties
方法获取添加。
List candidateProperties( ResolvedType type, AnnotatedMember member...) {
List properties = newArrayList();
// 根据 元素注解进行不同处理
if (member instanceof AnnotatedMethod) {
properties.addAll(findAccessorMethod(type, member)
.transform(propertyFromBean(givenContext, jacksonProperty))
.or(new ArrayList()));
} else if (member instanceof AnnotatedField) {
...
} else if (member instanceof AnnotatedParameter) {
...
}
根据 AnnotatedMember
判断类成员的类型,进行不同的处理。不过我们一般都会加入 @Data
注解, getxxx
方法导致我们基本都走到了第一个分支,调用了 propertyFromBean
方法。
return new Function>() {
@Override
public List apply(ResolvedMethod input) {
return newArrayList(beanModelProperty(input, jacksonProperty, givenContext));
接着是 beanModelProperty
。
private ModelProperty beanModelProperty(
return schemaPluginsManager.property(new ModelPropertyContext(propertyBuilder,...
最后调用了 schemaPluginsManager.property
方法。
public ModelProperty property(ModelPropertyContext context) {
// 根据文档类型取出 ModelPropertyBuilderPlugin
for (ModelPropertyBuilderPlugin enricher : propertyEnrichers.getPluginsFor(context.getDocumentationType())) {
enricher.apply(context);
}
ModelPropertyBuilderPlugin
是一个接口,看它的实现类 ApiModelPropertyPropertyBuilder
。
public void apply(ModelPropertyContext context) {
// 取出元素的注解
Optional 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())
...
}
}
终于找到问题的答案了,这个调用链有点长...可以看到是通过判断是否存在 @ApiModelProperty
注解,再进行赋值。所以 @ApiParam
不被处理。
如果单纯想让其显示描述的话,重写 ApiModelPropertyPropertyBuilder
的 apply
方法判断 @ApiParam
不失为一个好办法,如下。
public class ModelPropertyPlugin implements ModelPropertyBuilderPlugin {
@Override
public void apply(ModelPropertyContext context) {
Optional annotation = Optional.absent();
if (context.getBeanPropertyDefinition().isPresent()) {
annotation = annotation.or(findPropertyAnnotation(context.getBeanPropertyDefinition().get(), ApiParam.class));
}
ModelPropertyBuilder modelPropertyBuilder = context.getBuilder();
if (annotation.isPresent()) {
// 存在 @ApiParam注解。设置描述
modelPropertyBuilder.description(annotation.get().value());
}
} ...
完事。
在第一个问题说到 List
可能会丢失或描述有问题。其实,就对于单纯的 private Enum enum;
,在 Swagegr UI
展示时你也不能保证它显示的是你定义 code
还是 name
。而对于嵌套数据结构,就更没法处理了。
问题的原因是因为嵌套类型的 Enum
取其中的name
和code
时和 Enum 不同,具体原因因篇幅就不再详细说明。可通过以下方式取出。
BeanPropertyDefinition propertyDefinition = context.getBeanPropertyDefinition().get();
ModelPropertyBuilder modelPropertyBuilder = context.getBuilder();
ResolvedType resolvedTypeObj = (ResolvedType) Objects.requireNonNull(type).get(modelPropertyBuilder);
// 针对 Optional
Class erasedType = resolvedTypeObj.getErasedType();
// 针对 List
Class bindingsType = erasedType;
if (List.class.isAssignableFrom(erasedType)) {
bindingsType = resolvedTypeObj.getTypeBindings().getBoundType(0).getErasedType();
}
Class fieldType = propertyDefinition.getField().getRawType();
...
这样,可以根据类型,取出 Enum 类的 name 和 code。对于嵌套类型也就可以自定义设置描述了。
基于三个问题分析,基本也将 SpringFox 的加载机制介绍了一部分。
全文完
以下文章您可能也会感兴趣:
聊聊Hystrix 命令执行流程
Mysql redo log 漫游
RabbitMQ 如何保证消息可靠性
从对称加密到非对称加密再到认证中心 -- https 的证书申请
简单聊聊 TCP 的可靠性
延时队列:基于 Redis 的实现
你真的懂 Builder 设计模式吗?论如何实现真正安全的 Builder 模式
Actor 模型及 Akka 简介
从零搭建一个基于 lstio 的服务网格
容器管理利器:Web Terminal 简介
我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 [email protected] 。