在前后端分离开发的今天,接口文档显得尤为重要,其重要性有以下几点:1、约定接口信息,便于前后端分离开发;2、便于测试人员测试并检查数据;3、加快新员工上手工作的速度;4、后续工作交接。然而,接口文档的整理与编写又令繁重的开发工作雪上加霜。有没有好用的工具或者api可以简化这些工作呢?答案是肯定的,这也是我们今天要介绍的Swagger。话不多说,先来个Demo开开胃。
一、项目结构
(1) config:Swagger配置内容; (2) controller:测试接口;(3) spring:spring mvc的项目配置文件;
(4) web.xml:项目启动配置; (5) pom.xml:项目依赖管理
二、项目内容
SwaggerConfig.java
@Configuration
@EnableSwagger2
@EnableWebMvc
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(Controller.class))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("example接口文档")
.description("使用Swagger2构建RESTful APIs")
.build();
}
}
简单来说,这个文件就是用来配置Swagger2的,创建Docket对象来介绍接口信息。
(1) apis():根据配置决定生成接口的路径或注解; (2) paths():用于控制哪些接口生成;
TestController.java
@Controller
@RequestMapping("/test")
@Api("测试用例")
public class TestController {
@ResponseBody
@GetMapping("/hello")
@ApiOperation(nickname = "sayHello", value = "欢迎光临", notes = "欢迎光临", response = String.class)
@ApiImplicitParams({
@ApiImplicitParam(name = "name", value = "姓名", dataType = "String", paramType = "query")
})
public String sayHello(String name) {
return "Hello," + name;
}
}
(1) @Api:描述接口的作用,用于类层;(2) @ApiOperation:描述接口信息,用于方法层 (3) @ApiImplicitParams:用于介绍参数信息。
@ApiImplicitParams和@ApiParams的区别:前者一般用于请求url后直接带的参数,后者一般是用于以Body封装请求参数。
servlet-mvc.xml
<context:component-scan base-package="com.nieshk">
<context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.RestController"/>
</context:component-scan>
<aop:aspectj-autoproxy/>
<!-- enable autowire -->
<context:annotation-config />
<!-- 默认的注解映射的支持 -->
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
<!--<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>-->
</mvc:message-converters>
</mvc:annotation-driven>
<mvc:resources mapping="swagger-ui.html" location="classpath:/META-INF/resources/" />
<mvc:resources mapping="/webjars/**" location="classpath:/META-INF/resources/webjars/" />
文件作用:针对每个Servlet进行配置的,因此它的配置是在servlet的配置中,配置使用的是init-param, 它的作用就是在servlet初始化的时候,加载配置信息,完成servlet的初始化操作
spring-core.xml
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="searchSystemEnvironment" value="true"/>
<property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE"/>
<!--<property name="ignoreUnresolvablePlaceholders" value="true" />-->
</bean>
<!-- (Annotation方式配置services)enable component scanning (beware that this
does not enable mapper scanning!) 扫描除controller之外的bean -->
<context:component-scan base-package="com.nieshk" >
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- enable autowire -->
<context:annotation-config />
<!-- enable transaction demarcation with annotations -->
<tx:annotation-driven />
<task:annotation-driven/>
文件作用:用来与容器间进行交互的接口的组合,也就是说,这个接口定义了一系列的方法,servlet通过这些方法可以很方便地与自己所在的容器进行一些交互。
由于笔者比较懒,剩下的web.xml和pom.xml不再进行累述。
三、运行效果
访问/swagger-ui.html出现以下页面:
运行结果完美,输入参数,点击Try it out还能进行接口测试呢。
说好的原理详解呢?这难道只是挂羊头卖狗肉?老铁,说好的高逼格呢?说好的源码呢?
莫急莫急,接下来隆重介绍的就是Swagger2是怎么工作地。
四、原理详解
从SwaggerConfig.java中,可以看到在配置过程中使用了@Configuration、@EnableMvc、@EnableSwagger2三个注解。
1、 @Configuration:用于创建Bean对象、基于CGlib代理实现、用于全局配置。第一个和第三个好理解,关于CGlib代理可以参考文章《CGlib动态代理》
2、 @EnableWebMvc:引入类DelegatingWebMvcConfiguration.java、通过@Bean注册了和<mvc:annotation-driven/>一样的组件、添加@Configuration类来导入SpringMvc的配置、实现WebMvcConfigConfigurer的联系。
这家伙可是启动时加载的,原因是:他继承了WebMvcConfigurationSupport,而后者实现了ApplicationContextAware接口(Spring容器会检测容器中的所有实现了ApplicationContextAware接口的Bean)。
关于@EnableWebMvc的详细介绍可以参考《详解@EnableWebMvc》
3、@EnableSwagger2
开始之前,介绍几个注解功能:注解@Import,用来干嘛的呢?@Import用来导入@Configuration注解的配置类、声明@Bean注解的bean方法、导入ImportSelector的实现类或导入ImportBeanDefinitionRegistrar的实现类。@EnablePluginRegistries注解是spring-plugin模块提供的一个基于Plugin类型注册PluginRegistry实例到Spring上下文的注解,以下简称插件注册。
EnableSwagger2.java
@Import({Swagger2DocumentationConfiguration.class})
public @interface EnableSwagger2 {
}
可以看出,这个注解就一个作用,导入Swagger2DocumentationConfiguration.class,从类名可以知道,就是Swagger2生成接口文档的配置类嘛。
Swagger2DocumentationConfiguration.java
@Configuration
@Import({ SpringfoxWebMvcConfiguration.class, SwaggerCommonConfiguration.class })
@ComponentScan(basePackages = {
"springfox.documentation.swagger2.readers.parameter",
"springfox.documentation.swagger2.web",
"springfox.documentation.swagger2.mappers"
})
public class Swagger2DocumentationConfiguration {
@Bean
public JacksonModuleRegistrar swagger2Module() {
return new Swagger2JacksonModule();
}
}
这个类有什么用呢?主要是以下几点:
(1) 声明了@Bean注解的swagger2Module()方法:返回Swagger2JacksonModule实例化对象,用于引入模块,对接口的各种属性进行转换,注册模块、安装模块,信息序列化等。
(2) 组件扫描:将指定路径下的包扫描,便于Spring在被指定的包及其下级包(sub packages)中寻找bean,这三个路径的Bean对象作用分别如下:
readers.parameter:获取@ApiParam注解的接口参数名称
web:提供获取json格式的接口文档信息
mappers:安全、许可证、参数序列化等
导入SwaggerCommonConfiguration.java
@Configuration
@ComponentScan(basePackages = {
"springfox.documentation.swagger.schema",
"springfox.documentation.swagger.readers",
"springfox.documentation.swagger.web"
})
public class SwaggerCommonConfiguration {
@Bean
public static PropertySourcesPlaceholderConfigurer swaggerProperties() {
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
propertySourcesPlaceholderConfigurer.setIgnoreUnresolvablePlaceholders(true);
return propertySourcesPlaceholderConfigurer;
}
}
这个文件主要干了这几件事:
(1) 声明@Bean注解的swaggerProperties()方法:实现对Classpath下的配置文件的注入
(2) 组件扫描:
schema:获取@ApiModel、@ApiModelProperty注解的接口数据模型信息,实现了ModelBuilderPlugin的接口方法
readers:获取@ApiOperation注解的接口操作信息,获取@ApiParam注解的参数信息,实现了ParameterBuilderPlugin、ExpandedParameterBuilderPlugin、OperationBuilderPlugin、OperationModelsProviderPlugin等接口方法。
web:获取@Api注解的接口类的信息,对RequestMapping信息进行资源分组,提供接口资源访问Controller,实现了ApiListingBuilderPlugin、ResourceGroupingStrategy的接口方法。
导入SpringfoxWebMvcConfiguration.java
@Configuration
@Import({ ModelsConfiguration.class })
@ComponentScan(basePackages = {
"springfox.documentation.spring.web.scanners",
"springfox.documentation.spring.web.readers.operation",
"springfox.documentation.spring.web.readers.parameter",
"springfox.documentation.spring.web.plugins",
"springfox.documentation.spring.web.paths",
"springfox.documentation.spring.web.caching"
})
@EnablePluginRegistries({ DocumentationPlugin.class,
ApiListingBuilderPlugin.class,
OperationBuilderPlugin.class,
ParameterBuilderPlugin.class,
ExpandedParameterBuilderPlugin.class,
ResourceGroupingStrategy.class,
OperationModelsProviderPlugin.class,
DefaultsProviderPlugin.class,
PathDecorator.class
})
@EnableAspectJAutoProxy
public class SpringfoxWebMvcConfiguration {
@Bean
public Defaults defaults() {
return new Defaults();
}
@Bean
public DocumentationCache resourceGroupCache() {
return new DocumentationCache();
}
@Bean
public static ObjectMapperConfigurer objectMapperConfigurer() {
return new ObjectMapperConfigurer();
}
@Bean
public JsonSerializer jsonSerializer(List<JacksonModuleRegistrar> moduleRegistrars) {
return new JsonSerializer(moduleRegistrars);
}
}
这个文件干了这几件事:
(1) 声明@Bean注解方法:
defaults():接口的默认信息声明
resourceGroupCache():声明DocumentationCache,显而易见就是缓存的接口信息
objectMapperConfigurer:用于配置接口信息转换
jsonSerializer:json数据序列化
(2) 导入ModelsConfiguration.java
@Configuration
@ComponentScan(
basePackages = {"springfox.documentation.schema"}
)
@EnablePluginRegistries({ModelBuilderPlugin.class, ModelPropertyBuilderPlugin.class, TypeNameProviderPlugin.class})
public class ModelsConfiguration {
public ModelsConfiguration() {
}
@Bean
public TypeResolver typeResolver() {
return new TypeResolver();
}
}
(2.1) 插件注册:注册了ModelBuilderPlugin、ModelPropertyBuilderPlugin、TypeNameProviderPlugin这些插件
(2.2) 声明TypeResolver类型解析器方法
(3) 组件扫描
scanners:接口信息扫描
readers.operation:操作读取工具(模型、标签、响应等)
readers.parameter:参数读取(类型、模型、属性等)
plugins:Docket、接口选择器、DocumentationPlugins(用于管理接口信息注册的插件)、DocumentationPluginsBootstrapper(用于项目启动时加载接口文档配置信息)
paths:接口路径提供者、装饰器与适配器
caching:缓存切面,用于缓存接口信息
(4) 插件注册(一系列继承Plugin的接口)
(4.1) DocumentationPlugin:用于配置接口信息
boolean isEnabled():控制插件是否激活
DocumentationType getDocumentationType():获取文档类型
DocumentationContext configure():根据DocumentationContextBuilder创建文档内容
String getGroupName():获取插件分组信息
(4.2) ApiListingBuilderPlugin
void apply(ApiListingContext):实现接口方法,通过ApiListingBuilder覆盖ApiListing中属性
实现类:
ApiListingTagReader:获取@Api的tag信息、描述等
MediaTypeReader:获取@RequestMapping信息复制的consumes、produces
(4.3) OperationBuilderPlugin
void apply(OperationContext):实现接口方法,通过OperationBuilder重写Operation信息
实现类:
从插件的实现类中可以发现,这个插件就是为了读取接口的各种信息:请求方法、参数、描述、响应等一系列信息,获取请求参数时,主要是获取@ApiImplicitParam注解的参数信息
(4.4) ParameterBuilderPlugin
void apply(ParameterContext):获取输入参数和返回值,ParameterContext能用于重写参数属性
实现类:
实现类主要是获取参数的权限、数据类型、描述、名称、是否必须等信息,主要是获取@ApiParam注解的参数。
(4.5) ExpandedParameterBuilderPlugin:扩展接口参数的一些功能,比如判断这个参数的数据类型以及是否为这个接口的必须参数
void apply(ParameterExpansionContext):通过context中的ParameterBuilder重写参数
实现类:
实现类主要是获取扩展参数信息。
(4.6) ResourceGroupingStrategy
Set<ResourceGroup> getResourceGroups():根据请求路由与处理方法对请求参数进行分组,也就是根据@RequestMapping注解值进行分组。例如:TestController中@RequestMapping("/test),Test2Controller中@RequestMapping("/test)为同一个组;
String getResourceDescription():获取请求的描述
Integer getResourcePosition():获取资源位置
实现类:
ClassOrApiAnnotationResourceGrouping.java用于对@Api注解资源进行分组
SpringGroupingStrategy.java 用于对@RequestMapping注解资源进行分组
(4.7) OperationModelsProviderPlugin
void apply(RequestMappingContext):获取输入参数和返回值,RequestMappingContext能用于重写参数属性
实现类:
OperationModelsProvider.java 用于获取@RequestBody和@RequestPart注解的请求参数
SwaggerOperationModelsProvider.java 用于获取@ApiResponses和@ApiResponse注解的返回参数。
(4.8) DefaultsProviderPlugin
DocumentationContextBuilder create(DocumentationType):根据文档类型构造一个默认的文档内容构造器,如下:
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);
}
(4.9) PathDecorator
Function<String,String> decorator(PathContext):对RequestMappingContext进行转换
实现类:
以上就是@EnableSwagger2注解之后所引入的配置与工作。
五、 加载过程
了解了Swagger2配置原理之后,咱们再深入了解一下这些配置是怎么加载并访问的吧。在Spring中,ApplicationContext事件机制是观察者设计模式的实现,通过ApplicationEvent类和ApplicationListener接口,可以实现ApplicationContext事件处理。如果容器中有一个ApplicationListener Bean,每当ApplicationContext发布ApplicationEvent时,ApplicationListener Bean将自动被触发。这种事件机制都必须需要程序显示的触发。简而言之,当一个类实现了ApplicationListener接口,那么会在启动时加载。
在Swagger2的源码里,有一个类DocumentationPluginsBootstrapper.java,这个类干了啥事呢?实现了ApplicationListener接口,启动时会触发onApplicationEvent() 方法,从而加载了Swagger的配置信息与接口信息。
(1) 项目启动时,基于Plugin类型注册PluginRegistry实例到Spring上下文的注解(插件注册)
(2) 调用DocumentationPluginsManager.documentationPlugins()方法获取注册的插件
(3) 遍历获取到的插件,创建DocumentationContextBuilder对象,创建DocumentationContext上下文,调用方法ApiDocumentationScanner.scan()获取Documentation并添加到DocumentationCache中
(4) ApiDocumentationScanner.scan()方法中,主要作用如下:
(5) 启动过程中将接口信息缓存在DocumentationCache后,访问/v2/api-docs接口
(6) 获取到缓存的Documentation信息
(7) 调用ServiceModelToSwagger2Mapper.mapDocumentation(documentation)将Documentation转换为Swagger对象
(8) 调用JsonSerializer.toJson()方法将Swagger对象转换为Json,返回Json格式的接口文档信息。