springcloud+swagger微服務環境下實現文檔管理
需求
springcloud是多個模塊的,怎麼用Swagger管理接口呢?
比如我的微服務有以下模塊
-
eureka
-
gateway(zuul)
-
user-service
-
order-service
其中user和order模塊需要暴露swagger文檔,那
-
方案一:通過網關聚合成一個文檔,通過分組來切換不同模塊的文檔
-
方案二:要訪問哪個模塊的文檔就去對應的模塊訪問
-
user模塊的文檔:http://127.0.0.1:8020/swagger-ui.html 或 http://127.0.0.1:8080/user/swagger-ui.html(通過網關)
-
order模塊的文檔:http://127.0.0.1:8010/swagger-ui.html 或 http://127.0.0.1:8080/order/swagger-ui.html(通過網關)
-
1. 方案一
1.1 步驟
-
在user和order的pom.xml裏,都要這麼操作:引入springfox-swagger2,無需引入ui,ui交給網。有包管理的請自行將版本提取到parent模塊裏
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> <exclusions> <exclusion> <groupId>io.swagger</groupId> <artifactId>swagger-models</artifactId> </exclusion> <exclusion> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-models</artifactId> <version>1.6.0</version> </dependency> <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> <version>1.6.0</version> </dependency>
-
在user和order模塊裏,建Swagger2Config配置類,內容如下(請根據具體的需求看下要不要修改basePackage,這是指定要掃描什麼路徑)
package com.wyf.test.user.config; import io.swagger.models.Swagger; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 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; @ConditionalOnClass(value = {Swagger.class}) @Configuration @EnableSwagger2 public class Swagger2Config { @Value("${spring.application.name}") private String applicationName; @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.wyf.test")) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(applicationName + "接口文檔") .description(applicationName + "接口文檔") .version("1.0") .build(); } }
-
在Gateway的模塊的pom.xml裏新增swagger2和ui,兩個都要。有包管理的請自行將版本提取到parent模塊裏
<!-- Swagger2 BEGIN --> <!-- swagger2包中swagger-models版本有bug,example爲空串會爆出NumberFormatException,需要排除並引入高版本 排除時將 swagger-models 和 swagger-annotations 整一對排除 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> <exclusions> <exclusion> <groupId>io.swagger</groupId> <artifactId>swagger-models</artifactId> </exclusion> <exclusion> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-models</artifactId> <version>1.6.0</version> </dependency> <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> <version>1.6.0</version> </dependency> <!-- 不滿意可以註釋掉換其他UI,可以同時開啓多個UI --> <!-- 官方UI,請求路徑 http://{host}:{port}/swagger-ui.html --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <!-- bootstrap-ui,請求路徑:http://{host}:{port}/doc.html,覺得是最好的UI --> <!-- <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>swagger-bootstrap-ui</artifactId> <version>1.9.6</version> </dependency> --> <!-- Swagger2 END -->
-
在Gateway模塊裏新增Swagger2Config的配置
@Configuration @EnableSwagger2 public class Swagger2Config { @Bean public Docket docket() { // basePackage 需要掃描註解生成文檔的路徑 return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.wyf.test.gateway")) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("Swagger Entrance provided by GATEWAY") .description("這是展示通過網關聚合所有子模塊的swagger文檔的例子") .version("1.0").build(); } }
-
在Gateway模塊,要增加 “發現Swagger服務的各個微服務” 的邏輯。這部分的作用就是取得微服務模塊,然後拼出這些微服務模塊的Swagger文檔的路徑。而且這個類定義爲@Primary,會替代默認的那個
package com.wyf.test.gateway.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.netflix.zuul.filters.Route; import org.springframework.cloud.netflix.zuul.filters.RouteLocator; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; import springfox.documentation.swagger.web.SwaggerResource; import springfox.documentation.swagger.web.SwaggerResourcesProvider; import java.util.ArrayList; import java.util.List; @Component @Primary public class GatewaySwaggerResourcesProvider implements SwaggerResourcesProvider { @Autowired private RouteLocator routeLocator; @Override public List<SwaggerResource> get() { List<SwaggerResource> resources = new ArrayList<>(); List<Route> routes = routeLocator.getRoutes(); // 剛啓動Gateway的時候,Route只有2個,過個30秒,就註冊上來了,然後又4個,發現這4個有點重複 // 2個 // [{"id":"api-a","fullPath":"/user/**","path":"/**","location":"user-service","prefix":"/user","retryable":false,"sensitiveHeaders":[],"customSensitiveHeaders":false} // ,{"id":"api-b","fullPath":"/order/**","path":"/**","location":"order-service","prefix":"/order","retryable":false,"sensitiveHeaders":[],"customSensitiveHeaders":false}] // 4個(在排序上並不是按照id的字典升序,而是先會出現Gateway配置文件裏的) // [{"id":"api-a","fullPath":"/user/**","path":"/**","location":"user-service","prefix":"/user","retryable":false,"sensitiveHeaders":[],"customSensitiveHeaders":false} // ,{"id":"api-b","fullPath":"/order/**","path":"/**","location":"order-service","prefix":"/order","retryable":false,"sensitiveHeaders":[],"customSensitiveHeaders":false} // ,{"id":"order-service","fullPath":"/order-service/**","path":"/**","location":"order-service","prefix":"/order-service","retryable":false,"sensitiveHeaders":[],"customSensitiveHeaders":false} // ,{"id":"user-service","fullPath":"/user-service/**","path":"/**","location":"user-service","prefix":"/user-service","retryable":false,"sensitiveHeaders":[],"customSensitiveHeaders":false}] // 每個route會生成一個分組,因爲route有重複,我們需要去掉重複的,這樣界面好看點 // 如果location一樣就認爲重複了,好像route的排序會優先api-a和api-b,並不是因爲api List<String> alreadyAdded = new ArrayList<>(); for (Route route : routes) { if (!alreadyAdded.contains(route.getLocation())) { // 構造文檔的路徑(因爲routes的順序是先出現Gateway配置文件裏的,它的prefix是 /user、/order 而不是 /user-service、/order-server 這些,所以使用前者是比較好的,不會逼死強迫症 // (如果使用後者,請求網關的接口會變成http://xxx/user-service/user/get,而顯然http://xxx/user/user/get纔是 "正統" String docPath = route.getPrefix() + "/v2/api-docs"; // 用於顯示的分組名的字段,可自行決定 String groupName = route.getLocation(); resources.add(getSwaggerResource(groupName, docPath, "1.0")); } alreadyAdded.add(route.getLocation()); } return resources; } private SwaggerResource getSwaggerResource(String name, String location, String version) { SwaggerResource swaggerResource = new SwaggerResource(); swaggerResource.setName(name); swaggerResource.setLocation(location); swaggerResource.setSwaggerVersion(version); return swaggerResource; } }
-
啓動整個微服務,訪問文檔
訪問網關的地址,加上/swagger-ui.html,如果你用了別的ui,請用他們的xxx.html。有時候會報錯,需要等待一會等微服務註冊好了就恢復了。效果頁面如圖,切換分組處可以看到各模塊的文檔,可以正常調試接口。
1.2 這種方案的缺陷
- 所有註冊到eureka裏的服務,都會被當做存在swagger文檔出現在
Select a spec
列表裏,但是實際切換過去,當不存在的文檔時出錯
-
微服務 “延遲” 的問題
即微服務啓動後,有時候各服務並不是那麼及時註冊到註冊中心,存在延遲,所以swagger文檔可能會刷出錯誤,但是等一段時間(一般最多30秒左右)就能刷出來了。
-
從模塊中調試接口,是實際請求服務,還是通過網關請求的?
是通過網關的,看圖便知,另外默認不配置網關永不超時,會在斷點阻礙時間過長的時候斷開
1.3 換UI的問題
同樣支持換UI,在網關處改swagger ui的GAV
1.4 對於多服務節點
假如eureka、user、order都佈置多節點,會不會有問題? 實測在這種情況下,Swagger文檔也是沒問題的。多個user節點,並不會產生多個Swagger文檔分組!
2. 方案二(不建議)
就是在user和order模塊裏,和單springboot集成swagger的方法沒區別。
以下對user和order同樣的操作
-
修改pom.xml(swagger2和ui都要引入)
<!-- Swagger2 BEGIN --> <!-- swagger2包中swagger-models版本有bug,example爲空串會爆出NumberFormatException,需要排除並引入高版本 排除時將 swagger-models 和 swagger-annotations 整一對排除 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> <exclusions> <exclusion> <groupId>io.swagger</groupId> <artifactId>swagger-models</artifactId> </exclusion> <exclusion> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-models</artifactId> <version>1.6.0</version> </dependency> <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> <version>1.6.0</version> </dependency> <!-- 不滿意可以註釋掉換其他UI,可以同時開啓多個UI --> <!-- 官方UI,請求路徑 http://{host}:{port}/swagger-ui.html --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <!-- bootstrap-ui,請求路徑:http://{host}:{port}/doc.html,覺得是最好的UI --> <!-- <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>swagger-bootstrap-ui</artifactId> <version>1.9.6</version> </dependency> --> <!-- ui-layer,請求路徑:http://{host}:{port}/docs.html,覺得沒比原生的好 --> <!--<dependency> <groupId>com.github.caspar-chen</groupId> <artifactId>swagger-ui-layer</artifactId> <version>1.1.3</version> </dependency>--> <!-- Swagger2 END -->
-
新增swagger配置(user和order模塊改下包掃描路徑,改下title)
@Configuration @EnableSwagger2 public class Swagger2Config { @Bean public Docket docket() { // basePackage 需要掃描註解生成文檔的路徑 return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.wyf.test")) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("Order-service API") .description("") .version("1.0").build(); } }
3. 如何共存多套UI
其實是可以共存多個UI的,實際測試可行。注意有時候會發現其中一個UI能刷出來另外一個不行,可以稍微等一等再試,或者調換下一下兩個UI的順序再試下。總之我遇到過 “其中一個UI可以另外一個不行” 的情況,但是最終發現 “兩個UI可以並存”