SpringBoot3使用Gateway聚合Springdoc+Knife4j

SpringBoot3使用Gateway聚合Springdoc+Knife4j

前言

基础环境

将所有依赖集成好作为一个本地包供其他项目使用

  • jdk17
  • maven3.6+
  • springboot3.0
  • springcloud:2022.0.1
  • springcloud-alibaba:1.8.1-2022.0.0-R
    ##具包(framework-document)

依赖

<dependencies>
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webflux-api</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba.nacos</groupId>
        <artifactId>nacos-client</artifactId>
        <scope>provided</scope>
    </dependency>
</dependencies>

代码

文档信息properties

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

/**
 * The type Swagger properties.
 *
 * @author : Milo
 */
@ConfigurationProperties("springdoc.info")
public class DocumentInfoProperties {
    /**
     * 文档标题
     */
    private String title;
    /**
     * 文档描述
     */
    private String description;
    /**
     * 项目version
     */
    private String projectVersion;
    /**
     * 许可证
     */
    private String license;
    /**
     * 许可证URL
     */
    private String licenseUrl;
    /**
     * api 前缀
     */
    private String apiPrefix;
    /**
     * 项目负责人信息
     */
    private Contact contact = new Contact();

   /*省略get,set*/

    public static class Contact {
        /**
         * 联系人
         **/
        private String name;
        /**
         * 联系人url
         **/
        private String url;
        /**
         * 联系人email
         **/
        private String email;

        /*省略get,set*/
    }
}

文档信息Configuration

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author milo
 */
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(DocumentInfoProperties.class)
@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true")
@ConditionalOnWebApplication
public class DocumentConfiguration {
    @Bean
    public OpenAPI openApi(final DocumentInfoProperties docInfo) {
        return new OpenAPI().info(new Info()
            .title(docInfo.getTitle())
            .description(docInfo.getDescription())
            .version(docInfo.getProjectVersion())
            .contact(new Contact().name(docInfo.getContact().getName()).email(docInfo.getContact().getEmail()).url(docInfo.getContact().getUrl()))
            .license(new License().name(docInfo.getLicense()).url(docInfo.getLicenseUrl()))
        );
    }
}

监听服务,通过服务ID创建Group

服务节点变动事件

import org.springframework.context.ApplicationEvent;

/**
 * @author : Milo
 * 服务节点变动事件
 */
public class ServiceChangeEvent extends ApplicationEvent {
    public ServiceChangeEvent(Object source) {
        super(source);
    }
}

Nacos监听服务上下线

如果使用的其他的注册中心,主动去监听服务上下线,如果监听到了,直接发布ServiceChangeEvent

import com.alibaba.nacos.client.naming.event.InstancesChangeEvent;
import com.alibaba.nacos.common.notify.Event;
import com.alibaba.nacos.common.notify.NotifyCenter;
import com.alibaba.nacos.common.notify.listener.Subscriber;
import com.github.mpcloud.document.event.ServiceChangeEvent;
import com.github.mpcloud.framework.core.utils.spring.SpringContextHolder;
import org.springframework.beans.factory.InitializingBean;

/**
 * @author : Milo
 */
public class NacosServiceListener extends Subscriber<InstancesChangeEvent> implements InitializingBean {
    @Override
    public void onEvent(InstancesChangeEvent ignored) {
        // 这里可以用org.springframework.context.ApplicationContext.publishEvent替代
        SpringContextHolder.publishEvent(new ServiceChangeEvent(this));
    }

    @Override
    public Class<? extends Event> subscribeType() {
        return InstancesChangeEvent.class;
    }

    @Override
    public void afterPropertiesSet() {
        NotifyCenter.registerSubscriber(this);
    }
}

网关与SpringDoc整合的配置类

import com.alibaba.nacos.client.naming.event.InstancesChangeEvent;
import com.github.mpcloud.document.event.ServiceChangeEvent;
import com.github.mpcloud.document.listener.NacosServiceListener;
import com.github.mpcloud.framework.core.consts.Constant;
import com.github.mpcloud.framework.core.utils.spring.SpringContextHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springdoc.core.properties.AbstractSwaggerUiConfigProperties;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.core.properties.SwaggerUiConfigParameters;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;

import java.util.Set;

import static org.springdoc.core.utils.Constants.SPRINGDOC_ENABLED;
import static org.springdoc.core.utils.Constants.SPRINGDOC_SWAGGER_UI_ENABLED;

/**
 * @author : Milo
 */
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnProperty(name = { SPRINGDOC_ENABLED, SPRINGDOC_SWAGGER_UI_ENABLED }, matchIfMissing = true, havingValue = "true")
public class GatewayRouterGroupProviderConfiguration {
    public static final String PEN_API_ROUTE_NAME = "open-api-route";
    private static final Logger log = LoggerFactory.getLogger(GatewayRouterGroupProviderConfiguration.class);
    private final ReactiveDiscoveryClient discoveryClient;
    private final Registration registration;
    private final SpringDocConfigProperties springDocConfigProperties;
    private final SwaggerUiConfigParameters swaggerUiConfigParameters;

    public GatewayRouterGroupProviderConfiguration(ReactiveDiscoveryClient discoveryClient, final Registration registration, SpringDocConfigProperties springDocConfigProperties, SwaggerUiConfigParameters swaggerUiConfigParameters) {
        this.discoveryClient = discoveryClient;
        this.registration = registration;
        this.springDocConfigProperties = springDocConfigProperties;
        this.swaggerUiConfigParameters = swaggerUiConfigParameters;
    }

    /**
     * openapi 路由(重写服务名所在位置)
     *
     * @param builder 路由构造器
     *
     * @return 路由
     */
    @Bean
    public RouteLocator openApiRouteLocator(final RouteLocatorBuilder builder) {
        return builder.routes()
            .route(PEN_API_ROUTE_NAME, route -> route
                .path(this.springDocConfigProperties.getApiDocs().getPath() + "/**")
                .filters(filter -> filter.rewritePath(this.springDocConfigProperties.getApiDocs().getPath() + "/(?<segment>.*)", "/$\{segment}" + this.springDocConfigProperties.getApiDocs().getPath()))
                .uri(Constant.HTTP_PREFIX + "localhost:" + SpringContextHolder.getServerPort()))
            .build();
    }

    /**
     * 网关当前路由 (去掉服务名)
     *
     * @param builder 路由构造器
     *
     * @return 路由
     */
    @Bean
    public RouteLocator registrationRouteLocator(final RouteLocatorBuilder builder) {
        return builder.routes()
            .route(this.registration.getServiceId(), route ->
                route.path("/" + this.registration.getServiceId() + "/**")
                    .filters(filter -> filter.rewritePath("/" + this.registration.getServiceId() + "/(?<segment>.*)", "/$\{segment}"))
                    .uri(Constant.HTTP_PREFIX + "localhost:" + SpringContextHolder.getServerPort()))
            .build();
    }

    /**
     * nacos服务节点变动监听器
     *
     * @return NacosServiceListener
     */
    @Bean
    @ConditionalOnClass(InstancesChangeEvent.class)
    public NacosServiceListener nacosServiceListener() {
        return new NacosServiceListener();
    }

    /**
     * 目前有以下三种方式。比较推荐1和3,这两种方式 当前采用的是方式1
     * 1. 通过注册中心获取到组名(暂时没有从 spring cloud 里面得到监听服务上下线的办法)
     * 优点: 获取到准确的服务名
     * 缺点: 强依赖注册中心,需要对每个注册中心分别实现监听器
     * 2. 通过监听RefreshRoutesEvent获取到所有路由后获取组名
     * 优点: 不强依赖注册中心
     * 缺点: 获取服务名没有一种很友好的方式
     * 3. 通过定时任务每隔段时间去调用当前方法
     * 优点:获取到准确的服务名,不强依赖注册中心
     * 缺点:需要开辟一个线程池
     */
    @EventListener(classes = { ApplicationReadyEvent.class, ServiceChangeEvent.class })
    public synchronized void discover() {
        discoveryClient.getServices()
            .flatMap(discoveryClient::getInstances)
            .map(instance -> {
                final AbstractSwaggerUiConfigProperties.SwaggerUrl swaggerUrl = new AbstractSwaggerUiConfigProperties.SwaggerUrl();
                swaggerUrl.setName(instance.getServiceId());
                swaggerUrl.setDisplayName(instance.getServiceId());
                swaggerUrl.setUrl( "/" + instance.getServiceId() + this.springDocConfigProperties.getApiDocs().getPath());
                return swaggerUrl;
            }).subscribe(swaggerUrl -> {
                Set<AbstractSwaggerUiConfigProperties.SwaggerUrl> originalSwaggerUrls = this.swaggerUiConfigParameters.getUrls();
                originalSwaggerUrls.removeIf(originalSwaggerUrl -> originalSwaggerUrl.getName().equals(swaggerUrl.getName()));
                originalSwaggerUrls.add(swaggerUrl);
            }, ex -> log.error("Listener swagger service event error.", ex));
    }

}

使用工具包

gateway

依赖

<dependencies>
    <!--上面的步骤所构建的包(这个是我本地的,请替换为自己写的)-->
    <dependency>
        <groupId>com.github.mpcloud</groupId>
        <artifactId>framework-document</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.nacos</groupId>
        <artifactId>nacos-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>knife4j-openapi3-ui</artifactId>
    </dependency>
</dependencies>

配置

server:
  port: 8080
spring:
  application:
    name: mpcloud-gateway
  cloud:
    nacos:
      username: nacos
      password: nacos
      discovery:
        server-addr: localhost:8848
        namespace: mpcloud
        cluster-name: shanghai
        heart-beat-timeout: 40000
        heart-beat-interval: 20000
        ip-delete-timeout: 80000
      config:
        server-addr: localhost:8848
        namespace: mpcloud
        timeout: 3000
        file-extension: yml
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      globalcors:
        cors-configurations:
          '[/**]':
            allowedHeaders: '*'
            allowedMethods: '*'
            allowedOrigins: '*'
      httpclient:
        connect-timeout: 2000
        response-timeout: 60s
  main:
    banner-mode: off
  webflux:
    multipart:
      file-storage-directory: /tmp
      max-in-memory-size: 12MB
    format:
      date-time: yyyy-MM-dd HH:mm:ss
      date: yyyy-MM-dd
      time: HH:mm:ss
logging:
  file:
    path: ./logs
springdoc:
  api-docs:
    enabled: true
    groups:
      enabled: true
    path: /v3/api-docs
  swagger-ui:
    enabled: true
  info:
    title: 网关Api文档
    description: 网关Api文档
    project-version: 0.0.1
    license: https://milo-xiaomeng.github.io
    license-url: https://milo-xiaomeng.github.io
    contact:
      name: milo
      url: http://localhost:8080
      email: milo.xiaomeng@gmail.com
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: ALWAYS

web服务中使用

依赖

dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.nacos</groupId>
        <artifactId>nacos-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>
    <!--上面的步骤所构建的包(这个是我本地的,请替换为自己写的)-->
    <dependency>
        <groupId>com.github.mpcloud</groupId>
        <artifactId>framework-document</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
    </dependency>
</dependencies>

配置

server:
  port: 8083
spring:
  application:
    name: mpcloud-monitoring
  profiles:
    active: dev
  cloud:
    nacos:
      username: nacos
      password: nacos
      discovery:
        server-addr: localhost:8848
        namespace: mpcloud
        cluster-name: shanghai
      config:
        server-addr: localhost:8848
        namespace: mpcloud
        timeout: 3000
        file-extension: yml
springdoc:
  api-docs:
    enabled: true
    groups:
      enabled: true
    path: /v3/api-docs
  info:
    title: 监控Api文档
    description: 监控Api文档
    project-version: 0.0.1
    license: mpcloud.github.com
    license-url: mpcloud.github.com
    contact:
      name: milo
      url: http://localhost:8080
      email: milo.xiaomeng@gmail.com
  group-configs:
   - pathsToMatch: /**
     group: ${spring.application.name}
  show-actuator: true

效果

image.png

具体代码地址

gitee地址

我的博客