swagger
确实是个好东西,可以跟据业务代码自动生成相关的api
接口文档,尤其用于restful
风格中的项目,开发人员几乎可以不用专门去维护rest api
,这个框架可以自动为你的业务代码生成restfut
风格的api
,而且还提供相应的测试界面,自动显示json
格式的响应。大大方便了后台开发人员与前端的沟通与联调成本。
签于swagger
的强大功能,Java
开源界大牛spring
框架迅速跟上,它充分利用自已的优势,把swagger
集成到自己的项目里,整了一个spring-swagger
,后来便演变成springfox
。springfox
本身只是利用自身的aop
的特点,通过plug
的方式把swagger
集成了进来,它本身对业务api
的生成,还是依靠swagger
来实现。
关于这个框架的文档,网上的资料比较少,大部分是入门级的简单使用。本人在集成这个框架到自己项目的过程中,遇到了不少坑,为了解决这些坑,我不得不扒开它的源码来看个究竟。此文,就是记述本人在使用springfox
过程中对springfox
的一些理解以及需要注意的地方。
springfox
的大致原理就是,在项目启动的过种中,spring
上下文在初始化的过程,框架自动跟据配置加载一些swagger
相关的bean
到当前的上下文中,并自动扫描系统中可能需要生成ap
i文档那些类,并生成相应的信息缓存起来。如果项目MVC
控制层用的是springMvc
那么会自动扫描所有Controller
类,跟据这些Controller
类中的方法生成相应的api
文档。
因本人的项目就是SpringMvc
,所以此文就以SpringMvc
集成springfox
为例来讨论springfox
的使用与原理。
SpringMvc
集成springfox
的步骤
首先,项目需要加入以下三个依赖:
<!– sring mvc依赖 –>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.2.8.RELEASE</version>
</dependency>
<!– swagger2核心依赖 –>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>
<!– swagger-ui为项目提供api展示及测试的界面 –>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>
上面三个依赖是项目集成springmvc
及springfox
最基本的依赖,其它的依赖这里省略。其中第一个是springmvc
的基本依赖,第二个是swagger
依赖,第三个是界面相关的依赖,这个不是必须的,如果你不想用springfox
自带的api
界面的话,也可以不用这个,而另外自己写一套适合自己项目的界面。加入这几个依赖后,系统后会自动加入一些跟springfox
及swagger
相关jar
包,我粗略看了一下,主要有以下这么几个:
springfox-swagger2-2.6.1.jar
swagger-annotations-1.5.10.jar
swagger-models-1.5.10.jar
springfox-spi-2.6.1.jar
springfox-core-2.6.1.jar
springfox-schema-2.6.1.jar
springfox-swagger-common-2.6.1.jar
springfox-spring-web-2.6.1.jar
guava-17.0.jar
spring-plugin-core-1.2.0.RELEASE.jar
spring-plug-metadata-1.2.0.RELEASE.jar
spring-swagger-ui-2.6.1.jar
jackson-databind-2.2.3.jar
jackson-annotations-2.2.3.jar
上面是我通过目测觉得springfox
可能需要的jar
,可能没有完全例出springfox
所需的所有jar
。从上面jar
可以看出springfox
除了依赖swagger
之外,它还需要guava
、spring-plug
、jackson
等依赖包(注意jackson
是用于生成json
必须的jar
包,如果项目里本身没有加入这个依赖,为了集成swagger
的话必须额外再加入这个依赖)。
如果只用springfox
的默认的配置的话,与springmvc
集成起来非常简单,只要写一个类似于以下代码的类放到你的项目里就行了,代码如下:
@Configuration
@EnableWebMvc
@EnableSwagger2
publicclass ApiConfig {
}
注意到,上面是一个空的java
类文件,类名可以随意指定,但必须加入上述类中标出的@Configuration
、@EnableWebMvc
、@EnableSwagger2
三个注解,这样就完成了springmvc
与springfox
的基本集成,有了三个注解,项目启动后就可以直接用类似于以下的地址来查看api列表了:
http://127.0.0.1:8080/jadDemo/swagger-ui.html
这确实是一个很神奇的效果,简单的三个注解,系统就自动显示出项目里所有Controller
类的所有api
了。现在,我们就这个配置类入手,简单分析它的原理。这个类中没有任何代码,很显然,三个注解起了至关重要的作用。其中@Configuration
注解是spring
框架中本身就有的,它是一个被@Component
元注解标识的注解,所以有了这个注解后,spring
会自动把这个类实例化成一个bean
注册到spring
上下文中。第二个注解@EnableWebMvc
故名思义,就是启用springmvc
了,在Eclipse
中点到这个注解里面简单看一下,它就是通过元注解@Import(DelegatingWebMvcConfiguration.class)
往spring context
中塞入了一个DelegatingWebMvcConfiguration
类型的bean
。我想,这个类的目的应该就是为swagger提供了一些springmvc
方面的配置吧。第三个注解:@EnableSwagger2
,看名字应该可以想到,是用来集成swagger2
的,他通过元注解:@Import({Swagger2DocumentationConfiguration.class})
,又引入了一个Swagger2DocumentationConfiguration
类型的配置bean
,而这个就是Swagger
的核心配置了。它里面的代码如下:
@Configuration
@Import({ SpringfoxWebMvcConfiguration.class, SwaggerCommonConfiguration.class })
@ComponentScan(basePackages = {
"springfox.documentation.swagger2.readers.parameter",
"springfox.documentation.swagger2.web",
"springfox.documentation.swagger2.mappers"
})
publicclassSwagger2DocumentationConfiguration {
@Bean
public JacksonModuleRegistrar swagger2Module() {
returnnewSwagger2JacksonModule();
}
}
这个类头部通过一些注解,再引入SpringfoxWebMvcConfiguration
类和SwaggerCommonConfiguration
类,并通过ComponentScan
注解,自动扫描springfox .swagger2
相关的的bean
到spring context
中。这里,我最感兴趣的是SpringfoxWebMvcConfiguration
这个类,这个类我猜应该就是springfox
集成mvc
比较核心的配置了,点进去,看到以下代`码:
@Configuration
@Import({ModelsConfiguration.class })
@ComponentScan(basePackages = {
"springfox.documentation.spring.web.scanners",
"springfox.documentation.spring.web.readers.operation",
"springfox.documentation.spring.web.plugins",
"springfox.documentation.spring.web.paths"
})
@EnablePluginRegistries({
DocumentationPlugin.class,
ApiListingBuilderPlugin.class,
OperationBuilderPlugin.class,
ParameterBuilderPlugin.class,
ExpandedParameterBuilderPlugin.class,
ResourceGroupingStrategy.class,
OperationModelsProviderPlugin.class,
DefaultsProviderPlugin.class,
PathDecorator.class
})
publicclassSpringfoxWebMvcConfiguration {
}
这个类中下面的代码,无非就是通过@Bean
注解再加入一些新的Bean
,我对它的兴趣不是很大,我最感兴趣的是头部通过@EnablePluginRegistries
加入的那些东西。springfox
是基于spring-plug
的机制整合swagger
的,spring-plug
具体是怎么实现的,我暂时还没有时间去研究spring-plug
的原理。但在下文会提到自己写一个plug
插件来扩展swagger
的功能。上面通过@EnablePluginRegistries
加入的plug
中,还没有时间去看它全部的代码,目前我看过的代码主要有ApiListingBuilderPlugin.class
,OperationBuilderPlugin.class
,ParameterBuilderPlugin.class
, ExpandedParameterBuilderPlugin.class
,
第一个ApiListingBuilderPlugin
,它有两个实现类,分别是ApiListingReader
和SwaggerApiListingReader
。其中ApiListingReader
会自动跟据Controller
类型生成api
列表,而SwaggerApiListingReader
会跟据有@Api
注解标识的类生成api
列表。OperationBuilderPlugin
插件就是用来生成具体api文档的,这个类型的插件,有很多很多实现类,他们各自分工,各做各的事情,具体我没有仔细去看,只关注了其中一个实现类:OperationParameterReader
,这个类是用于读取api
参数的Plugin
。它依赖于ModelAttributeParameterExpander
工具类,可以将Controller
中接口方法参数中非简单类型的命令对像自动解析它内部的属性得出包含所有属性的参数列表(这里存在一个可能会出现无限递归的坑,下文有介绍)。而ExpandedParameterBuilderPlugin
插件,主要是用于扩展接口参数的一些功能,比如判断这个参数的数据类型以及是否为这个接口的必须参数等等。总体上说,整个springfox-swagger
内部其实是由这一系列的plug
转运起来的。他们在系统启动时,就被调起来,有些用来扫描出接口列表,有些用来读取接口参数等等。他们共同的目地就是把系统中所有api
接口都扫描出来,并缓存起来供用户查看。那么,这一系列表plug
到底是如何被调起来的,它们的执行入口倒底在哪?
我们把注意点放到上文SpringfoxWebMvcConfiguration
这个类代码头部的ComponentScan
注解内容上来,这一段注解中扫描了一个叫springfox.documentation.spring.web.plugins
的package
,这个package
在springfox-spring-web-2.6.1.jar
中可以找到。这个package
下,我们发现有两个非常核心的类,那就是DocumentationPluginsManager
和DocumentationPluginsBootstrapper
。对于第一个DocumentationPluginsManager
,它是一个没有实现任何接口的bean
,但它内部有诸多PluginRegistry
类型的属性,而且都是通过@Autowired
注解把属性值注入进来的。接合它的类名来看,很容易想到,这个就是管理所有plug的一个管理器了。很好理解,因为ComponentScan
注解的配置,所有的plug
实例都会被spring
实例化成一个bean
,然后被注入到这个DocumentationPluginsManager
实例中被统一管理起来。在这个package
中的另一个重要的类DocumentationPluginsBootstrapper
,看名字就可以猜到,他可能就是plug
的启动类了。点进去看具体时就可以发现,他果然是一个被@Component
标识了的组件,而且它的构造方法中注入了刚刚描述的DocumentationPluginsManager
实例,而且最关键的,它还实现了SmartLifecycle
接口。对spring bean
生命周期有所了解的人的都知道,这个组件在被实例化为一个bean
纳入srping context
中被管理起来的时候,会自动调用它的start()
方法。点到start()
中看代码时就会发现,它有一行代码scanDocumentation(buildContext(each));
就是用来扫描api
文档的。进一步跟踪这个方法的代码,就可以发现,这个方法最终会通过它的DocumentationPluginsManager
属性把所有plug
调起一起扫描整个系统并生成api文档。扫描的结果,缓存在DocumentationCache
这个类的一个map属性中。
以上就是,srpingMvc
整合springfox
的大致原理。它主要是通过EnableSwagger2
注解,向spring context
注入了一系列bean
,并在系统启动的时候自动扫描系统的Controller
类,生成相应的api
信息并缓存起来。此外,它还注入了一些被@Controller
注解标识的Controller
类,作为ui
模块访问api
列表的入口。比如springfox-swagger2-2.6.1.jar
包中的Swagger2Controller
类。这个Controller
就是ui
模块中用来访问api
列表的界面地址。在访问http://127.0.0.1:8080/jadDemo/swagger-ui.html
这个地址查看api
列表时,通过浏览器抓包就可以看到,它是通过类似于http://127.0.0.1:8080/jadDemo/v2/api-docs?group=sysGroup
这样的地址异步获得api
信息(Json
格式)并显示到界面上,这个地址后台对应的Controller
入口就是上文的Swagger2Controller
类,这个类收到请求后,直接从事先初始化好的缓存中的取出api信息生成json字符串返回。
了解了springfox
的原理,下面来看看springfox
使用过程中,我遇到的哪些坑。
springfox
第一大坑:配置类生成的bean
必须与spring mvc
共用同一个上下文。
前文描述了,在springmvc
项目中,集成springfox
是只要在项目写一个如下的没有任何业务代码的简单配置类就可以了。
@Configuration
@EnableWebMvc
@EnableSwagger2
publicclass ApiConfig {
}
因为@Configuration
注解的作用,spring
会自动把它实例化成一个bean
注入到上下文。但切记要注意的一个坑就是:这个bean
所在的上下文必须跟spring mvc
为同一个上下文。怎么解理呢?因为在实际的spring mvc
项目中,通常有两个上下文,一个是跟上下文,另一个是spring mvc
(它是跟上下文的子上下文)。其中跟上下文是就是web.xml
文件中跟spring
相关的那个org.springframework.web.context.request.RequestContextListener
监听器,加载起来的上下文,通常我们会写一个叫spring-contet.xml
的配置文件,这里面的bean
最终会初始化到跟上下文中,它主要包括系统里面的service,dao
等bean
,也包括数据源、事物等等。而另一个上下文是就是spring mvc
了,它通过web.xml
中跟spring mvc
相关的那个org.springframework.web.servlet.DispatcherServlet
加载起来,他通常有一个配置文件叫spring-mvc.xml
。我们在写ApiConfig
这个类时,如果决定用@Configuration
注解来加载,那么就必须保证这个类所在的路径刚好在springmvc
的component-scan
的配置的base-package
范围内。因为在ApiConfig
在被spring
加载时,会注入一列系列的bean
,而这些bean
中,为了能自动扫描出所有Controller
类,有些bean
需要依赖于SpringMvc
中的一些bean
,如果项目把Srpingmvc
的上下文与跟上下文分开来,作为跟上下文的子上下文的话。如果不小心让这个ApiConfig
类型的bean
被跟上文加载到,因为root context
中没有spring mvc
的context
中的那些配置类时就会报错。
实事上,我并不赞成通过@Configuration
注解来配置Swagger
,因为我认为,Swagger
的api
功能对于生产项目来说是可有可无的。我们Swagger
往往是用于测试环境供项目前端团队开发或供别的系统作接口集成使上。系统上线后,很可能在生产系统上隐藏这些api
列表。 但如果配置是通过@Configuration
注解写死在java
代码里的话,那么上线的时候想去掉这个功能的时候,那就尴尬了,不得不修改java代码重新编译。基于此,我推荐的一个方法,通过spring
最传统的xml文件配置方式。具体做法就是去掉@Configuration
注解,然后它写一个类似于<bean class=”com.jad.web.mvc.swagger.conf.ApiConfig"/>
这样的bean
配置到spring
的xml
配置文件中。在root context
与mvc
的context
分开的项目中,直接配置到spring-mvc.xml
中,这样就保证了它跟springmvc
的context
一定处于同一个context
中。
springfox第二大坑:Controller类的参数,注意防止出现无限递归的情况。
Spring mvc
有强大的参数绑定机制,可以自动把请求参数绑定为一个自定义的命令对像。所以,很多开发人员在写Controller
时,为了偷懒,直接把一个实体对像作为Controller方法的一个参数。比如下面这个示例代码:
@RequestMapping(value = “update”)
public String update(MenuVo menuVo, Model model){
}
这是大部分程序员喜欢在Controller
中写的修改某个实体的代码。在跟swagger
集成的时候,这里有一个大坑。如果MenuVo
这个类中所有的属性都是基本类型,那还好,不会出什么问题。但如果这个类里面有一些其它的自定义类型的属性,而且这个属性又直接或间接的存在它自身类型的属性,那就会出问题。例如:假如MenuVo
这个类是菜单类,在这个类时又含有MenuVo
类型的一个属性parent
代表它的父级菜单。这样的话,系统启动时swagger
模块就因无法加载这个api而直接报错。报错的原因就是,在加载这个方法的过程中会解析这个update
方法的参数,发现参数MenuVo
不是简单类型,则会自动以递归的方式解释它所有的类属性。这样就很容易陷入无限递归的死循环。
为了解决这个问题,我目前只是自己写了一个OperationParameterReader
插件实现类以及它依赖的ModelAttributeParameterExpander
工具类,通过配置的方式替换掉到springfox
原来的那两个类,偷梁换柱般的把参数解析这个逻辑替换掉,并避开无限递归。当然,这相当于是一种修改源码级别的方式。我目前还没有找到解决这个问题的更完美的方法,所以,只能建议大家在用spring-fox Swagger
的时候尽量避免这种无限递归的情况。毕竟,这不符合springmvc
命令对像的规范,springmvc
参数的命令对像中最好只含有简单的基本类型属性。
springfox第三大坑:api分组相关,Docket实例不能延迟加载
springfox
默认会把所有api
分成一组,这样通过类似于http://127.0.0.1:8080/jadDemo/swagger-ui.html
这样的地址访问时,会在同一个页面里加载所有api
列表。这样,如果系统稍大一点,api
稍微多一点,页面就会出现假死的情况,所以很有必要对api
进行分组。api
分组,是通过在ApiConf
这个配置文件中,通过@Bean
注解定义一些Docket
实例,网上常见的配置如下:
@EnableWebMvc
@EnableSwagger2
publicclass ApiConfig {
@Bean
public Docket customDocket() {
return newDocket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
}
}
上述代码中通过@Bean
注入一个Docket
,这个配置并不是必须的,如果没有这个配置,框架会自己生成一个默认的Docket
实例。这个Docket
实例的作用就是指定所有它能管理的api
的公共信息,比如api
版本、作者等等基本信息,以及指定只列出哪些api
(通过api
地址或注解过滤)。
Docket
实例可以有多个,比如如下代码:
@EnableWebMvc
@EnableSwagger2
publicclass ApiConfig {
@Bean
public Docket customDocket1() {
return newDocket(DocumentationType.SWAGGER_2)
.groupName(“apiGroup1”).apiInfo(apiInfo()).select()
.paths(PathSelectors.ant(“/sys/**”));
}
@Bean
public Docket customDocket2() {
return newDocket(DocumentationType.SWAGGER_2)
.groupName(“apiGroup2”).apiInfo(apiInfo())
.select()
.paths(PathSelectors.ant(“/shop/**”));
}
}
当在项目中配置了多个Docket
实例时,也就可以对api
进行分组了,比如上面代码将api
分为了两组。在这种情况下,必须给每一组指定一个不同的名称,比如上面代码中的apiGroup1
和apiGroup2
,每一组可以用paths
通过ant
风格的地址表达式来指定哪一组管理哪些api。比如上面配置中,第一组管理地址为/sys
/开头的api
第二组管理/shop/
开头的api
。当然,还有很多其它的过滤方式,比如跟据类注解、方法注解、地址正则表达式等等。分组后,在api
列表界面右上角的下拉选项中就可以选择不同的api
组。这样就把项目的api
列表分散到不同的页面了。这样,即方便管理,又不致于页面因需要加载太多api而假死。
然而,同使用@Configuration
一样,我并不赞成使用@Bean
来配置Docket
实例给api
分组。因为这样,同样会把代码写死。所以,我推荐在xml
文件中自己配置Docket
实例实现这些类似的功能。当然,考虑到Docket
中的众多属性,直接配置bean
比较麻烦,可以自己为Docket
写一个FactoryBean
,然后在xml
文件中配置FactoryBean
就行了。然而将Docket
配置到xml
中时。又会遇到一个大坑,就那是,spring
对bean
的加载方式默认是延迟加载的,在xml
中直接配置这些Docket
实例Bean
后。你会发现,没有一点效果,页面左上角的下拉列表中跟本没有你的分组项。
这个问题曾困扰过我好几个小时,后来凭经验推测出可能是因为sping bean
默认延迟加载,这个Docket
实例还没加载到spring context
中。实事证明,我的猜测是对的。我不知道这算是springfox
的一个bug
,还是因为我跟本不该把对Docket
的配置从原来的java
代码中搬到xml
配置文件中来。
springfox
其它的坑:springfox
还有些其它的坑,比如@ApiOperation
注解中,如果不指定httpMethod
属性具体为某个get
或post
方法时,api
列表中,会它get
,post
,delete
,put
等所有方法都列出来,搞到api
列表重复的太多,很难看。另外,还有在测试时,遇到登录权限问题,等等。这一堆堆的比较容易解决的小坑,因为篇幅有限,我就不多说了。还有比如@Api
、@ApiOperation
及@ApiParam
等等注解的用法,网上很多这方面的文档,我就不重复了。
对Java技术,架构技术感兴趣的同学,欢迎加QQ群619881427,一起学习,相互讨论。
群内已经有小伙伴将知识体系整理好(源码,笔记,PPT,学习视频),欢迎加群免费领取。
分享给喜欢Java的,喜欢编程,有梦想成为架构师的程序员们,希望能够帮助到你们。
不是Java的程序员也没关系,帮忙转发给更多朋友!谢谢。
一个分享小技巧点击阅读原文也。可以轻松到电子杂志学习资料哦!