swagger
簡介
swagger
確實是個好東西,可以跟據業務代碼自動生成相關的api
接口文檔,尤其用於restful
風格中的項目,開發人員幾乎可以不用專門去維護rest
api
,這個框架可以自動爲你的業務代碼生成restfut
風格的api
,而且還提供相應的測試界面,自動顯示json
格式的響應。大大方便了後臺開發人員與前端的溝通與聯調成本。
springfox-swagger
簡介
籤於swagger
的強大功能,Java
開源界大牛spring
框架迅速跟上,它充分利用自已的優勢,把swagger
集成到自己的項目裏,整了一個spring-swagger
,後來便演變成springfox
。springfox
本身只是利用自身的aop
的特點,通過plug
的方式把swagger
集成了進來,它本身對業務api
的生成,還是依靠swagger
來實現。
關於這個框架的文檔,網上的資料比較少,大部分是入門級的簡單使用。本人在集成這個框架到自己項目的過程中,遇到了不少坑,爲了解決這些坑,我不得不扒開它的源碼來看個究竟。此文,就是記述本人在使用springfox
過程中對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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
上面三個依賴是項目集成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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
上面是我通過目測覺得springfox
可能需要的jar
,可能沒有完全例出springfox
所需的所有jar
。從上面jar
可以看出springfox
除了依賴swagger
之外,它還需要guava
、spring-plug
、jackson
等依賴包(注意jackson
是用於生成json
必須的jar
包,如果項目裏本身沒有加入這個依賴,爲了集成swagger
的話必須額外再加入這個依賴)。
springfox
的簡單使用
如果只用springfox
的默認的配置的話,與springmvc
集成起來非常簡單,只要寫一個類似於以下代碼的類放到你的項目裏就行了,代碼如下:
@Configuration
@EnableWebMvc
@EnableSwagger2
publicclass ApiConfig {
}
- 1
- 2
- 3
- 4
- 5
- 6
注意到,上面是一個空的java
類文件,類名可以隨意指定,但必須加入上述類中標出的@Configuration
、@EnableWebMvc
、@EnableSwagger2
三個註解,這樣就完成了springmvc
與springfox
的基本集成,有了三個註解,項目啓動後就可以直接用類似於以下的地址來查看api列表了:
http://127.0.0.1:8080/jadDemo/swagger-ui.html
- 1
這確實是一個很神奇的效果,簡單的三個註解,系統就自動顯示出項目裏所有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();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
這個類頭部通過一些註解,再引入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 {
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
這個類中下面的代碼,無非就是通過@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 {
}
- 1
- 2
- 3
- 4
- 5
- 6
因爲@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){
}
- 1
- 2
- 3
- 4
這是大部分程序員喜歡在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());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
上述代碼中通過@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/**”));
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
當在項目中配置了多個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
等等註解的用法,網上很多這方面的文檔,我就不重複了。