今天使用Gateway应用系统定制开发整合的文档的时候发现Knife4j应用系统定制开发文档请求异常,应用系统定制开发查看数据包发现请求了应用系统定制开发这样的一个路径。(省流助手:应用系统定制开发错误原因是获取api-doc应用系统定制开发的方法错误,应用系统定制开发如果不明白我在说什么,应用系统定制开发那么可以往下看看)
应用系统定制开发整合的代码是在网上直接CV的,应用系统定制开发看来是需要做一些修改,其中比较重要的是在gateway的两个配置,其他服务的配置文件和单机时一致。gateway的配置文件如下:
第一个是Config
@Slf4j@Component@Primary@AllArgsConstructorpublic 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的各个配置节点 */@RestControllerpublic 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的-resource获取api-docs,然后再请求api-docs。
-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用到的那一个。
@Configurationpublic 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(); }}
- 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这个任务),既然他这么写那我也就先这么用,答案以后再探究。