• springdoc与spring cloud gateway整合经验分享


    springdoc与spring cloud gateway整合经验分享

    最近对系统的架构进行了升级,从spring boot 2.1.x升级到了2.7.0.原先使用的是swagger2进行API文档管理。升级后出现了不少兼容性问题,索性将swagger升级到了springdoc.

    项目配置:

    • spring boot: 2.7.0
    • spring cloud: 2021.0.3
    • springdoc: 1.16.10

    一、升级子服务

    首先,将各业务子系统进行升级。这个升级过程基本与网上相关的教程差不多。

    1. 引入SpringDoc的依赖

    先在主pom中,加入springdoc相关的依赖管理:

        <dependencyManagement>
            <dependencies>
    		    
    		    <dependency>
    		        <groupId>org.springdocgroupId>
    		        <artifactId>springdoc-openapi-uiartifactId>
    		        <version>${springdoc.version}version>
    		    dependency>
    		    <dependency>
    		        <groupId>org.springdocgroupId>
    		        <artifactId>springdoc-openapi-webflux-uiartifactId>
    		        <version>${springdoc.version}version>
    		    dependency>
    		    <dependency>
    		        <groupId>org.springdocgroupId>
    		        <artifactId>springdoc-openapi-commonartifactId>
    		        <version>${springdoc.version}version>
    		    dependency>
            dependencies>
        dependencyManagement>
        ...
        <properties>
            ...
            <springdoc.version>1.6.10springdoc.version>
        properties>
    
    • 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

    接下来,在子项目中,去除swagger2的依赖,换上springdoc的依赖。

    注意,这里有个坑。一开始为了方便,大家可能会想保留旧的注解,先把springdoc运行起来,看看效果,这样可以减少一些工作量。可问题在于springdoc虽然是基于swagger3开发,但是并不兼容swagger2,而且会导致对swagger相关包依赖的版本冲突,导致springdoc无法正常加载。因此,一定要把swagger2的包清理干净

    			<dependency>
    		        <groupId>org.springdocgroupId>
    		        <artifactId>springdoc-openapi-uiartifactId>
    		    dependency>
    
    • 1
    • 2
    • 3
    • 4

    还有一点要注意的是,一般稍大一些的项目,会开发很多的二方包,这些二方包中因为会封装很多共用 的POJO,因此一般也会对swagger2有依赖。这些依赖也会传递到各子系统中,引起版本冲突。因此,如果有二方包时,建议先将二方包中的swagger依赖改为可选。效果如下。

     			<dependency>
                    <groupId>io.springfoxgroupId>
                    <artifactId>springfox-swagger2artifactId>
                    <optional>trueoptional>
                dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2. 调整配置

    springdoc是开箱即用的,理论上只要删除旧的swaager配置类就可以了。但是,从安全方面考虑,最好限制在生产环境下启用。springdoc这方面比swagger好的一点,是默认提供了一个开关配置。建议在application.yml中将这个配置默认关闭。

    2.1 生产环境默认关闭API文档服务
    springdoc:
      api-docs:
        # 默认生产环境关闭文档功能。
        enabled: false
    
    • 1
    • 2
    • 3
    • 4

    在开发或者测试环境中开启,比如application-dev.yml中。

    springdoc:
      api-docs:
        enabled: true
    
    • 1
    • 2
    • 3
    2.2 文档说明信息配置(可选)

    如果需要配置文档的说明信息,可以增加一个配置类。

    /**
     * SpringDoc配置。
     *
     * @author 马翼超
     * @since 1.0
     */
    @Configuration
    @ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true")
    public class SpringDocConfig {
    
        @Bean
        @ConfigurationProperties(prefix = "springdoc.api-docs.info")
        public Info springDocInfo() {
            return new Info();
        }
    
        @Bean
        public OpenAPI openAPI(Info infoConfig) {
            return new OpenAPI().info(infoConfig);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这里springdoc.api-docs.info的配置项位置并不是springdoc默认的,可以自定义。

    配置范例。

    springdoc:
      api-docs:
        info:
          title: 服务Api文档
          description: 文档说明
          contact:
          	name: Myc
          	email: mycsoft@qq.com
          	url: http://mycsoft.cn/
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    具体的配置项可以查看Info的源码。

    3. 替换Swagger2的注解

    接下来就是更换注解的工作了。如果POJO与Controller类很多,那这就是一个比较辛苦的工作了。我的经验是使用正则表达式进行全局替换,这样速度要快很多。比如:将@ApiModelProperty\(\s*value\s*=\s*(".+")\)替换为@Schema(description = &1).

    3.1 Swagger与Springdoc注解对照表
    swagger 2spring doc描述
    @Api@Tag修饰 controller 类,类的说明
    @ApiOperation@Operation修饰 controller 中的接口方法,接口的说明
    @ApiModel@Schema修饰实体类,该实体的说明
    @ApiModelProperty@Schema修饰实体类的属性,实体类中属性的说明
    @ApiImplicitParams@Parameters接口参数集合
    @ApiImplicitParam@Parameter接口参数
    @ApiParam@Parameter接口参数

    网上有些文章中,将@ApiModel@ApiModelPropertyvalue替换为@Schematitle。我感觉这样展示的效果并不好,建议替换为description。有兴趣的同学可以试验一下两者的区别。

    3.2 Controller范例
    @Slf4j
    @RestController
    @RequestMapping("/sample")
    @Tag(name = "sample接口")
    @CrossOrigin
    public class HelloController {
    
        @Autowired
        private AuthService service;
    
        @Operation(summary = "问候")
        @GetMapping("/hello")
        public String hello(
                @Parameter(description = "名称") 
                @RequestParam String name
        ) {
            return "hello " + name;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    注意,如果未来有网关聚合文档的需求,controller上需要增加@CrossOrigin注解,解决跨域问题。

    3.3 POJO范例
    @Data
    @Schema(description ="个人信息")
    public class PersonalInfo {
    
        @Schema(description = "姓名")
        private String name;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    3.4 预览效果

    替换完之后,就可以运行一下看看效果了。地址是 http://localhost:8080/swagger-ui/

    效果截图

    二、升级网关(Spring Cloud Gateway)

    建议先将所有子系统都升级完之后再升级网关应用。不然很难进行文档聚合。

    Spring Cloud Gateway采用的是响应式,因此,要使用对应的WebFlux组件。

    		
            <dependency>
                <groupId>org.springdocgroupId>
                <artifactId>springdoc-openapi-webflux-uiartifactId>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    参考子系统的升级的其它步骤,如果在网关上也有实现接口,那么这样就可以直接运行了。

    1. 聚合子服务API文档

    通常我们希望网关可以聚合所有前端接口的文档,这样前端的同学就不需要频繁的切换服务了。虽然springdoc不能自动进行聚合处理,不过相比swagger,提供了一些便捷的手段。

    一般聚合文档的模式都是按服务分组。这里和大家分享一下我整理的三种方案。

    1.1 聚合方案一:手工配置

    范例如下:

    spring:
      cloud:
        gateway:
          discovery:
            locator:
              enabled: true
          routes:
          	...
            # ==============================================================
            # apidocs资源路由配置
            - id: hello-api-doc
              uri: lb://sample-hello/
              predicates:
                ## 转发地址格式为 uri/archive
                - Path=/sample-hello/v3/api-docs/**
              filters:
                - StripPrefix=1
    springdoc:
      ...
      swagger-ui:
        urls:
          - name: 网关服务接口
            url: /v3/api-docs
          - name: Hello服务
          	# url前缀要与路由配置中的Patch呼应。
            url: /sample-hello/v3/api-docs
    
    • 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. 如果子服务太多,配置量会比较大;
    2. apidocs的路由配置需要与其它正常业务路由写死在配置中,在生产环境下不方便剥离。会有安全隐患。

    1.2 聚合方案二:自动配置

    通过编写自动配置类的方式,进行自动解析。这种方案的案例网上很多,方式不一,这里我就是不一一累述了。有兴趣的同学要查看相关的文档。给大家推荐几篇。

    看似方便,但是比较难灵活的控制需要暴露的接口。适用于网关路由规则相对单纯,一个服务一个路由的情况。
    不过在安全性要求较高的生产环境中,或者权限控制比较复杂的场景中,这中“一刀切”的路由配置显然是不合适的。

    我将前两种方案进行了整合为一种半自动的方案。

    1.3 聚合方案三:半自动配置

    对于子服务的路由采用自动化配置的方式。不过绑定上springdoc的开关。这样就可以防止资源在生产环境下被过度开放的风险。

    1.3.1 自动开启子服务的API文档聚合资源路由
    /**
     * Springdoc子服务apidocs资源路由。
     *
     * 本配置用于聚合子服务api文档时,自动开通相关子服务在网关的/v3/apidocs/**路由的场景。
     *
     * 仅用于springdoc功能打开的场景。
     *
     * @author 马翼超
     * @since 1.0
     */
    @Slf4j
    @RequiredArgsConstructor
    @Configuration
    @ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true")
    public class SpringDocSubApiDocsRouteAutoConfiguration implements ApplicationEventPublisherAware {
    
        private static final String DISCOVERY_CLIENT_ID_PRE = "ReactiveCompositeDiscoveryClient_";
    
        @Value("${spring.application.name}")
        private String selfServiceName;
    
        @Autowired
        private RouteDefinitionWriter routeDefinitionWriter;
        @Autowired
        private RouteDefinitionLocator locator;
    
        @Setter
        private ApplicationEventPublisher applicationEventPublisher;
    
        @SneakyThrows
        @PostConstruct
        public void init() {
            installAllSubServiceApiDocsRoutes();
        }
    
        /**
         * 加载所有子服务的apidocs的路由配置。
         */
        private void installAllSubServiceApiDocsRoutes() {
            List<RouteDefinition> definitions = ofNullable(locator.getRouteDefinitions().collectList().block())
                    .orElseGet(ArrayList::new);
            final String selfServiceId = DISCOVERY_CLIENT_ID_PRE + selfServiceName;
            //解析出所有子服务名。
            List<String> services = definitions.stream()
                    .filter(routeDefinition -> ofNullable(routeDefinition.getId())
                    //只保留服务级别的路由。
                    .filter(id -> id.startsWith(DISCOVERY_CLIENT_ID_PRE))
                    //排除本系统。
                    .filter(id -> !selfServiceId.equalsIgnoreCase(id))
                    .isPresent())
                    .map(routeDefinition -> routeDefinition.getUri().toString().replace("lb://", "").toLowerCase())
                    .collect(toList());
            services.forEach(this::installRoute);
            if (CollectionUtils.isNotEmpty(services) && log.isInfoEnabled()) {
                log.info("自动安装了{}个子服务的apidocs路由:{}", services.size(),
                        services.stream().collect(joining(",")));
            }
        }
    
        /**
         * 安装一个子服务的apidoc路由。
         *
         * @param serviceName
         */
        private void installRoute(String serviceName) {
            RouteDefinition routeDefinition = new RouteDefinition();
            routeDefinition.setId(serviceName + "-apidocs");
            routeDefinition.setUri(URI.create("lb://" + serviceName));
            routeDefinition.setPredicates(Arrays.asList(new PredicateDefinition("Path=/" + serviceName + "/v3/api-docs/**")));
            routeDefinition.setFilters(Arrays.asList(new FilterDefinition("StripPrefix=1")));
            routeDefinition.setOrder(-1);
            routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
            applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this.routeDefinitionWriter));
        }
    
    }
    
    • 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
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    这个配置类,简单来说,就是当springdoc被启用时,会在启动后自动加载所有子服务的apidocs路由。生成的内容与方案一中的路由配置一样。每个子系统以自己的服务名为标识,生成一个{服务名}-apidocs的路由,Path的判定规则是Path=/{服务名}/v3/apidocs/**

    这里虽然也开放了所有子服务的/v3/api-docs/的资源,不过受springdoc开关的控制。一般只会在开发环境启用,安全风险可忽略。

    1.3.2 子服务文档聚合UI配置

    然后对于需要暴露的接口采用手工配置的方式url/{服务名}/v3/apidocs/的约束进行配置。这样就可以手动指定只需要暴露给前端同学的服务。

    springdoc:
      ...
      swagger-ui:
        urls:
          - name: 网关服务接口
            url: /v3/api-docs
          - name: Hello服务
          	# url前缀要与路由配置中的Patch呼应。
            url: /sample-hello/v3/api-docs
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    三、扩展:子服务接口分包

    再和大家分享一个接口分类的经验。一般每个子系统的接口会分这么几类。

    1. 面向前端调用的接口:这类接口受前端需求的影响比较大,需要暴露给前端应用;
    2. 内部协调接口:这类接口仅用于内部系统之间进行业务协作用,一般不暴露给前端;
    3. 面向第三方接口:这类接口用于与外部系统对接,比如开放平台接口等,需要暴露给第三方的开发者。

    这几类接口我们都需要提供完备的文档供调用者进行对接(一人全栈开发模式可以滑走了),但是对于不同类的阅读者,把所有接口都一并暴露出来不合适,有时也会触犯保密规定。那么,我们可以利用springdoc的分组机制对这些接口进行自动化分类。
    基于刚刚的半自动配置方案,我已经可以做到针对服务进行接口分类,还能不能对接口的分类管理再细化呢?
    答案是可以的。springdoc继承了swagger的分组机制,并提供了默认的分组配置规则。比如,我们可以将一个服务的接口按路径进行分组。如:

    springdoc:
      group-configs:
        - group: front
          display-name: 前端接口
          paths-to-match: /front/**
        - group: inner
          display-name: 内部接口
          paths-to-match: /inner/**
        - group: openapi
          display-name: 第三方开放接口
          paths-to-match: /openapi/**
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    然后在Web网关中只配置前端接口

    springdoc:
      ...
      swagger-ui:
        urls:
          - name: 网关服务接口
            url: /v3/api-docs
          - name: Hello服务
            url: /sample-hello/v3/api-docs/front
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    开放平台接口网关中只配置第三方开放接口

    spring:
      cloud:
        gateway:
          discovery:
            locator:
              enabled: true
          routes:
          	...
            # ==============================================================
            # apidocs资源路由配置
            - id: hello-api-doc
              uri: lb://sample-hello/
              predicates:
                ## 转发地址格式为 uri/archive
                - Path=/sample-hello/v3/api-docs/openapi
              filters:
                - StripPrefix=1
    springdoc:
      ...
      swagger-ui:
        urls:
          - name: Hello接口服务
            url: /sample-hello/v3/api-docs/openapi
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    开放平台的网关api文档可能会在生产环境中启用,因此,这里的路由配置建议也是手动配置

    附录:参考与引用

  • 相关阅读:
    EtherCAT 伺服控制功能块实现
    where 1=1 是什么意思
    tokenizers Tokenizer 类
    【八大排序】java版(上)(冒泡、快排、堆排、选择排序)
    【Rust日报】2022-06-28 RustExplorer - 自带10000个crate的Rust在线运行环境
    【再识C进阶3(上)】详细地认识字符串函数、进行模拟字符串函数以及拓展内容
    Django常用命令
    转铁蛋白修饰蛇床子素长循环脂质体/负载三七皂苷R1的PEG-PLGA纳米粒(R1@Tf-PEG-PLGA NPs)
    华为数字化转型之道认知篇第一章数字化转型,华为的战略选择
    《排错》Python重新安装后,执行yum命令报错
  • 原文地址:https://blog.csdn.net/mycsoft/article/details/126413551