SpringCloud 集成统一文档管理(Knife4j)

1,303 阅读4分钟

Knife4j介绍

  • 官网:doc.xiaominfo.com/
  • Knife4j是一个集Swagger2 和 OpenAPI3 为一体的增强解决方案

SpringCloud 集成 Knife4j

1、思路及分包情况展示

将基础配置抽取成一个通用的工具类,业务模块如需依赖该模块直接引入该工具包即可,然后进行简单的配置,此时就实现了单体SpringBoot集成Knife4j(如图:common-knife4j模块)

接下来将各业务模块访问通过网关路由+nacos相关规则代理出来,实现一个地址访问多个接口文档(如图:ability-gateway模块 knife4j包)

截屏2023-06-21 上午10.05.21.png

2、单体业务模块集成Knife4j

2.1 knife4j工具类抽取

引入相关依赖包

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
     </dependency>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
    </dependency>
</dependencies>

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
        </resource>
    </resources>
</build>

添加 SwaggerProperties.java

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;


/**
 * description: swagger基础信息配置
 * author: chengpengai
 * date: 2023-04-08 周六 10:08 PM
 */
@Component
@ConfigurationProperties("swagger.config")
public class SwaggerProperties {
    //版本
    private String docVersion;
    //标题
    private String title;
    //描述
    private String description;
    //联系人姓名
    private String contactName;
    //访问路径
    private String contactUrl;
    //联系人邮箱
    private String contactEmail;
    //许可证
    private String license;
    //许可证地址
    private String licenseUrl;

    public SwaggerProperties() {
    }

    public String getDocVersion() {
        return this.docVersion;
    }

    public void setDocVersion(String docVersion) {
        this.docVersion = docVersion;
    }

    public String getTitle() {
        return this.title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDescription() {
        return this.description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getContactName() {
        return this.contactName;
    }

    public void setContactName(String contactName) {
        this.contactName = contactName;
    }

    public String getContactUrl() {
        return this.contactUrl;
    }

    public void setContactUrl(String contactUrl) {
        this.contactUrl = contactUrl;
    }

    public String getContactEmail() {
        return this.contactEmail;
    }

    public void setContactEmail(String contactEmail) {
        this.contactEmail = contactEmail;
    }

    public String getLicense() {
        return this.license;
    }

    public void setLicense(String license) {
        this.license = license;
    }

    public String getLicenseUrl() {
        return this.licenseUrl;
    }

    public void setLicenseUrl(String licenseUrl) {
        this.licenseUrl = licenseUrl;
    }
}

添加 Knife4jConfig.java

import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import com.google.common.collect.Lists;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.util.StringUtils;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

import javax.annotation.Resource;
import java.util.List;


/**
 * description:
 *      配置knife4j主页信息
 *      需要 knife4j 的权限控制(账号密码) 才需要引用此包:  @Import(BeanValidatorPluginsConfiguration.class), 对应jar:spring-boot-starter-validation
 *
 * author: chengpengai
 * date: 2023-04-08 周六 10:08 PM
 */
@Configuration
@EnableSwagger2WebMvc
@EnableKnife4j
@Import(BeanValidatorPluginsConfiguration.class)
public class Knife4jConfig {

    @Resource
    private SwaggerProperties swaggerProperties;
    
    @Value("${spring.application.name}")
    private String projectName;

    @Bean
    public Docket createRestApi() {
        List<Parameter> parameters = Lists.newArrayList();
        parameters.add((new ParameterBuilder()).name("token").description("认证token").modelRef(new ModelRef("string")).parameterType("header").required(false).build());
        parameters.add((new ParameterBuilder()).name("token").description("认证token").modelRef(new ModelRef("string")).parameterType("header").required(false).build());
        
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(this.apiInfo())
                .select()
                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                .paths(PathSelectors.any())
                .build()
//                .groupName(StringUtils.isEmpty(projectName)? UUID.randomUUID().toString(): projectName)
                .globalOperationParameters(parameters)
                .securitySchemes(this.securitySchemes())
                .securityContexts(this.securityContexts());
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
            .title(this.swaggerProperties.getTitle())
            .description(this.swaggerProperties.getDescription())
            .contact(new Contact(this.swaggerProperties.getContactName(), this.swaggerProperties.getContactUrl(), this.swaggerProperties.getContactEmail()))
            .termsOfServiceUrl(this.swaggerProperties.getContactUrl())
//            .license(this.swaggerProperties.getLicense())
//            .licenseUrl(this.swaggerProperties.getLicenseUrl())
            .version(this.swaggerProperties.getDocVersion())
            .build();
    }

    private List<ApiKey> securitySchemes() {
        return Lists.newArrayList(new ApiKey[]{new ApiKey("token", "token", "header")});
    }

    private List<SecurityContext> securityContexts() {
        return Lists.newArrayList(new SecurityContext[]{SecurityContext.builder().securityReferences(this.defaultAuth()).forPaths(PathSelectors.regex("^(?!auth).*$")).build()});
    }

    List<SecurityReference> defaultAuth() {
        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[]{authorizationScope};
        return Lists.newArrayList(new SecurityReference[]{new SecurityReference("token", authorizationScopes)});
    }
    
}

添加SwaggerEnable

import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

import java.lang.annotation.*;

/**
 * description:
 * author: chengpengai
 * date: 2023-04-08 周六 10:08 PM
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableSwagger2WebMvc
@EnableKnife4j
public @interface SwaggerEnable {
}

添加基础配置(也可添加到业务模块中)

spring:
  #开启资源映射
  resources:
    add-mappings: true
knife4j:
  #是否启用Knife4j
  enable: true
  #访问文档认证设置
  basic:
    enable: true
    username: acp
    password: 888888

2.2 业务服务引入Knife4j

引入common-knife4j服务包(该包为上面抽取的服务,非三方包)

<dependency>
    <groupId>org.ai.com</groupId>
    <artifactId>common-knife4j</artifactId>
</dependency>

添加配置信息

swagger:
  config:
    docVersion: 1.0
    title: 订单模块
    description: 订单模块1.0
    contactName: 艾(ChengPengAi)
    contactUrl: www.chengpeng.com
    contactEmail: 3164604364@qq.cpm

测试

访问地址:http://127.0.0.1:服务端口/项目访问前缀/doc.html#/home

截屏2023-06-21 上午10.38.14.png

3、网关代理统一访问处理

网关路由配置

spring:
  cloud:
   gateway:
     routes:
       - id: LearningBuddy (此处需要和业务模块 server.servlet.context-path 值相同)
         uri: lb://learning-buddy-server (spring.application.name)
         predicates:
           - Path=/LearningBuddy/**
       - id: oauth
         uri: lb://ability-oauth
         predicates:
           - Path=/oauth/**

添加 Knife4jResourceProvider.java

import com.alibaba.cloud.nacos.ribbon.NacosRibbonClientConfiguration;
import com.alibaba.cloud.nacos.ribbon.NacosServer;
import com.alibaba.cloud.nacos.ribbon.NacosServerList;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * description:
 *      使用Spring Boot单体架构集成swagger时,是通过包路径进行业务分组,然后在前端进行不同模块的展示,而在微服务架构下,单个服务类似于原来业务组;
 *      springfox-swagger提供的分组接口是swagger-resource,返回的是分组接口名称、地址等信息;
 * author: chengpengai
 * date: 2023-04-12 周三 2:03 PM
 */
@Component
@RequiredArgsConstructor
public class Knife4jResourceProvider implements SwaggerResourcesProvider {
   
   /**
    * swagger2默认的url后缀
    */
   private static final String SWAGGER2_URL = "/v2/api-docs";
   
   /**
    * 路由定位器
    */
   private final RouteLocator routeLocator;
   
   /**
    * 网关应用名称
    */
   @Value("${spring.application.name}")
   private String gatewayName;
   
   /**
   * nacos客户端
   */
   @Autowired
   private DiscoveryClient discoveryClient;
   
   /**
    * 获取 Swagger 资源
    *    获取条件:所有配置的网关路由服务 - 排除网关地址 - 未注册到nacos的服务或者nacos存在异常的服务
    */
   @Override
   public List<SwaggerResource> get() {
      List<SwaggerResource> resources = new ArrayList<>();
      List<String> routeHosts = new ArrayList<>();
      // 1. 获取路由Uri 中的Host=> 服务注册则为服务名=》app-service001
      routeLocator.getRoutes()
         .filter(route -> route.getId() != null)
         //过滤掉网关的文档信息
         .filter(route -> !gatewayName.equals(route.getId()))
         //根据服务状态注入api文档信息
         .filter(route -> !CollectionUtils.isEmpty(discoveryClient.getInstances(route.getUri().getHost())))
         .subscribe(route -> routeHosts.add(route.getId()));
      // 2. 创建自定义资源
      for (String routeHost : routeHosts) {
         String serviceUrl = "/" + routeHost + SWAGGER2_URL; // 后台访问添加服务名前缀
         SwaggerResource swaggerResource = new SwaggerResource(); // 创建Swagger 资源
         swaggerResource.setUrl(serviceUrl); // 设置访问地址
         swaggerResource.setName(routeHost); // 设置名称
         swaggerResource.setSwaggerVersion("3.0.0");
         resources.add(swaggerResource);
      }
      return resources;
   }
   
}

添加 Knife4jResourceController.java

import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.swagger.web.SwaggerResource;

import java.util.List;

/**
 * description: knife4j访问资源接口重写
 * author: chengpengai
 * date: 2023-04-12 周三 2:06 PM
 */
@RestController
@RequestMapping("/swagger-resources")
@RequiredArgsConstructor
public class Knife4jResourceController {
   private final Knife4jResourceProvider knife4jResourceProvider;
   
   @RequestMapping
   public ResponseEntity<List<SwaggerResource>> swaggerResources() {
      return new ResponseEntity<>(knife4jResourceProvider.get(), HttpStatus.OK);
   }
}

测试

通过左侧下拉可以动态切换 截屏2023-06-21 上午10.57.38.png