草庐IT

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

Dnils 2023-04-11 原文

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

有关Gateway整合微服务文档:Knife4j文档请求异常、Swagger报错Failed to load API definition.的更多相关文章

  1. Matlab imread()读到了什么 (浅显 当复习文档了) - 2

    matlab打开matlab,用最简单的imread方法读取一个图像clcclearimg_h=imread('hua.jpg');返回一个数组(矩阵),往往是a*b*cunit8类型解释一下这个三维数组的意思,行数、数和层数,unit8:指数据类型,无符号八位整形,可理解为0~2^8的数三个层数分别代表RGB三个通道图像rgb最常用的是24-位实现方法,即RGB每个通道有256色阶(2^8)。基于这样的24-位RGB模型的色彩空间可以表现256×256×256≈1670万色当imshow传入了一个二维数组,它将以灰度方式绘制;可以把图像拆分为rgb三层,可以以灰度的方式观察它figure(1

  2. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  3. Ruby 等同于 Sphinx 文档生成器? - 2

    Ruby有一些不错的文档生成器,例如Yard、rDoc,甚至Glyph。问题是Sphinx可以做网站、PDF、epub、LaTex等。它在重组文本中完成所有这些事情。在Ruby世界中有替​​代方案吗?也许是程序的组合?如果我也能使用Markdown就更好了。 最佳答案 自1.0版以来,Sphinx有了“域”的概念,它是从Python和/或C以外的语言标记代码实体(如方法调用、对象、函数等)的方法。有一个rubydomain,所以你可以只使用Sphinx本身。您唯一会缺少的(我认为)是Sphinx使用autodoc从源代码自动创建文档

  4. ruby-on-rails - 在 irb 中阅读文档 - 2

    我怀念ipython的一件事是它有一个?为特定功能挖掘文档的运算符。我知道ruby​​有一个类似的命令行工具,但是我在irb中调用它非常不方便。ruby/irb有类似的东西吗? 最佳答案 Pry是IPython的Ruby版本,它支持?命令来查找有关方法的文档,但语法略有不同:pry(main)>?File.dirnameFrom:file.cinRubyCore(CMethod):Numberoflines:6visibility:publicsignature:dirname()Returnsallcomponentsofthef

  5. ruby - 使用 Nokogiri 和 Ruby 从 html 文档获取链接和 href 文本? - 2

    我正在尝试使用nokogirigem提取页面上的所有url及其链接文本,并将链接文本和url存储在散列中。FooBar我想回去{"Foo"=>"#foo","Bar"=>"#bar"} 最佳答案 这是一个单行:Hash[doc.xpath('//a[@href]').map{|link|[link.text.strip,link["href"]]}]#=>{"Foo"=>"#foo","Bar"=>"#bar"}拆分一点可以说更具可读性:h={}doc.xpath('//a[@href]').eachdo|link|h[link.t

  6. ruby - 如何让 Nokogiri 解析并返回 XML 文档? - 2

    这是一些奇怪的例子:#!/usr/bin/rubyrequire'rubygems'require'open-uri'require'nokogiri'print"withoutread:",Nokogiri(open('http://weblog.rubyonrails.org/')).class,"\n"print"withread:",Nokogiri(open('http://weblog.rubyonrails.org/').read).class,"\n"运行此返回:withoutread:Nokogiri::XML::Documentwithread:Nokogiri::

  7. H2数据库配置及相关使用方式一站式介绍(极为详细并整理官方文档) - 2

    目录H2数据库入门以及实际开发时的使用1.H2数据库的初识1.1H2数据库介绍1.2为什么要使用嵌入式数据库?1.3嵌入式数据库对比1.3.1性能对比1.4技术选型思考2.H2数据库实战2.1H2数据库下载搭建以及部署2.1.1H2数据库的下载2.1.2数据库启动2.1.2.1windows系统可以在bin目录下执行h2.bat2.1.2.2同理可以通过cmd直接使用命令进行启动:2.1.2.3启动后控制台页面:2.1.3spring整合H2数据库2.1.3.1引入依赖文件2.1.4数据库通过file模式实际保存数据的位置2.2H2数据库操作2.2.1Mysql兼容模式2.2.2Mysql模式

  8. Ruby YAML 多文档 - 2

    这是我的YAML文件“test.yml”:---alpha:100.0beta:200.0gama:300.0---3...第一个文档是一个散列。第二个文档是一个整数。我正在尝试将它们作为散列和整数加载到Ruby程序中。这是我目前的尝试:require'yaml'variables=YAML.load_file('test.yml')putsvariables.inspect 最佳答案 要访问单个文件中的多个YAML文档,请使用load_stream方法(正如“matt”在对其他答案之一的评论中提到的):YAML.load_stre

  9. ruby-on-rails - 如何使用 grape swagger ui 传递数组? - 2

    我在下面定义了api端点:paramsdorequires:ids,type:Array,desc:'Arrayofgroupids'end我无法从Swagger生成的UI传递数组。如果我输入[1,2,3,4]或ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3然后两者都无效.如果我使用数组调用spec中的api,它就可以工作。我的客户想尝试Swagger的整个api,所以我想要一个适用于SwaggerUI的解决方案。 最佳答案 我对所有情况的解决方案:paramsdorequires:ids,type:Arra

  10. ruby - 自动将院子文档框架添加到现有的 Rails 遗留代码中 - 2

    关闭。这个问题不符合StackOverflowguidelines.它目前不接受答案。我们不允许提问寻求书籍、工具、软件库等的推荐。您可以编辑问题,以便用事实和引用来回答。关闭4年前。Improvethisquestion我希望能够将模板化的YARD文档样式注释插入到我现有的Rails遗留应用程序中。目前它的评论很少。我想要具有指定参数的类header和方法header(通过从我假定的方法签名中提取)和返回值的占位符。在PHP代码中,我有一些工具可以检查代码并在适当的位置创建插入到代码中的文档header注释。在带有Ducktyping等的Ruby中,我确信诸如@params等类型之类

随机推荐