Swagger原理解析

swagger簡介

swagger確實是個好東西,可以跟據業務代碼自動生成相關的api接口文檔,尤其用於restful風格中的項目,開發人員幾乎可以不用專門去維護rest api,這個框架可以自動爲你的業務代碼生成restfut風格的api,而且還提供相應的測試界面,自動顯示json格式的響應。大大方便了後臺開發人員與前端的溝通與聯調成本。

springfox-swagger簡介

籤於swagger的強大功能,Java開源界大牛spring框架迅速跟上,它充分利用自已的優勢,把swagger集成到自己的項目裏,整了一個spring-swagger,後來便演變成springfoxspringfox本身只是利用自身的aop的特點,通過plug的方式把swagger集成了進來,它本身對業務api的生成,還是依靠swagger來實現。

關於這個框架的文檔,網上的資料比較少,大部分是入門級的簡單使用。本人在集成這個框架到自己項目的過程中,遇到了不少坑,爲了解決這些坑,我不得不扒開它的源碼來看個究竟。此文,就是記述本人在使用springfox過程中對springfox的一些理解以及需要注意的地方。

springfox大致原理

springfox的大致原理就是,在項目啓動的過種中,spring上下文在初始化的過程,框架自動跟據配置加載一些swagger相關的bean到當前的上下文中,並自動掃描系統中可能需要生成api文檔那些類,並生成相應的信息緩存起來。如果項目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

上面三個依賴是項目集成springmvcspringfox最基本的依賴,其它的依賴這裏省略。其中第一個是springmvc的基本依賴,第二個是swagger依賴,第三個是界面相關的依賴,這個不是必須的,如果你不想用springfox自帶的api界面的話,也可以不用這個,而另外自己寫一套適合自己項目的界面。加入這幾個依賴後,系統後會自動加入一些跟springfoxswagger相關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之外,它還需要guavaspring-plugjackson等依賴包(注意jackson是用於生成json必須的jar包,如果項目裏本身沒有加入這個依賴,爲了集成swagger的話必須額外再加入這個依賴)。

springfox的簡單使用

如果只用springfox的默認的配置的話,與springmvc集成起來非常簡單,只要寫一個類似於以下代碼的類放到你的項目裏就行了,代碼如下:

@Configuration
@EnableWebMvc
@EnableSwagger2
publicclass ApiConfig {

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

注意到,上面是一個空的java類文件,類名可以隨意指定,但必須加入上述類中標出的@Configuration@EnableWebMvc@EnableSwagger2三個註解,這樣就完成了springmvcspringfox的基本集成,有了三個註解,項目啓動後就可以直接用類似於以下的地址來查看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相關的的beanspring 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.classExpandedParameterBuilderPlugin.class,

  第一個ApiListingBuilderPlugin,它有兩個實現類,分別是ApiListingReaderSwaggerApiListingReader。其中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.pluginspackage,這個packagespringfox-spring-web-2.6.1.jar中可以找到。這個package下,我們發現有兩個非常核心的類,那就是DocumentationPluginsManagerDocumentationPluginsBootstrapper。對於第一個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,daobean,也包括數據源、事物等等。而另一個上下文是就是spring mvc了,它通過web.xml中跟spring mvc相關的那個org.springframework.web.servlet.DispatcherServlet加載起來,他通常有一個配置文件叫spring-mvc.xml。我們在寫ApiConfig這個類時,如果決定用@Configuration註解來加載,那麼就必須保證這個類所在的路徑剛好在springmvccomponent-scan的配置的base-package範圍內。因爲在ApiConfig在被spring加載時,會注入一列系列的bean,而這些bean中,爲了能自動掃描出所有Controller類,有些bean需要依賴於SpringMvc中的一些bean,如果項目把Srpingmvc的上下文與跟上下文分開來,作爲跟上下文的子上下文的話。如果不小心讓這個ApiConfig類型的bean被跟上文加載到,因爲root context中沒有spring mvccontext中的那些配置類時就會報錯。 
  實事上,我並不贊成通過@Configuration註解來配置Swagger,因爲我認爲,Swaggerapi功能對於生產項目來說是可有可無的。我們Swagger往往是用於測試環境供項目前端團隊開發或供別的系統作接口集成使上。系統上線後,很可能在生產系統上隱藏這些api列表。 但如果配置是通過@Configuration註解寫死在java代碼裏的話,那麼上線的時候想去掉這個功能的時候,那就尷尬了,不得不修改java代碼重新編譯。基於此,我推薦的一個方法,通過spring最傳統的xml文件配置方式。具體做法就是去掉@Configuration註解,然後它寫一個類似於<bean class=”com.jad.web.mvc.swagger.conf.ApiConfig"/>這樣的bean配置到springxml配置文件中。在root contextmvccontext分開的項目中,直接配置到spring-mvc.xml中,這樣就保證了它跟springmvccontext一定處於同一個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分爲了兩組。在這種情況下,必須給每一組指定一個不同的名稱,比如上面代碼中的apiGroup1apiGroup2,每一組可以用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中時。又會遇到一個大坑,就那是,springbean的加載方式默認是延遲加載的,在xml中直接配置這些Docket實例Bean後。你會發現,沒有一點效果,頁面左上角的下拉列表中跟本沒有你的分組項。

  這個問題曾困擾過我好幾個小時,後來憑經驗推測出可能是因爲sping bean默認延遲加載,這個Docket實例還沒加載到spring context中。實事證明,我的猜測是對的。我不知道這算是springfox的一個bug,還是因爲我跟本不該把對Docket的配置從原來的java代碼中搬到xml配置文件中來。 
  springfox其它的坑:springfox還有些其它的坑,比如@ApiOperation註解中,如果不指定httpMethod屬性具體爲某個getpost方法時,api列表中,會它get,post,delete,put等所有方法都列出來,搞到api列表重複的太多,很難看。另外,還有在測試時,遇到登錄權限問題,等等。這一堆堆的比較容易解決的小坑,因爲篇幅有限,我就不多說了。還有比如@Api@ApiOperation@ApiParam等等註解的用法,網上很多這方面的文檔,我就不重複了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章