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这个任务),既然他这么写那我也就先这么用,答案以后再探究。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340