Spring Cloud集成Knife4j管理接口文档
Knife4j
前身是swagger-bootstrap-ui
,取名knife4j
是希望她能像一把匕首一样小巧,轻量,并且功能强悍,更名也是希望把她做成一个为Swagger接口文档服务的通用性解决方案,不仅仅只是专注于前端Ui前端.虽然目前还只是在前端,但以后功能肯定不止于此。
2.0
版本主要是使用Vue
+Ant Design
对前端Ui进行重写,该版本是真正的前后端分离版本,同时依赖于Vue
的技术生态,以后会有更多有趣的功能实现,全方位满足开发者的需要。
Knife4j简介
核心功能
该UI增强包主要包括两大核心功能:文档说明 和 在线调试-
文档说明
:根据Swagger的规范说明,详细列出接口文档的说明,包括接口地址、类型、请求示例、请求参数、响应示例、响应参数、响应码等信息,使用
Knife4j
能能根据该文档说明,对该接口的使用情况一目了然。 -
在线调试 :提供在线接口联调的强大功能,自动解析当前接口参数,同时包含表单验证,调用参数可返回接口响应内容、headers、Curl请求命令实例、响应时间、响应状态码等信息,帮助开发者在线调试,而不必通过其他测试工具测试接口是否正确,简介、强大。
UI增强
Knife4j
在满足以上功能的同时,还提供了文档的增强功能,这些功能是官方
swagger-ui
所没有的,每一个增强的功能都是贴合实际,考虑到开发者的实际开发需要,是必不可少的功能,主要包括:
- 个性化配置 :通过个性化ui配置项,可自定义UI的相关显示信息
-
离线文档:根据标准规范,生成的在线
markdown
离线文档,开发者可以进行拷贝生成markdown
接口文档,通过其他第三方markdown
转换工具转换成html
或pdf
,这样也可以放弃swagger2markdown
组件 -
接口排序 :自1.8.5后,ui支持了接口排序功能,例如一个注册功能主要包含了多个步骤,可以根据
Knife4j
提供的接口排序规则实现接口的排序,step化接口操作,方便其他开发者进行接口对接
UI特点
-
以
markdown
形式展示文档,将文档的请求地址、类型、请求参数、示例、响应参数分层次依次展示,接口文档一目了然,方便开发者对接 -
在线调试栏除了自动解析参数外,针对必填项着颜色区分,同时支持tab键快速输入上下切换.调试时可自定义Content-Type请求头类型
-
个性化配置项,支持接口地址、接口description属性、UI增强等个性化配置功能
-
接口排序,支持分组及接口的排序功能
-
支持
markdown
文档离线文档导出,也可在线查看离线文档 -
调试信息全局缓存,页面刷新后依然存在,方便开发者调试
-
以更人性化的treetable组件展示Swagger Models功能
-
响应内容可全屏查看,针对响应内容很多的情况下,全屏查看,方便调试、复制
-
文档以多tab方式可显示多个接口文档
-
请求参数栏请求类型、是否必填着颜色区分
-
主页中粗略统计接口不同类型数量
-
支持接口在线搜索功能
-
左右菜单和内容页可自由拖动宽度
-
支持自定义全局参数功能,主页包括header及query两种类型
-
i18n国际化支持,目前支持:中文简体、中文繁体、英文
-
JSR-303 annotations 注解的支持
对于Spring Cloud微服务集成,有2种不同的配置:网关和微服务
Spring Cloud Gateway网关
我们的网关做了文档聚合的作用,也就是将所有微服务文档聚合到网关提供文档统一入口,当然你也可以单独做一个“文档聚合”服务😀第一步:pom引入相关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>
第二步: 文档聚合业务编码
在我们使用Spring Boot等单体架构集成swagger项目时,是通过对包路径进行业务分组,然后在前端进行不同模块的展示,而在微服务架构下,我们的一个服务就类似于原来我们写的一个业务分组
springfox-swagger
提供的分组接口是
swagger-resource
,返回的是分组接口名称、地址等信息
在Spring Cloud微服务架构下,我们需要重写该接口,主要是通过网关的注册中心动态发现所有的微服务文档,代码如下:
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 做的服务注册中心并且支持了 ConfigMap 动态配置,必须做如下代码改造下:
/**
* @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;
}
}
文档聚合相关接口:
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)));
}
}
微服务接口
下面我们看下消息服务做了什么配置?第一步:pom引入相关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
在服务内不提供文档入口,如果需要在消息服务直接能显示文档,请替换为knife4j-spring-boot-starter
第二步 :增加@EnableKnife4j注解
/**
* @author James
* @date 19/4/2
*/
@Configuration
@EnableKnife4j
@EnableSwagger2
@Import(BeanValidatorPluginsConfiguration.class)
public class SwaggerConfig {
...
}
好了,大功告成!
注意点
在集成Spring Cloud Gateway网关的时候,会出现没有basePath的情况(即定义的例如/user、/order等微服务的前缀),这个情况在使用zuul网关的时候不会出现此问题。因此,在Gateway网关需要添加一个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
关于 SpringfoxSwagger 详细使用,这里不过多叙述,可自行通过下面地址查阅
-
GitHub : https://gith ub.com/springfox/springfox
-
文档 :http://springfox.io
下面我们重点讲解SpringfoxSwagger提供的与 K ni fe4j 有关的2个接口, K ni fe4j 包会根据下面2个接口来动态生成文档
-
分组接口: /swagg er-resources
-
详情实例接口: /v2/api-docs
Swagger分组
Swagger的分组接口是用后端配置不同的扫描包,将后端的接口,按配置的扫描包基础属性响应给前端,看看分组接口响应的json内容:
[
{
"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有些较低的版本中,并没有location属性,高版本会有该属性
分组的后端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())); }
详情实例接口
详情实例接口是根据分组名称动态获取该组下配置的basePackage所有的接口描述信息,如下图所示:理解这2个接口,就理解 Knife4j 是怎么回事了😀 大家最近安好,中国加油💪