Spring Cloud集成Knife4j管理接口文档

James唐

共 21206字,需浏览 43分钟

 ·

2023-06-24 09:44

 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 注解的支持

Knife4j集成

对于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微服务架构下,我们需要重写该接口,主要是通过网关的注册中心动态发现所有的微服务文档,代码如下:

      
        @Slf4j
      
      
        
          @Component
        
      
      
        
          @Primary
        
      
      
        
          @AllArgsConstructor
        
      
      
        public class SwaggerResourceConfig implements SwaggerResourcesProvider {
      
      
        
          
private final RouteLocator routeLocator; private final GatewayProperties gatewayProperties;

@Override 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; } }

文档聚合相关接口:

      
        
          @RestController
        
      
      
        public class SwaggerHandler {
      
      
        
          
@Autowired(required = false) private SecurityConfiguration securityConfiguration;
@Autowired(required = false) private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResources;
@Autowired public SwaggerHandler(SwaggerResourcesProvider swaggerResources) { this.swaggerResources = swaggerResources; }

@GetMapping("/swagger-resources/configuration/security") public Mono> securityConfiguration() { return Mono.just(new ResponseEntity<>( Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK)); }
@GetMapping("/swagger-resources/configuration/ui") public Mono> uiConfiguration() { return Mono.just(new ResponseEntity<>( Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK)); }
@GetMapping("/swagger-resources") 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 {
      
      
        
          
...
}

好了,大功告成!

bf9e3c689bcb18a2d29eb5d8fe27b0cd.webp

注意点

在集成Spring Cloud Gateway网关的时候,会出现没有basePath的情况(即定义的例如/user、/order等微服务的前缀),这个情况在使用zuul网关的时候不会出现此问题。因此,在Gateway网关需要添加一个Filter实体Bean,代码如下:

      
        
          @Component
        
      
      
        public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {
      
      
            private static final String HEADER_NAME = "X-Forwarded-Prefix";
      
      
        
          
private static final String URI = "/v2/api-docs";
@Override 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的效果图,其实还是挺不错的

11fdd046aa6b8396b8ef9c5058493460.webp

当然Knife4jspringfox-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属性,高版本会有该属性

c2e929621eeaf2ba07446cfe58ea24d8.webp

分组的后端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所有的接口描述信息,如下图所示:

d3ebbcfe1fb5c4302ef0d3cf57772407.webp

理解这2个接口,就理解 Knife4j 是怎么回事了😀 大家最近安好,中国加油💪
浏览 13
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报