• 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    第二个是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)));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    错误定位

    通过控制台的网络请求记录可以看到,我们是先请求了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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    花絮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}"))
                            .uri("lb://gulimall-product"))
                    .route(p -> p
                            .predicate(exchang->exchang.getRequest().getPath().toString().equals("/routelocator"))
                            .uri("lb://gulimall-product"))
                    .build();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    总结

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

  • 相关阅读:
    国科大数据挖掘期末复习——聚类分析
    【 java 枚举类】java 枚举类
    【云原生之kubernetes实战】在k8s环境下部署Discuz论坛系统
    间隔不到一年开两店,温州鸿雁全屋智能经销商透露了他的生意经
    CKS 认证备考指南
    nodejs 入门基本操作
    中国贵金属白银技巧:与做人
    数据结构 - ArrayList - 动态修改的数组
    面试突击71:GET 和 POST 有什么区别?
    CTF学习路线!最详细没有之一!(建议收藏)
  • 原文地址:https://blog.csdn.net/weixin_45654405/article/details/126513991