Spring Cloud集成Knife4j管理接口文檔
?Knife4j前身是swagger-bootstrap-ui,取名knife4j是希望她能像一把匕首一樣小巧,輕量,并且功能強悍,更名也是希望把她做成一個為Swagger接口文檔服務(wù)的通用性解決方案,不僅僅只是專注于前端Ui前端.雖然目前還只是在前端,但以后功能肯定不止于此。
2.0版本主要是使用Vue+Ant Design對前端Ui進行重寫,該版本是真正的前后端分離版本,同時依賴于Vue的技術(shù)生態(tài),以后會有更多有趣的功能實現(xiàn),全方位滿足開發(fā)者的需要。
Knife4j簡介
核心功能
該UI增強包主要包括兩大核心功能:文檔說明 和 在線調(diào)試-
文檔說明
:根據(jù)Swagger的規(guī)范說明,詳細列出接口文檔的說明,包括接口地址、類型、請求示例、請求參數(shù)、響應(yīng)示例、響應(yīng)參數(shù)、響應(yīng)碼等信息,使用
Knife4j能能根據(jù)該文檔說明,對該接口的使用情況一目了然。 -
在線調(diào)試 :提供在線接口聯(lián)調(diào)的強大功能,自動解析當(dāng)前接口參數(shù),同時包含表單驗證,調(diào)用參數(shù)可返回接口響應(yīng)內(nèi)容、headers、Curl請求命令實例、響應(yīng)時間、響應(yīng)狀態(tài)碼等信息,幫助開發(fā)者在線調(diào)試,而不必通過其他測試工具測試接口是否正確,簡介、強大。
UI增強
Knife4j
在滿足以上功能的同時,還提供了文檔的增強功能,這些功能是官方
swagger-ui
所沒有的,每一個增強的功能都是貼合實際,考慮到開發(fā)者的實際開發(fā)需要,是必不可少的功能,主要包括:
- 個性化配置 :通過個性化ui配置項,可自定義UI的相關(guān)顯示信息
-
離線文檔:根據(jù)標(biāo)準(zhǔn)規(guī)范,生成的在線
markdown離線文檔,開發(fā)者可以進行拷貝生成markdown接口文檔,通過其他第三方markdown轉(zhuǎn)換工具轉(zhuǎn)換成html或pdf,這樣也可以放棄swagger2markdown組件 -
接口排序 :自1.8.5后,ui支持了接口排序功能,例如一個注冊功能主要包含了多個步驟,可以根據(jù)
Knife4j提供的接口排序規(guī)則實現(xiàn)接口的排序,step化接口操作,方便其他開發(fā)者進行接口對接
UI特點
-
以
markdown形式展示文檔,將文檔的請求地址、類型、請求參數(shù)、示例、響應(yīng)參數(shù)分層次依次展示,接口文檔一目了然,方便開發(fā)者對接 -
在線調(diào)試欄除了自動解析參數(shù)外,針對必填項著顏色區(qū)分,同時支持tab鍵快速輸入上下切換.調(diào)試時可自定義Content-Type請求頭類型
-
個性化配置項,支持接口地址、接口description屬性、UI增強等個性化配置功能
-
接口排序,支持分組及接口的排序功能
-
支持
markdown文檔離線文檔導(dǎo)出,也可在線查看離線文檔 -
調(diào)試信息全局緩存,頁面刷新后依然存在,方便開發(fā)者調(diào)試
-
以更人性化的treetable組件展示Swagger Models功能
-
響應(yīng)內(nèi)容可全屏查看,針對響應(yīng)內(nèi)容很多的情況下,全屏查看,方便調(diào)試、復(fù)制
-
文檔以多tab方式可顯示多個接口文檔
-
請求參數(shù)欄請求類型、是否必填著顏色區(qū)分
-
主頁中粗略統(tǒng)計接口不同類型數(shù)量
-
支持接口在線搜索功能
-
左右菜單和內(nèi)容頁可自由拖動寬度
-
支持自定義全局參數(shù)功能,主頁包括header及query兩種類型
-
i18n國際化支持,目前支持:中文簡體、中文繁體、英文
-
JSR-303 annotations 注解的支持
對于Spring Cloud微服務(wù)集成,有2種不同的配置:網(wǎng)關(guān)和微服務(wù)
Spring Cloud Gateway網(wǎng)關(guān)
我們的網(wǎng)關(guān)做了文檔聚合的作用,也就是將所有微服務(wù)文檔聚合到網(wǎng)關(guān)提供文檔統(tǒng)一入口,當(dāng)然你也可以單獨做一個“文檔聚合”服務(wù)??第一步:pom引入相關(guān)jar包
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>knife4j-spring-boot-starterartifactId>
<version>${knife4j.version}version>
dependency>
${knife4j.version}取最新的版本,我們使用的是2.0.2
需要注意的是knife4j需要依賴lombok,如果沒有使用的話請增加下面的配置
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.4version>
<scope>providedscope>
dependency>
第二步: 文檔聚合業(yè)務(wù)編碼
在我們使用Spring Boot等單體架構(gòu)集成swagger項目時,是通過對包路徑進行業(yè)務(wù)分組,然后在前端進行不同模塊的展示,而在微服務(wù)架構(gòu)下,我們的一個服務(wù)就類似于原來我們寫的一個業(yè)務(wù)分組
springfox-swagger
提供的分組接口是
swagger-resource
,返回的是分組接口名稱、地址等信息
在Spring Cloud微服務(wù)架構(gòu)下,我們需要重寫該接口,主要是通過網(wǎng)關(guān)的注冊中心動態(tài)發(fā)現(xiàn)所有的微服務(wù)文檔,代碼如下:
4j
public class SwaggerResourceConfig implements SwaggerResourcesProvider {
private final RouteLocator routeLocator;
private final GatewayProperties gatewayProperties;
public List get() {
List resources = new ArrayList<>();
List routes = new ArrayList<>();
routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId())).forEach(route -> {
route.getPredicates().stream()
.filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
.forEach(predicateDefinition -> resources.add(swaggerResource(route.getId(),
predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
.replace("**", "v2/api-docs"))));
});
return resources;
}
private SwaggerResource swaggerResource(String name, String location) {
log.info("name:{},location:{}",name,location);
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion("2.0");
return swaggerResource;
}
}
上面代碼為Knife4j官方提供,由于我們是使用了 k8s 的 APIServer+ETCD 做的服務(wù)注冊中心并且支持了 ConfigMap 動態(tài)配置,必須做如下代碼改造下:
/**
* @author James Tang
* @date Created in 2020/3/18 19:48
*/
@Slf4j
@Component
@Primary
@AllArgsConstructor
public class SwaggerResourceConfig implements SwaggerResourcesProvider {
// private final RouteLocator routeLocator;
// private final GatewayProperties gatewayProperties;
private final InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository;
private final String API_DOCS_ROUTE_ID_POSTFIX = "apidocs";
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
// List routes = new ArrayList<>();
// routeLocator.getRoutes().subscribe(route -> {
// log.info(route.toString());
// routes.add(route.getId());
// });
Set<String> serviceGroups = Sets.newHashSet();
inMemoryRouteDefinitionRepository.getRouteDefinitions().subscribe(routeDefinition -> {
log.info(routeDefinition.toString());
if (routeDefinition.getId().endsWith(API_DOCS_ROUTE_ID_POSTFIX)) {
String[] tmpSplits = routeDefinition.getId().split("_");
if (tmpSplits.length > 1) {
String groupName = tmpSplits[1];
if (!serviceGroups.contains(groupName)) {
routeDefinition.getPredicates().stream()
.filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName())).findFirst().ifPresent(predicateDefinition -> {
String routePath = predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0");
String groupPath = routePath.substring(1).split("/")[0];
resources.add(swaggerResource(groupName, String.format("/%s/v2/api-docs?group=%s", groupPath, groupName)));
});
serviceGroups.add(groupName);
}
}
}
});
// gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId())).forEach(route -> {
// route.getPredicates().stream()
// .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
// .forEach(predicateDefinition -> resources.add(swaggerResource(route.getId(),
// predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
// .replace("**", "v2/api-docs"))));
// });
return resources;
}
private SwaggerResource swaggerResource(String name, String location) {
log.info("name:{},location:{}", name, location);
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion("2.0");
return swaggerResource;
}
}
文檔聚合相關(guān)接口:
public class SwaggerHandler {
private SecurityConfiguration securityConfiguration;
private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResources;
public SwaggerHandler(SwaggerResourcesProvider swaggerResources) {
this.swaggerResources = swaggerResources;
}
public Mono> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
}
public Mono> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}
public Mono swaggerResources() {
return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
}
}
微服務(wù)接口
下面我們看下消息服務(wù)做了什么配置?第一步:pom引入相關(guān)jar包
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.4version>
<scope>providedscope>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.8.0version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.8.0version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-bean-validatorsartifactId>
<version>2.8.0version>
dependency>
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>knife4j-micro-spring-boot-starterartifactId>
<version>${knife4j.version}version>
dependency>
${knife4j.version}取最新的版本,我們使用的是2.0.2
knife4j-micro-spring-boot-starter在服務(wù)內(nèi)不提供文檔入口,如果需要在消息服務(wù)直接能顯示文檔,請?zhí)鎿Q為knife4j-spring-boot-starter
第二步 :增加@EnableKnife4j注解
/**
* @author James
* @date 19/4/2
*/
@Configuration
@EnableKnife4j
@EnableSwagger2
@Import(BeanValidatorPluginsConfiguration.class)
public class SwaggerConfig {
...
}
好了,大功告成!

注意點
在集成Spring Cloud Gateway網(wǎng)關(guān)的時候,會出現(xiàn)沒有basePath的情況(即定義的例如/user、/order等微服務(wù)的前綴),這個情況在使用zuul網(wǎng)關(guān)的時候不會出現(xiàn)此問題。因此,在Gateway網(wǎng)關(guān)需要添加一個Filter實體Bean,代碼如下:
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {
private static final String HEADER_NAME = "X-Forwarded-Prefix";
private static final String URI = "/v2/api-docs";
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if (!StringUtils.endsWithIgnoreCase(path,URI )) {
return chain.filter(exchange);
}
String basePath = path.substring(0, path.lastIndexOf(URI));
ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
return chain.filter(newExchange);
};
}
}
然后在配置文件指定這個filter
spring:
application:
name: service-doc
cloud:
gateway:
routes:
id: service-user
uri: lb://service-user
predicates:
Path=/user/**
# - Header=Cookie,Set-Cookie
filters:
SwaggerHeaderFilter
StripPrefix=1
id: service-order
uri: lb://service-order
predicates:
Path=/order/**
filters:
SwaggerHeaderFilter //指定filter
StripPrefix=1
這個“注意點”是官方提供的,我們并沒有遇到這個問題,也沒加這個配置,寫在這里希望遇到此類問題的人可以快速集成
其實Swagger2文檔其實不止一種UI效果,在沒有接觸Knife4j之前,大家一般會使用springfox的幾個庫,下面是使用springfox-swagger-ui的效果圖,其實還是挺不錯的

Springfox-Swagger
關(guān)于 SpringfoxSwagger 詳細使用,這里不過多敘述,可自行通過下面地址查閱
-
GitHub :? https://gith ub.com/springfox/springfox
-
文檔 :http://springfox.io
下面我們重點講解SpringfoxSwagger提供的與 K ni fe4j 有關(guān)的2個接口, K ni fe4j 包會根據(jù)下面2個接口來動態(tài)生成文檔
-
分組接口: /swagg er-resources
-
詳情實例接口: /v2/api-docs
Swagger分組
Swagger的分組接口是用后端配置不同的掃描包,將后端的接口,按配置的掃描包基礎(chǔ)屬性響應(yīng)給前端,看看分組接口響應(yīng)的json內(nèi)容:
[
{
"name": "分組接口",
"url": "/v2/api-docs?group=分組接口",
"swaggerVersion": "2.0",
"location": "/v2/api-docs?group=分組接口"
},
{
"name": "默認接口",
"url": "/v2/api-docs?group=默認接口",
"swaggerVersion": "2.0",
"location": "/v2/api-docs?group=默認接口"
}
]
在Springfox-Swagger有些較低的版本中,并沒有l(wèi)ocation屬性,高版本會有該屬性

分組的后端Java配置代碼如下:
@Bean(value = "defaultApi")
public Docket defaultApi() {
ParameterBuilder parameterBuilder=new ParameterBuilder();
List parameters= Lists.newArrayList();
parameterBuilder.name("token").description("token令牌").modelRef(new ModelRef("String"))
.parameterType("header").defaultValue("abc")
.required(true).build();
parameters.add(parameterBuilder.build());
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.groupName("默認接口")
.select()
.apis(RequestHandlerSelectors.basePackage("com.swagger.bootstrap.ui.demo.controller"))
.paths(PathSelectors.any())
.build().globalOperationParameters(parameters)
.securityContexts(Lists.newArrayList(securityContext(),securityContext1())).securitySchemes(Lists.newArrayList(apiKey(),apiKey1()));
}
@Bean(value = "groupRestApi")
public Docket groupRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(groupApiInfo())
.groupName("分組接口")
.select()
.apis(RequestHandlerSelectors.basePackage("com.swagger.bootstrap.ui.demo.group"))
.paths(PathSelectors.any())
.build().securityContexts(Lists.newArrayList(securityContext(),securityContext1())).securitySchemes(Lists.newArrayList(apiKey(),apiKey1()));
}
詳情實例接口
詳情實例接口是根據(jù)分組名稱動態(tài)獲取該組下配置的basePackage所有的接口描述信息,如下圖所示:

