SPRING CLOUD GATEWAY 集成 SWAGGER 方案总结 (请求路径中带有 ",")

本贴最后更新于 1337 天前,其中的信息可能已经时移世改

转载:https://www.freesion.com/article/50961265470/

前言

在微服务大行其道到今天,服务到碎片化也带来了管理和监控的困难(统一集成网关系统在前面的 sia-gateway 的文章中有做分享,感兴趣的可以前往阅读)。
swagger 为我们开发带来了极大到便利,但是在庞杂的系统中,即便对于开发的接入和调试,对各个分散对在线文档,也显得有些杂乱无章。即便是有如同 yapi 等在线文档系统,也同样需要精力去维护,而且对于文档与代码分离,同样会带来版本不一致和调试困难等问题。
在网关层集成 swagger 显得非常易用和优雅。但是面对各个公司不同网关配置方案,给 swagger 的集成带来和诸多不便和技术难题。下面我分享我在实现 spring cloud gateway 集成 swagger 的实现方案。# 系列文章目录
提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加


一、服务的 SWAGGER 配置

1.引入依赖包

在 pom 文件中添加如下依赖,推荐使用 2.9.2 版本

<!-- swagger 包--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency>

2.添加 SWAGGER 配置类

@Configuration @EnableSwagger2 @ConditionalOnProperty(name = "swagger.enable", havingValue = "true") public class SwaggerConfig { @Bean public Docket openApi() { return new Docket(DocumentationType.SWAGGER_2) .groupName("open") .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.example.api.controller")) .paths(PathSelectors.any()) .build(); } @Bean public Docket adminApi() { return new Docket(DocumentationType.SWAGGER_2) .groupName("admin") .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.example.api.controller")) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("微服务swagger") .contact(new Contact("LiuChao", "", "liuchao332@163.com")) .version("1.0") .description("API 描述") .build(); } }

说明: 1、此配置类为基本的 swagger 集成的基本配置,有些区别的是,此处配置类 2 个 Docket 元素,分别设置了不同的 groupName。此操作是为了满足,将业务端和管理端分成不同的 swagger 页面进行展示。这个地方会为网关集成带来一些改变。不是必须要这样设置,不想将接口分开,放到同一个页面也没什么不可。 2、在配置项中需要新增 swagger.enable 配置,来控制是否启用 swagger。# 二、SPRING CLOUD GATEWAY 集成 SWAGGER

1.SPRING CLOUD GATEWAY 搭建

spring cloud gateway 的集成非常简单,基本上实现来了开箱即用,进行简单的配置,集成 eureka,就能满足我们基本的业务场景,这里就不在赘述了。

2.在网关中引入 SWAGGER 包

和微服务所应用的包相同

3.添加配置类

网关的 swagger 集成方案与服务单元有较大不同,也更为复杂。

@Configuration public class SwaggerConfig { @Bean public SecurityConfiguration securityConfiguration() { return SecurityConfigurationBuilder.builder().build(); } @Bean public UiConfiguration uiConfiguration() { return UiConfigurationBuilder.builder().showExtensions(true).build(); } }

此配置类中定义 SecurityConfiguration,UiConfiguration 将在下面的配置中用到这 2 个 bean

@RestController @RequestMapping("/swagger-resources") public class SwaggerHandler { private final SecurityConfiguration securityConfiguration; private final UiConfiguration uiConfiguration; private final SwaggerResourcesProvider swaggerResources; @Autowired public SwaggerHandler(SwaggerResourcesProvider swaggerResources, SecurityConfiguration securityConfiguration, UiConfiguration uiConfiguration) { this.swaggerResources = swaggerResources; this.securityConfiguration = securityConfiguration; this.uiConfiguration = uiConfiguration; } @GetMapping("/configuration/security") public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() { return Mono.just(new ResponseEntity<>( Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK)); } @GetMapping("/configuration/ui") public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() { return Mono.just(new ResponseEntity<>( Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK)); } @GetMapping public Mono<ResponseEntity<List<SwaggerResource>>> swaggerResources() { return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK))); } }

此类定义了 swagger 网关层的开放接口,在访问 swagger-ui 中会拉去此接口的数据。

@Primary @Component @AllArgsConstructor public class SwaggerProvider implements SwaggerResourcesProvider { public static final String SOURCE_URI = "http://%s/swagger-resources"; private final RouteLocator routeLocator; private final RouteDefinitionLocator routeDefinitionLocator; private final RestTemplate restTemplate; @Override public List<SwaggerResource> get() { List<SwaggerResource> resources = new ArrayList<>(); List<String> routes = new ArrayList<>(); routeLocator.getRoutes().subscribe(route -> routes.add(route.getId())); routeDefinitionLocator.getRouteDefinitions() .filter(routeDefinition -> routes.contains(routeDefinition.getId())) .subscribe(routeDefinition -> routeDefinition.getPredicates().stream() .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName())) .forEach(predicateDefinition -> resources .addAll(swaggerResource(routeDefinition)))); return resources; } private List<SwaggerResource> swaggerResource(RouteDefinition route) { try { String sourceUrl = String.format(SOURCE_URI, route.getUri().getHost()); ResponseEntity<String> content = restTemplate.getForEntity(sourceUrl, String.class); List<SwaggerResource> swaggerResources = JsonUtil.toList(content.getBody(), SwaggerResource.class); swaggerResources.stream().forEach(swaggerResource -> { swaggerResource.setName(route.getUri().getHost().toLowerCase() + "-" + swaggerResource.getName()); swaggerResource.setUrl("/" + route.getUri().getHost().toLowerCase() + swaggerResource.getUrl()); }); return swaggerResources; } catch (Exception e) { return new ArrayList<>(); } } }

这个类是核心,这个类封装的是 SwaggerResource,即在 swagger-ui.html 页面中顶部的选择框,选择服务的 swagger 页面内容。
RouteLocator:获取 spring cloud gateway 中注册的路由
RouteDefinitionLocator:获取 spring cloud gateway 路由的详细信息
RestTemplate:获取各个配置有 swagger 的服务的 swagger-resources

三、私货

在一般的情况下,网关一般使用一个服务,一个路由的配置方式,我也建议大家这么使用,这样管理控制都非常灵活。但在项目中,也遇到过一种特别的配置方式。如下:

routes: - id: remove_api_prefix uri: http://127.0.0.1:80 order: 1 predicates: - Path=/api/** filters: - StripPrefix=1

按照这种配置方式,在请求 url 前加上/api 前缀,网关在收到此类请求后,将 api 截取掉,在转发给自己;网关再次收到没有 api 的请求后,对请求第二级目录按服务名进行路由。
这种方式可以将在注册中心注册对服务都暴露到网关到路由中,就不需要对每个服务配置路由规则。
对于新加的服务,也不行要配置新的路由。

但是这样也引起了新的问题,在 swagger 的 baseUrl,出现了,符号

www.example-gateway.com/api,/example-service/

且通过 swagger 发送的请求,在 url 上都出现了,。导致不能够请求到正确到地址上,然后就开始分析这个,的来源。

curl -X POST “http://www.example-gateway.com/api,/example-service/user/info” -H “accept: /

1.分析一

初步可以判断,这个符号肯定和这种特殊的路由方式有关系,因为在使用一服务一路由的配置方式的时候,并没有这个问题。从哪里入手呢,还是从服务的 swagger 页面入手,因为这个符号就来自这里。

如上截图,在 api-docs 接口中所返回的数据里,basePath 就已经带上里这个符号。有了一个初步的定位思路,从 swagger 的 api-docs 接口入手。

2.分析二

在找到 swagger 接口源头,往下 debug,终于在 XForwardPrefixPathAdjuster 类中发现类这个符号出现的地方。源码如下图,

public class XForwardPrefixPathAdjuster implements PathAdjuster { static final String X_FORWARDED_PREFIX = "X-Forwarded-Prefix"; private final HttpServletRequest request; public XForwardPrefixPathAdjuster(HttpServletRequest request) { this.request = request; } public String adjustedPath(String path) { String prefix = this.request.getHeader("X-Forwarded-Prefix"); if (prefix != null) { return !SpringVersionCapability.supportsXForwardPrefixHeader(SpringVersion.getVersion()) ? prefix + path : prefix; } else { return path; } } }

本以为发现源头,但是在 debug 的过程中,到这里接收到 http 头的 X-Forwarded-Prefix 参数,已经带有这个符号。
由此可以判断,在 swagger 接收到到 http 请求就已经带有这个符号。所以只能往上找原因。那就只有一个地方了,问题出在网关。

3.分析三

然后我就开始 spring-cloud-gateway-core-2.2.2.RELEASE.jar 包里,找什么地方在封装 header 的 X-Forwarded-Prefix 参数。有赖于 spring 合理的代码和包结构,没用多少时间就找到 XForwardedHeadersFilter 类,如下所示:

private void updateRequest(HttpHeaders updated, URI originalUri, String originalUriPath, String requestUriPath) { if (requestUriPath != null && originalUriPath.endsWith(requestUriPath)) { String prefix = substringBeforeLast(originalUriPath, requestUriPath); if (prefix != null && prefix.length() > 0 && prefix.length() <= originalUri.getPath().length()) { this.write(updated, "X-Forwarded-Prefix", prefix, this.isPrefixAppend()); } } }

在此方法,完成的 header 头的 X-Forwarded-Prefix 的封装。再往下看 write 方法

private void write(HttpHeaders headers, String name, String value, boolean append) { if (append) { headers.add(name, value); List<String> values = headers.get(name); String delimitedValue = StringUtils.collectionToCommaDelimitedString(values); headers.set(name, delimitedValue); } else { headers.set(name, value); } }

在这里,当网关在截取掉 api,再发送给自己,这个地方就会在此请求中,List 会将这个请求头中封装两次的 X-Forwarded-Prefix,/api 和/example-service。StringUtils.collectionToCommaDelimitedString(values)方法将它们拼接到一起,就产生了,符号。如今要想办法消除掉这个符号。灵机一动,在写个 filer,将,干掉就行了。

4.解决

添加一个 global filter,删除 X-Forwarded-Prefix 参数到,符号。完美解决。

@Component public class XForwardedPrefixFilter implements HttpHeadersFilter, Ordered { @Override public HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange) { List<String> xForwareds = input.get("X-Forwarded-Prefix"); String forward = xForwareds.get(0); if (forward.contains(",")) { forward = forward.replaceAll(",", "/"); } input.set("X-Forwarded-Prefix", forward); return input; } @Override public int getOrder() { return 1; } }
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3198 引用 • 8215 回帖 • 1 关注
  • Swagger

    Swagger 是一款非常流行的 API 开发工具,它遵循 OpenAPI Specification(这是一种通用的、和编程语言无关的 API 描述规范)。Swagger 贯穿整个 API 生命周期,如 API 的设计、编写文档、测试和部署。

    26 引用 • 35 回帖 • 1 关注
  • 网关
    5 引用 • 7 回帖
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    948 引用 • 1460 回帖
1 操作
jockming112 在 2021-08-24 17:07:40 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...