Gateway整合微服务文档:Knife4j文档请求异常、Swagger报错Failed to load API definition.

今天使用Gateway整合微服务的文档的时候发现Knife4j文档请求异常,查看数据包发现请求了这样的一个路径。(省流助手:错误原因是获取api-doc的方法错误,如果不明白我在说什么,那么可以往下看看)
在这里插入图片描述
整合的代码是在网上直接CV的,看来是需要做一些修改,其中比较重要的是在gateway的两个配置,其他服务的配置文件和单机时一致。gateway的配置文件如下:
第一个是Config

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

    @Override // 请求网关时就会执行此方法
    public List<SwaggerResource> get() {
    
    
        List<SwaggerResource> resources = new ArrayList<>();
        List<String> routes = new ArrayList<>();
        //获取所有路由的ID并加入到routes里
        routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
        //过滤出配置文件中定义的路由->过滤出Path Route Predicate->根据路径拼接成api-docs路径->生成SwaggerResource
        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;
    }
}

第二个是Handler

/**
 * 自定义Swagger的各个配置节点
 */
@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;
    }

    /**
     * Swagger安全配置,支持oauth和apiKey设置
     */
    @GetMapping("/swagger-resources/configuration/security")
    public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
    
    
        return Mono.just(new ResponseEntity<>(
                Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
    }

    /**
     * Swagger UI配置
     */
    @GetMapping("/swagger-resources/configuration/ui")
    public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
    
    
        return Mono.just(new ResponseEntity<>(
                Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
    }

    /**
     * Swagger资源配置,微服务中这各个服务的api-docs信息
     */
    @GetMapping("/swagger-resources")
    public Mono<ResponseEntity> swaggerResources() {
    
    
        return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
    }
}

错误定位

通过控制台的网络请求记录可以看到,我们是先请求了Handler的swagger-resource获取api-docs,然后再请求api-docs。
在这里插入图片描述
api-doc指的就是下图中的蓝色url(http:localhost:10000/v2/api-docs)。这个api-docs还可以用于把文档导入postman等api测试工具,很方便。
在这里插入图片描述
SwaggerHandler 接受到swagger-resource请求时会调用自动注入进来的 swaggerResources 的get方法,这个get方法是我们在SwaggerResourceConfig重写的,所以我们在这个get方法里打断点。
get方法通过自动注入拿到gateway的routeLocator和gatewaProperties,其中routeLocator里面包含三个字段delegate、routes、cache。
cache里面可以看到我们在gateway里配置的所有路由,形成一条链。可以发现,我们使用java写的路由配置在整个链条中排在最前面的。
在这里插入图片描述

从下面的源码可以看到routeLocator的getRoutes方法其实就是直接从cache里面上图的信息排序返回。并且这些routes是以Flux的形式组织起来的,也就是一个响应式流,所以需要使用subscribe来触发数据流,把所有路由id加入到我们自己创建的一个列表里。
在这里插入图片描述
这里又通过gatewaProperties的getRoutes方法再获取routes,不过这次可以看到,只有静态声明在配置文件里的路由。
在这里插入图片描述
接下来就是一系列的流处理了,过滤掉路由id不包含在我们上一步提取出来的路由id集合的配置文件,剩下的每一个都进行匹配,查看predicate是不是Path类型的,如果是path的话就的我们配置的路径值取出来,其中我们的值存放在一个哈希表中,key是‘_genkey_0’,我们可以通过NameUtils去获得这个自动生成前缀。(详细结构可看下图debugger控制台的variables那一栏)
在这里插入图片描述
数据流出来之后我们就可以看到,经过处理,我们获得了6个url,gateway微服务的swagger会通过这几个url去获取json文件,从而将各个微服务的文档聚合成一个文档。(下图debugger控制台的variables那一栏)
在这里插入图片描述
很显然,此时我的网关配置不正确导致网关的swagger获取不到正确的api-docs。因此,只要路径映射正确就好了。

解决方案

  • 修改路由规则(StripPrefix去除前缀再转发)
  • 修改获取SwaggerResource的规则

目前的swagger文档位置在每个微服务路径下的根路径,例如 localhost:10000/swagger-ui.html,这时候的获取SwaggerResource的规则是通过path去匹配的,很显然这不可能映射根目录(要匹配根目录就要修改路由规则匹配根路径的请求,这不但无法区分微服务,并且会拦截所有请求)。

还有需要注意的是凡是在resource中的所有匹配成功的路由id都会被加入文档中,所以这就意味着我们必须修改获取SwaggerResource的规则,把/api/xxx开头的路由剔除。

在这里插入图片描述
因此我们增加一组路由如下图:
在这里插入图片描述
StripPrefix=2就是在转发之间剔除路径的前面两个前缀,也就是/swagger/ware 了,转发过去的路径就变成了根目录。
并且获取get方法改为如下

    @Override // 请求网关时就会执行此方法
    public List<SwaggerResource> get() {
    
    
        List<SwaggerResource> resources = new ArrayList<>();
        List<String> routes = new ArrayList<>();
        //获取所有路由的ID并加入到routes里
        routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
        //过滤出配置文件中定义的路由->过滤出Path Route Predicate->根据路径拼接成api-docs路径->生成SwaggerResource
        gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId())).forEach(route -> {
    
    
            route.getPredicates().stream()
                    .filter(predicateDefinition ->{
    
    
                        boolean condition1 =("Path").equalsIgnoreCase(predicateDefinition.getName());
                        String url = predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0");
                        boolean condition2=false;
                        if(url.length()>9){
    
    
                            condition2 = ("/swagger/").equalsIgnoreCase(url.substring(0,9));
                        }
                        return condition1 && condition2;
                    })
                    .forEach(predicateDefinition -> resources.add(swaggerResource(route.getId(),
                            predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
                                    .replace("**", "v2/api-docs"))));
        });

        return resources;
    }

花絮1:pathMapping

我曾一度以为可以通过修改swagger配置的pathMapping去改变映射路径。其实这对访问swagger的路径没有任何影响。也就说我在配置文件中把pathMapping设置成test,我此时用http://localhost:88/swagger-ui.htm或者http://localhost:88/swagger/ware/v2/api-docs都可以访问或者获得json数据。而使用http://localhost:88/test/swagger-ui.htm或者http://localhost:88/swagger/ware/test/v2/api-docs都会404

在这里插入图片描述
那这pathMapping是什么用呢?
简单来说这个pathMapping指的是使用swagger测试接口发送请求的时候带上这个前缀(如图中带上前缀test)。前端的请求一般都会有/api/product作为前缀,而这个前缀实在gateway的时候过滤掉了,这个的作用是模拟前端进行请求,而不是直接请求后端接口。

例如看下面的例子,此时的baseurl是 localhost:88/swagger/ware/ 其中localhost:88是网关的地址,转发请求的时候会自动去除前缀/swagger/ware/
在这里插入图片描述
随便找一个接口测试,发现我们远的的路径应该是localhost:88/swagger/ware/ware/purchase/info/2的,但是由于我们配置了pathMapping为test,所以在baseurl和path之间多了个test(pathMapping)
也就是说swagger的请求路径为baseurl+pathMapping+path
在这里插入图片描述

花絮2:路由规则的java写法

上面提到我们使用了java写了路由配置,路由配置如下,下面内容在yml里面配置的路由可以配置出等价的路由,但是我们从上面的分析也可以看到,他们处于链路的最上面。其中的RouteLocator 就是我们上面在SwaggerConfig用到的那一个。

@Configuration
public class TestConfig {
    
    

    /*
    * 通过RouteLocatorBuilder的routes,可以逐一建立路由,每调用route一次可建立一条路由规则.
    * p的代表是PredicateSpec,可以透过它的predicate来进行断言,要实现的接口就是Java 8的Predicate,
    * 通过exchange取得了路径,然后判断它是不是以/testRouteLocator/开头。
    * */
    @Bean
    public RouteLocator routeLocator(RouteLocatorBuilder builder) {
    
    
        return builder.routes()
                .route(p -> p
                        .predicate(exchange -> exchange.getRequest().getPath().subPath(0).toString().startsWith(("/testRouteLocator/")))
                        .filters(f -> f.rewritePath("/testRouteLocator/(?<remaining>.*)", "/${remaining}"))
                        .uri("lb://gulimall-product"))
                .route(p -> p
                        .predicate(exchang->exchang.getRequest().getPath().toString().equals("/routelocator"))
                        .uri("lb://gulimall-product"))
                .build();
    }
}

总结

虽然只是简单的整合一个gateway和knife4j、swagger,但是其中牵涉了许多路由规则(PrefixStrip)、路由写法(yml和java)、响应式编程等等,这个过程对我来说还是挺有挑战的。
对于为什么用routeLocator和gatewaProperties的交集来匹配路由还是有些想不清楚,不清楚原博主这么写的意图是什么,暂时想不到有什么场景必须要这么做(我认为只需要gatewaProperties就可以完成获取SwaggerResource这个任务),既然他这么写那我也就先这么用,答案以后再探究。

猜你喜欢

转载自blog.csdn.net/weixin_45654405/article/details/126513991