隨着前后端的分離,借口文檔變的尤其重要,springfox是通過注解的形式自動生成API文檔,利用它,可以很方便的書寫restful API,swagger主要用於展示springfox生成的API文檔。
官網地址:http://springfox.github.io/springfox/
Springfox大致原理
springfox的大致原理就是,在項目啟動的過種中,spring上下文在初始化的過程,框架自動跟據配置加載一些swagger相關的bean到當前的上下文中,並自動掃描系統中可能需要生成api文檔那些類,並生成相應的信息緩存起來。如果項目MVC控制層用的是springMvc那么會自動掃描所有Controller類,跟據這些Controller類中的方法生成相應的api文檔。
Spring集成Springfox步驟及說明:
一、添加Swagger2依賴
<!-- Swagger --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.6.1</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.6.1</version> </dependency>
二、application.properties中添加配置
#解決中文亂碼問題 banner.charset=UTF-8 server.tomcat.uri-encoding=UTF-8 spring.http.encoding.charset=UTF-8 spring.http.encoding.enabled=true spring.http.encoding.force=true spring.messages.encoding=UTF-8 #Swagger Configure Properties sop.swagger.enable=true sop.swagger.packageScan=com.example sop.swagger.title=UserController Restfull API sop.swagger.description=UserController Restfull API sop.swagger.version=3.0
三、創建SwaggerConfigProperties加載配置項
package com.example.config; import java.io.Serializable; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @ConfigurationProperties(prefix = "sop.swagger") @Component public class SwaggerConfigProperties implements Serializable { /** * 是否開啟Swagger */ private boolean enable = false; /** * 要掃描的包 */ private String packageScan; /** * 標題 */ private String title; /** * 描述 */ private String description; /** * 版本信息 */ private String version; public boolean isEnable() { return enable; } public void setEnable(boolean enable) { this.enable = enable; } public String getPackageScan() { return packageScan; } public void setPackageScan(String packageScan) { this.packageScan = packageScan; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } }
四、創建Swagger2配置類
package com.example.config;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 public class Swagger2 { @Autowired private SwaggerConfigProperties scp; @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.example.web")) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(scp.getTitle()) .description(scp.getDescription()) .version("1.0") .build(); } }
五、創建model
package com.example.model; import io.swagger.annotations.ApiModelProperty; public class User { @ApiModelProperty(value = "主鍵") private Long id; @ApiModelProperty(value = "名字") private String name; @ApiModelProperty(value = "年齡") private Integer age; @ApiModelProperty(value = "密碼") private String password; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", password=" + password + '}'; } }
六、創建Controller
package com.example.web;
import java.util.Collections; import java.util.HashMap; import java.util.Map; import com.example.model.User; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import springfox.documentation.annotations.ApiIgnore; @RestController @Api("userController相關api") public class UserController { @ApiOperation("獲取用戶信息") @ApiImplicitParams({ @ApiImplicitParam(paramType = "header", name = "username", dataType = "String", required = true, value = "用戶的姓名", defaultValue = "xiaoqiang"), @ApiImplicitParam(paramType = "query", name = "password", dataType = "String", required = true, value = "用戶的密碼", defaultValue = "xiaoxiong") }) @ApiResponses({ @ApiResponse(code = 400, message = "請求參數沒填好"), @ApiResponse(code = 404, message = "請求路徑沒有或頁面跳轉路徑不對") }) @RequestMapping(value = "/getUser", method = RequestMethod.GET) public User getUser(@RequestHeader("username") String username, @RequestParam("password") String password) { User user = new User(); user.setName(username); user.setPassword(password); return user; } @ApiOperation(value = "創建用戶", notes = "根據User對象創建用戶") @ApiImplicitParam(name = "user", value = "用戶詳細實體user", required = true, dataType = "User") @RequestMapping(value = "", method = RequestMethod.POST) public String postUser(@RequestBody User user) { Map<Long, User> users = Collections.synchronizedMap(new HashMap<Long, User>()); users.put(user.getId(), user); return "success"; } @ApiIgnore @RequestMapping(value = "/", method = RequestMethod.GET) public String home() { return "hello"; } }
完成上述代碼添加上,啟動Spring Boot程序,訪問:http://localhost:8080/swagger-ui.html
。就能看到前文所展示的RESTful API的頁面。我們可以再點開具體的API請求,以POST類型的/users請求為例,可找到上述代碼中我們配置的Notes信息以及參數user的描述信息,如下圖所示。
七、API文檔訪問與調試
在上圖請求的頁面中,我們看到user的Value是個輸入框?是的,Swagger除了查看接口功能外,還提供了調試測試功能,我們可以點擊上圖中右側的Model Schema(黃色區域:它指明了User的數據結構),此時Value中就有了user對象的模板,我們只需要稍適修改,點擊下方“Try it out!”按鈕,即可完成了一次請求調用!
此時,你也可以通過幾個GET請求來驗證之前的POST請求是否正確。
相比為這些接口編寫文檔的工作,我們增加的配置內容是非常少而且精簡的,對於原有代碼的侵入也在忍受范圍之內。因此,在構建RESTful API的同時,加入swagger來對API文檔進行管理,是個不錯的選擇。
八、springfox、swagger.annotations注解部分參數介紹
在上面只展示了如何使用,這里將對上面添加的swagger注解進行說明,筆記使用時參考了swagger annotations Api 手冊,接下來進行部分常用注解使用說明介紹。
- @ApiIgnore 忽略注解標注的類或者方法,不添加到API文檔中
- @ApiOperation 展示每個API基本信息
- value api名稱
- notes 備注說明
- @ApiImplicitParam 用於規定接收參數類型、名稱、是否必須等信息
- name 對應方法中接收參數名稱
- value 備注說明
- required 是否必須 boolean
- paramType 參數類型 body、path、query、header、form中的一種
- body 使用@RequestBody接收數據 POST有效
- path 在url中配置{}的參數
- query 普通查詢參數 例如 ?query=q ,jquery ajax中data設置的值也可以,例如 {query:”q”},springMVC中不需要添加注解接收
- header 使用@RequestHeader接收數據
- form 筆者未使用,請查看官方API文檔
- dataType 數據類型,如果類型名稱相同,請指定全路徑,例如 dataType = “java.util.Date”,springfox會自動根據類型生成模型
- @ApiImplicitParams 包含多個@ApiImplicitParam
- @ApiModelProperty 對模型中屬性添加說明,例如 上面的PageInfoBeen、BlogArticleBeen這兩個類中使用,只能使用在類中。
- value 參數名稱
- required 是否必須 boolean
- hidden 是否隱藏 boolean
- 其他信息和上面同名屬性作用相同,hidden屬性對於集合不能隱藏,目前不知道原因
- @ApiParam 對單獨某個參數進行說明,使用在類中或者controller方法中都可以。注解中的屬性和上面列出的同名屬性作用相同
其他注解:https://github.com/swagger-api/swagger-core/wiki/Annotations#apimodel
九、springfox中的那些坑
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工具類,通過配置的方式替換掉到srpingfox原來的那兩個類,偷梁換柱般的把參數解析這個邏輯替換掉,並避開無限遞歸。當然,這相當於是一種修改源碼級別的方式。我目前還沒有找到解決這個問題的更完美的方法,所以,只能建議大家在用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 public class ApiConfig { @Bean public Docket customDocket() { return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()); } }
上述代碼中通過@Bean注入一個Docket,這個配置並不是必須的,如果沒有這個配置,框架會自己生成一個默認的Docket實例。這個Docket實例的作用就是指定所有它能管理的api的公共信息,比如api版本、作者等等基本信息,以及指定只列出哪些api(通過api地址或注解過濾)。
Docket實例可以有多個,比如如下代碼:
@EnableWebMvc @EnableSwagger2 public class ApiConfig { @Bean public Docket customDocket1() { return new Docket(DocumentationType.SWAGGER_2) .groupName("apiGroup1").apiInfo(apiInfo()).select() .paths(PathSelectors.ant("/sys/**")); } @Bean public Docket customDocket2() { return new Docket(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等等注解的用法,網上很多這方面的文檔,我就不重復了。
開源:Swagger Butler 1.1.0發布,利用ZuulRoute信息簡化配置內容
Swagger Butler是一個基於Swagger與Zuul構建的API文檔匯集工具。通過構建一個簡單的Spring Boot應用,增加一些配置就能將現有整合了Swagger的Web應用的API文檔都匯總到一起,方便查看與測試。
項目地址
快速入門
該工具的時候非常簡單,先通過下面幾步簡單入門:
第一步:構建一個基礎的Spring Boot應用
如您還不知道如何創建Spring Boot應用,可以先閱讀本篇入門文章
第二步:在pom.xml中引入依賴
<parent> |
第三步:創建應用主類,增加@EnableSwaggerButler
注解開啟Swagger Butler功能
|
第四步:配置文件中增加Swagger文檔的地址配置
spring.application.name=swagger-butler-example-static |
上面配置了兩個文檔位置,由於這里還沒有引入服務發現機制,所以Zuul的路由需要我們自己配置。然后在配置resource信息的時候,從1.1.0版本開始做了較大的調整,由於具體的訪問路徑是可以通過路由信息產生的,所以對於resource的配置信息只關注三個內容:
name
:API文檔在swagger中展現名稱api-docs-path
:要獲取的swagger文檔的具體路徑;如果不配置會使用全局的swagger.butler.api-docs-path
配置,默認為/v2/api-docs
。;這里的配置主要用戶一些特殊情況,比如服務自身設置了context-path,或者修改了swagger默認的文檔路徑swagger-version
:swagger版本信息;如果不配置會使用全局的swagger.butler.swagger-version
配置,默認為2.0
。
第五步:訪問http://localhost:11000/swagger-ui.html
代碼示例具體可見
swagger-butler-example-static
目錄
Zuul的路由與SwaggerResources配置之間的關系
如上示例中<route-name>
展示了Zuul的路由名稱與SwaggerResources配置之間的關聯關系
zuul.routes.<route-name>.path=/service-b/** |
注意:在沒有使用自動配置或整合服務治理的時候,要生成Swagger文檔的時候,resources信息中的
name
屬性是必須配置的,api-docs-path
和swagger-version
不配置的時候會使用默認的全局配置
全局配置
對於Swagger文檔獲取的全局配置內容,目前主要包含下面幾個參數:
swagger.butler.api-docs-path=/v2/api-docs |
使用Zuul中的路由自動配置(新特性)
在快速入門示例中我們配置了兩個路由信息,同時為這兩個路由信息配置了對應的Swagger信息來獲取API文檔詳情,從1.1.0版本開始,增加了幾個通過Zuul的路由配置來自動生成文檔信息的參數,這樣可以減少快速入門示例中那些繁瑣的配置。對於快速入門例子,我們可以做如下改造:
# swagger resource |
在設置了swagger.butler.auto-generate-from-zuul-routes=true
之后會默認的根據zuul中的路由信息來生成SwaggerResource。其中,原來resource中的name
會使用zuul route的名稱(比如:上面的user和product),而api-docs-path
和swagger-version
配置會使用默認的全局配置。如果resource中的三個參數有特殊情況要處理,可以采用快速入門中的配置方式來特別指定即可。
忽略某些路由生成
# swagger resource |
如上示例,通過swagger.butler.ignore-routes
參數可以從當前配置的路由信息中排除某些路由內容不生成文檔,配置內容為zuul中的路由名稱,配置多個的時候使用,
分割。
注意:
swagger.butler.ignore-routes
和swagger.butler.generate-routes
不能同時配置。這兩個參數都不配置的時候,默認為zuul中的所有路由生成文檔。
指定某些路由生成
# swagger resource |
如上示例,通過swagger.butler.generate-routes
參數可以從當前配置的路由信息中指定某些路由內容生成文檔,配置內容為zuul中的路由名稱,配置多個的時候使用,
分割。
注意:
swagger.butler.ignore-routes
和swagger.butler.generate-routes
不能同時配置。這兩個參數都不配置的時候,默認為zuul中的所有路由生成文檔。
與服務治理整合
與eureka整合
在整合eureka獲取所有該注冊中心下的API文檔時,只需要在上面工程的基礎上增加下面的配置:
第一步:pom.xml
中增加eureka
依賴,比如:
<dependencies> |
第二步:應用主類增加@EnableDiscoveryClient
,比如:
|
第三步:修改配置文件,增加eureka的配置,比如:
spring.application.name=swagger-butler-example-eureka |
由於整合了eureka之后,zuul會默認為所有注冊服務創建路由配置(默認的路由名為服務名),所以只需要通過swagger.butler.auto-generate-from-zuul-routes=true
參數開啟根據路由信息生成文檔配置的功能,配合swagger.butler.ignore-routes
和swagger.butler.generate-routes
參數就可以指定要生成的范圍了,如果某些服務需要特殊配置,也可以通過wagger.butler.resources.*
的配置來覆蓋默認設置,比如上面的swagger.butler.resources.swagger-service-b.api-docs-path=/xxx/v2/api-docs
指定了swagger-service-b
服務獲取swagger文檔的請求路徑為/xxx/v2/api-docs
。
代碼示例具體可見
swagger-butler-example-eureka
目錄
與consul整合
在整合eureka獲取所有該注冊中心下的API文檔時,只需要在上面工程的基礎上增加下面的配置:
第一步:pom.xml
中增加consul
依賴,比如:
<dependencies> |
第二步:應用主類增加@EnableDiscoveryClient
,比如:
|
第三步:配置文件中增加eureka的配置,比如:
spring.application.name=swagger-butler-example-consul |
這里除了consul自身的配置之外,其他內容與整合eureka時候的是一樣的。
代碼示例具體可見
swagger-butler-example-consul
目錄