在网关实现合并多个微服务Swagger接口文档的详细步骤

原创 吴就业 113 0 2021-05-03

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/d09d99b08e7c4f348c605e9f25a4f809

作者:吴就业
链接:https://wujiuye.com/article/d09d99b08e7c4f348c605e9f25a4f809
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

由于微服务的划分,使用Swagger生成的接口文档也随之拆散,前端同事不得不把每个微服务的接口文档保存为浏览器标签,方便快速切换。在引入网关之后我们想改善这个问题,统一多个微服务接口文档的入口,最好不需要将每个微服务暴露到外网,能够统一配置是否开启接口文档功能,也不需要为接口文档配置路由规则。

WebFlux整合Swagger

基于Spring Cloud Gateway开发微服务网关的前提是我们已经了解了响应式编程,并且会使用Project Reactor、WebFlux提供的API。而在网关项目中整合Swagger实际就是在WebFlux项目中整合Swagger。

首先是在项目中添加Swagger相关依赖,注意选择版本号。

由于我们项目使用的Spring Boot版本号是2.3.0.RELEASE,因此我们选择的Swagger版本号为2.10.x。

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-spring-webflux</artifactId>
    <version>2.10.5</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.10.5</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.10.5</version>
</dependency>

接着需要为WebFlux添加静态资源文件访问路径映射,即添加ResourceWebHandler。

@EnableSwagger2WebFlux
@Configuration
public class WebfluxConfiguration implements WebFluxConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/swagger-ui.html**")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

}

到此为止,我们只是为一个WebFlux项目配置了Swagger功能,现在打开浏览器访问/swagger-ui.html#/看到的只是Swagger为网关项目生成的接口文档。

合并多个微服务Swagger接口文档

方案一(笔者从一些博客看到的)

在网关项目中自定义SwaggerResourcesProvider替换Swagger提供的。

自定义SwaggerResourcesProvider实现SwaggerResourcesProvider接口的get方法,方法可返回多个SwaggerResource,每个SwaggerResource对应每个微服务,我们可以过滤掉网关自身的,代码如下。

@Primary
@Profile({"dev", "test"}) // 仅本地测试、测试环境开启
@Component
public class GatewaySwaggerResourcesProvider implements SwaggerResourcesProvider{
    @Override
    public List<SwaggerResource> get() {
        List<SwaggerResource> swaggerResources = new ArrayList<>();
        routeProperties.getRoutes().forEach(routeDefinition -> {
            String routeId = routeDefinition.getId();
            String baseUrl;
            if (isLocalDebug) {
                 baseUrl = "http://127.0.0.1:" + routeDefinition.getUri().getPort();
            } else {
                 baseUrl = ${网关域名} + "/" + ${路由前缀};
            }
            swaggerResources.add(getSwaggerResources(routeId, baseUrl));
        });
        return swaggerResources;
    }

    private SwaggerResource getSwaggerResources(String name, String baseUrl) {
            SwaggerResource resource = new SwaggerResource();
            resource.setName(name);
            resource.setLocation(baseUrl + "/v2/api-docs");
            resource.setSwaggerVersion("2.0");
            return resource;
    }
}

这些SwaggerResource就是我们在Swagger ui看到的”select a definition”的一个个选项,如下图所示。

图片

关于SwaggerResource:

GatewaySwaggerResourcesProvider中的下面这段代码只是拼接接口文档的baseUrl,满足“baseUrl+/v2/api-docs”能够直接在浏览器访问。

String baseUrl;
if (isLocalDebug) {
   baseUrl = "http://127.0.0.1:" + routeDefinition.getUri().getPort();
} else {
   baseUrl = ${网关域名} + "/" + ${路由到该应用的路由规则};
}

由于SwaggerResource配置的location是由前端直接发起请求的,而不是由网关发起请求获取再响应给前端,因此需要在网关为每个微服务配置“/v2/api-docs”接口的路由规则

最后还需要为其它微服务配置支持跨域请求,否则Swagger前端无法调用SwaggerResource配置的location向后端服务发起“/v2/api-docs”接口请求。

在其它微服务中添加跨域请求配置如下(注意:不是在网关添加!)。

@Profile({"dev", "test"}) // 仅测试环境
@ConditionalOnClass(WebMvcConfigurer.class)
@Configuration
public class CorsAutoConfiguration {

    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }

}

方案二

虽然方案一可行,但弊端是要为每个微服务添加Swagger的“/v2/api-docs”接口路由配置,笔者并未采用这种方式,而是使用自己想的一种方案:通过在网关代理后端每个微服务的“/v2/api-docs”接口请求实现。

简单说,就是每个SwaggerResource的location配置的都是网关代理的“/v2/api-docs”接口,并且给location拼接一个参数,如指向用户中心的SwaggerResource配置的location为“https://网关host/proxySwagger/v2/api-docs?routeId=usercenter”。然后由网关提供/proxySwagger/v2/api-docs接口,实现根据参数routeId向后端微服务发起“/v2/api-docs”接口请求,并将响应结果直接响应给前端。

GatewaySwaggerResourcesProvider代码实现如下:

@Primary
@Profile({"dev", "test"}) // 仅本地测试、测试环境开启
@Component
public class GatewaySwaggerResourcesProvider implements SwaggerResourcesProvider {

    @Resource
    private RouteProperties routeProperties;
    @Value("${server.domain:http://127.0.0.1:8600}")
    private String gatewayDomain;

    @Override
    public List<SwaggerResource> get() {
        List<SwaggerResource> swaggerResources = new ArrayList<>();
        // 遍历路由定义
        routeProperties.getRoutes().forEach(routeDefinition -> {
            String routeId = routeDefinition.getId();
            // 参数1:路由的id
            // 参数2:网关的域名
            swaggerResources.add(getSwaggerResources(routeId, gatewayDomain));
        });
        return swaggerResources;
    }

    private SwaggerResource getSwaggerResources(String routeId, String baseUrl) {
        SwaggerResource resource = new SwaggerResource();
        resource.setName(routeId);
        resource.setLocation(baseUrl + "/proxySwagger/v2/api-docs?routeId=" + routeId);
        resource.setSwaggerVersion("2.0");
        return resource;
    }

}

由网关提供/v2/api-docs的代理接口/proxySwagger/v2/api-docs?routeId=${routeId},代码如下:

@Profile({"dev", "test"})
@RestController
@RequestMapping("/proxySwagger")
public class GatewayApiDocsController {

    @Resource
    private RouteProperties routeProperties;
    private WebClient webClient;
    private boolean isLocalDebug;

    @PostConstruct
    public void init() {
        webClient = WebClient.create();
        isLocalDebug = ....;// dev: true, test: false
    }

    @GetMapping("/v2/api-docs")
    public Mono<String> proxyApiDocs(@RequestParam("routeId") String routeId) {
        // 根据路由id获取路由配置
        RouteDefinition routeDefinition = routeProperties.getRoutes().stream()
                .filter(rd -> rd.getId().equals(routeId))
                .findFirst().get();
        String baseUrl;
        URI routeUri = routeDefinition.getUri();
        if (isLocalDebug) {
            // 本地debug
            baseUrl = "http://127.0.0.1";
        } else {
            baseUrl = "http://" + routeUri.getHost();
        }
        if (routeUri.getPort() > 0) {
            baseUrl += (":" + routeUri.getPort());
        }
        // 转发给后端微服务
        return webClient.get()
                .uri(baseUrl + "/v2/api-docs")
                .retrieve()
                .bodyToMono(String.class);
    }
    
}

这种方案相比第一种方案优点在于:

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

Spring Native与WebFlux一样注定昙花一现?

从Spring Native官方文档来看,我是承认它的优秀的,我也会继续关注它,或许将来在合适的项目中去使用它,至少从目前的了解来看,我还不会只为性能买单,一是对现有项目的改造成本略高,二是出于目前项目的成熟度考虑我们还缺少一些云原生组件的支持。

使用Redis实现积分排行榜,并支持同积分按时间排序

使用Redis实现实时更新的排行榜并不难,Redis提供的ZSet数据结构就很适合用于实现排行榜,但如何实现相同积分情况下再支持按时间排序呢?

如何写出健壮的业务代码

我们一开始总会自信的觉得自己写出来的代码是个美女,只是写着写着越来越胖,最终写成了个200斤的胖子,自己见了都嫌弃……

多人协作如何管理Git分支

关于Git分支管理,每个团队在不同阶段都有自己的管理策略,最近我们团队也争论过这个问题。

(a+b)*10,10是存在哪里的?是常量池么?

今天看到一个很有意思的提问:(a+b)*10,10是存放在哪里的?是常量池么?如果是常量池,在进行运算的时候,是通过指针来找到的吧?

通过Linux系统调用实现文件拷贝命令深入理解Java文件读写的底层实现(含MappedByteBuffer)

继 《Java文件的简单读写、随机读写、NIO读写与使用MappedByteBuffer读写》,本篇通过调用Linux OS文件操作系统函数实现copy命令以加深我们对Java文件读写底层实现的理解。