springdoc与spring cloud gatewayapp开发定制公司app开发定制公司整合经验分享
springdoc与spring cloud gateway整合经验分享
app开发定制公司最近对系统的架构进行了升级,从spring boot 2.1.x升级到了2.7.0.app开发定制公司原先使用的是swagger2app开发定制公司进行文档管理。app开发定制公司升级后出现了不少兼容性问题,索性将swagger升级到了springdoc.
项目配置:
- spring boot: 2.7.0
- spring cloud: 2021.0.3
- springdoc: 1.16.10
一、app开发定制公司升级子服务
首先,app开发定制公司将各业务子系统进行升级。app开发定制公司这个升级过程基本与网app开发定制公司上相关的教程差不多。
1. 引入SpringDoc的依赖
先在主pom中,加入springdocapp开发定制公司相关的依赖管理:
<dependencyManagement> <dependencies> <!-- springDoc --> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> <version>${springdoc.version}</version> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-webflux-ui</artifactId> <version>${springdoc.version}</version> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-common</artifactId> <version>${springdoc.version}</version> </dependency> </dependencies> </dependencyManagement> ... <properties> ... <springdoc.version>1.6.10</springdoc.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
接下来,app开发定制公司在子项目中,去除swagger2的依赖,换上springdoc的依赖。
注意,app开发定制公司这里有个坑。一开始为了方便,大家可能会想保留旧的注解,先把springdoc运行起来,看看效果,这样可以减少一些工作量。可问题在于springdoc虽然是基于swagger3开发,但是并不兼容swagger2,而且会导致对swagger相关包依赖的版本冲突,导致springdoc无法正常加载。因此,一定要把swagger2的包清理干净。
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> </dependency>
- 1
- 2
- 3
- 4
还有一点要注意的是,一般稍大一些的项目,会开发很多的二方包,这些二方包中因为会封装很多共用 的POJO,因此一般也会对swagger2有依赖。这些依赖也会传递到各子系统中,引起版本冲突。因此,如果有二方包时,建议先将二方包中的swagger依赖改为可选。效果如下。
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <optional>true</optional> </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 2 | spring doc | 描述 |
---|---|---|
@Api | @Tag | 修饰 controller 类,类的说明 |
@ApiOperation | @Operation | 修饰 controller 中的接口方法,接口的说明 |
@ApiModel | @Schema | 修饰实体类,该实体的说明 |
@ApiModelProperty | @Schema | 修饰实体类的属性,实体类中属性的说明 |
@ApiImplicitParams | @Parameters | 接口参数集合 |
@ApiImplicitParam | @Parameter | 接口参数 |
@ApiParam | @Parameter | 接口参数 |
网上有些文章中,将
@ApiModel
与@ApiModelProperty
的value
替换为@Schema
的title
。我感觉这样展示的效果并不好,建议替换为description
。有兴趣的同学可以试验一下两者的区别。
3.2 Controller范例
@Slf4j@RestController@RequestMapping("/sample")@Tag(name = "sample接口")@CrossOriginpublic 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 预览效果
替换完之后,就可以运行一下看看效果了。地址是 。
二、升级网关(Spring Cloud Gateway)
建议先将所有子系统都升级完之后再升级网关应用。不然很难进行文档聚合。
Spring Cloud Gateway采用的是响应式,因此,要使用对应的WebFlux组件。
<!-- springdoc --> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-webflux-ui</artifactId> </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=1springdoc: ... 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
这个方法简单粗暴。可以快速验证。而且可以灵活的控制需要暴露的服务接口。不过有两个问题。
- 如果子服务太多,配置量会比较大;
- 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
三、扩展:子服务接口分包
再和大家分享一个接口分类的经验。一般每个子系统的接口会分这么几类。
- 面向前端调用的接口:这类接口受前端需求的影响比较大,需要暴露给前端应用;
- 内部协调接口:这类接口仅用于内部系统之间进行业务协作用,一般不暴露给前端;
- 面向第三方接口:这类接口用于与外部系统对接,比如开放平台接口等,需要暴露给第三方的开发者。
这几类接口我们都需要提供完备的文档供调用者进行对接(一人全栈开发模式可以滑走了),但是对于不同类的阅读者,把所有接口都一并暴露出来不合适,有时也会触犯保密规定。那么,我们可以利用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=1springdoc: ... 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文档可能会在生产环境中启用,因此,这里的路由配置建议也是手动配置。