xiaoymin / knife4j

Knife4j is a set of Swagger2 and OpenAPI3 All-in-one enhancement solution
https://doc.xiaominfo.com
Apache License 2.0
4.1k stars 616 forks source link

在Spring Cloud Gateway网关聚合微服务knife4j接口文档时,网关路由配置的首个服务无法访问时,接口文档的首页无法显示(如果用原生的springfox-swagger2、springfox-swagger-ui就不会这样) #728

Open LuoYingxiong opened 8 months ago

LuoYingxiong commented 8 months ago

我有gateway、auth-server、service-admin三个微服务,在Gateway上做了knife4j的微服务接口文档聚合。 在gateway中分别为auth-server、service-admin做了路由配置。如下 Gateway 遇到的问题是: 路由配置的首个微服务不能访问时,从网关访问knife4j首页就一片空白,影响我切到正常服务的接口文档。 请看视频:

https://github.com/xiaoymin/knife4j/assets/35752648/098641fd-ad09-48c4-a1e2-eb4bd7b09bbb

但是我把网关的依赖更换成原生的Swagger(不改变任何代码),就没有有上述问题(虽然首页会报错,但不影响我从下拉框切换到其他服务的接口文档) 请看视频:

https://github.com/xiaoymin/knife4j/assets/35752648/753d657d-f9a4-4e8c-b1b1-69cda69d202c

还请修复一下这个页面展示的bug!!

如下是我Gateway网关的依赖和knife4j的配置代码 (knife4j版本:2.0.9)

pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!-- config客户端 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
    </dependency>

    <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>knife4j-spring-boot-starter</artifactId>
                <version>2.0.9</version>
    </dependency>
</dependencies>

<!-- 构建配置 -->
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <fork>true</fork>
            </configuration>
        </plugin>
    </plugins>
</build>

路由配置文件: 路由配置

Swagger 配置代码:

import lombok.RequiredArgsConstructor;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;

import java.util.ArrayList;
import java.util.List;

/**
 * 因为Swagger暂不支持webflux项目,所以不能在Gateway配置SwaggerConfig,需要编写
 * GatewaySwaggerProvider实现SwaggerResourcesProvider接口,用于获取SwaggerResources
 *
 * @see org.springframework.context.annotation.Primary
 * 该注解用于表明GatewaySwaggerProvider是SwaggerResourcesProvider接口的首选实现,
 * 在存在多个相同类型的Bean时,优先选择被标注了@Primary注解的Bean进行注入。
 */
@RequiredArgsConstructor
@Component
@Primary
public class GatewaySwaggerProvider implements SwaggerResourcesProvider {

    public static final String API_URI = "/v2/api-docs";
    private final RouteLocator routeLocator;
    private final GatewayProperties gatewayProperties;

    @Override
    public List<SwaggerResource> get() {
        List<SwaggerResource> resources = new ArrayList<>();
        List<String> routes = new ArrayList<>();

        // 取出Spring Cloud Gateway中的route
        routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));

        // 结合 application.yml中的路由配置,只获取有效的route节点
        gatewayProperties.getRoutes().stream()
                .filter(routeDefinition -> routes.contains(routeDefinition.getId()))
                .forEach(
                        routeDefinition -> routeDefinition.getPredicates().stream()
                                .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
                                .forEach(
                                        predicateDefinition -> resources.add(
                                                swaggerResource(
                                                        routeDefinition.getId(),
                                                        predicateDefinition.getArgs()
                                                                .get(NameUtils.GENERATED_NAME_PREFIX + "0")
                                                                .replace("/**", API_URI)
                                                )
                                        )
                                )
                );
        return resources;
    }

    private SwaggerResource swaggerResource(String name, String location) {
        SwaggerResource swaggerResource = new SwaggerResource();
        swaggerResource.setName(name);
        swaggerResource.setLocation(location);
        swaggerResource.setSwaggerVersion("2.0");
        return swaggerResource;
    }
}
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.*;

import java.util.Optional;

/**
 * 因为没有在Spring Cloud Gateway中配置SwaggerConfig,但是运行Swagger-UI的时候需要依赖一些接口,
 * 所以需要建立相应的Swagger-Resource端点
 */
@RestController
@RequestMapping("/swagger-resources")
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("/configuration/security")
    public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
        return Mono.just(
                new ResponseEntity<>(
                        Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()),
                        HttpStatus.OK
                )
        );
    }

    @GetMapping("/configuration/ui")
    public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
        return Mono.just(
                new ResponseEntity<>(
                        Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()),
                        HttpStatus.OK
                )
        );
    }

    @ApiOperation(value = "加载所有的微服务信息显示到Swagger2 文档聚合页左上角的下拉列表")
    @GetMapping
    public Mono<ResponseEntity> swaggerResources() {
        return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
    }
}
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

/**
 * SwaggerHeader 过滤器
 */
@Component
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {

    private static final String HEADER_NAME = "X-Forwarded-Prefix";

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            String path = request.getURI().getPath();
            if (!StringUtils.endsWithIgnoreCase(path, GatewaySwaggerProvider.API_URI)) {
                return chain.filter(exchange);
            }
            String basePath = path.substring(0, path.lastIndexOf(GatewaySwaggerProvider.API_URI));
            ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
            ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();

            return chain.filter(newExchange);
        };
    }
}
xiaoymin commented 8 months ago

gateway的聚合,你可以用这个

https://doc.xiaominfo.com/docs/middleware-sources/spring-cloud-gateway/spring-gateway-introduction

https://doc.xiaominfo.com/docs/blog/gateway/knife4j-gateway-introduce

LuoYingxiong commented 8 months ago

聚合网关,你可以用这个

https://doc.xiaominfo.com/docs/middleware-sources/spring-cloud-gateway/spring-gateway-introduction

https://doc.xiaominfo.com/docs/blog/gateway/knife4j-gateway-introduce

多谢!使用你们提供的新插件knife4j-gateway-spring-boot-starter的【服务发现】模式确实解决了我的问题,但是出现了新的问题,在网关聚合接口文档的服务下拉框里,服务名出现了重复,请看视频:

https://github.com/xiaoymin/knife4j/assets/35752648/0421d968-60ae-4c3f-9f86-57fcb3a433af

我的项目框架信息如下: Spring Boot 版本: 2.3.12.RELEASE Spring Cloud 版本: Hoxton.SR12 auth-server、service-admin微服务使用的 knife4j-micro-spring-boot-starter版本 :2.0.9

Spring Cloud Gateway网关配置如下: Gateway 网关层更换成了新的依赖 配置文件

刚才尝试了knife4j-gateway-spring-boot-starter 4.5.0版本,依然存在上述问题

xiaoymin commented 8 months ago

你可以在knife4j-gateway里面debug看看,获取服务列表的时候,为什么为出现重复。spring cloud config我并没有兼容测试过,目前是对nacos、eureka做过集成测试

LuoYingxiong commented 8 months ago

你可以在knife4j-gateway里面debug看看,获取服务列表的时候,为什么为出现重复。spring cloud config我并没有兼容测试过,目前是对nacos、eureka做过集成测试

我测试过了,和spring cloud config 没关系,即使我把配置写在gateway工程的bootstrap.yml也会出现同样的问题。源码层面的调试还有待观察