前言:
在微服务系统中,通常每个服务都会暴露其接口文档,在前端人员或测试人员查看的时候,并不是那么方便,我们需要告诉相关人员每个服务的文档地址,由于swagger/knif4j(knif4j为更易用的swagger封装,点此了解knif4j)支持以rest的方式获取所有微服务的文档数据,再配合Gateway的路由,我们可以轻松的通过其相关API做到聚合文档的效果,做到只提供一个文档地址,便可查看所有微服务下的接口文档
注意:
-
各微服务的knife4j配置及文档访问,观阅此文前默认认为你已经是完成且可访问的,如未完成或遇到问题等,参考knif4j官网按照文档进行操作,或在评论区留言。
-
如果想方便简单的接入,可直接参考、套用我的代码,之前写过一篇文章为Nacos结合Gateway实现动态路由,本文示例代码仍基于其仓库进行编写,仓库地址:springcloud-nacos-example tree:knif4j
点个关注,来个star,甚是感激 😃 -
以下操作全程在Gateway模块,各微服务模块如果Docket的Group类型(API分组,比如openApi,adminApi,feignApi等)定义统一,则无需改变
一、依赖
Gateway聚合文档服务,需要knif4j的依赖:
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
本例使用的版本为2.0.9,其他版本的请自行结合并尝试
二、配置
1).自定义配置文件
在resources下新建:swagger-faced.yml
(也可放在配置中心,按个人所需)
此配置文件也可以用.properties代替,不过为了其内容具有结构性,此处采用了yaml配置
配置各API分组下,分别有哪些服务需要暴露:
swagger:
faced:
open:
groupName: 'OPEN前端'
groupUrl: 'group=openApi'
applications:
- server1-application
- server2-application
admin:
groupName: 'ADMIN后台'
groupUrl: 'group=adminApi'
applications:
- server1-application
- server2-application
微服务中配置的Docket的Group,需跟此处swagger.faced.**
的key一致,
例子中两个分组:open,admin,那么微服务中也同样有两个分组(或其中之一,详情可看示例代码)
属性解释:
groupName: 定义了API分组的前缀名称,后面代码中使用“【】”括了起来使其更直观
groupUrl: 可以理解为访问doc需要的kv参数,此参数将被拼接到v2/api-docs?
(swagger文档数据获取接口,GET类型)后进行使用
applications: 属性(list)配置了各分组下有哪些服务需要暴露,可以按需合理的进行暴露
2).SwaggerFacedBundleConfig配置(属性配置):
SwaggerFacedBundleConfig
将自定义的yaml配置注入到spring环境中,以便在swaggerResourceProvider中访问配置:
@Configuration
class SwaggerFacedBundleConfig {
private val swaggerFacedBundleName = "swagger-faced.yml"
@Bean
fun swaggerFacedProperties(): PropertySourcesPlaceholderConfigurer? {
val configurer = PropertySourcesPlaceholderConfigurer()
val yaml = YamlPropertiesFactoryBean()
yaml.setResources(ClassPathResource(swaggerFacedBundleName))
configurer.setProperties(yaml.getObject()!!)
return configurer
}
@Bean
@ConfigurationProperties(prefix = "swagger.faced.open")
fun open() = SwaggerFacedItem()
@Bean
@ConfigurationProperties(prefix = "swagger.faced.admin")
fun admin() = SwaggerFacedItem()
}
open class SwaggerFacedItem {
open lateinit var groupName: String
open lateinit var groupUrl: String
open lateinit var applications: List<String>
}
3).swaggerResourceProvider配置(实际聚合配置):
此配置类利用Gateway的路由列表以及上述配置,对各微服务API文档的resource聚合进行提供
@Component
@Primary
class SwaggerResourceConfig : SwaggerResourcesProvider {
val log = LoggerFactory.getLogger(SwaggerResourceConfig::class.java)
@Autowired
lateinit var swaggerFaced: List<SwaggerFacedItem>
override fun get(): List<SwaggerResource> {
val resources = mutableListOf<SwaggerResource>()
NacosRouteDefinitionRepository.routeDefinitions.stream().forEach { route: RouteDefinition? ->
route!!.predicates.stream()
.filter { predicateDefinition: PredicateDefinition ->
"Path".equals(
predicateDefinition.name,
ignoreCase = true
)
}
.forEach { predicateDefinition: PredicateDefinition ->
addResource(
resources,
route,
predicateDefinition
)
}
}
return resources.sortedBy { it.name }
}
private fun addResource(
resources: MutableList<SwaggerResource>,
route: RouteDefinition?,
predicateDefinition: PredicateDefinition
) {
for (facedItem in swaggerFaced) {
facedItem.applications.filter { route!!.id.equals(it, ignoreCase = true) }.forEach { _ ->
resources.add(
swaggerResource(
"【${facedItem.groupName}】" + route!!.id,//中括号更直观
predicateDefinition.args[NAME_PREFIX]!!
.replace("**", SWAGGER_DOC_PATH + facedItem.groupUrl)
)
)
}
}
}
private fun swaggerResource(name: String, location: String): SwaggerResource {
log.debug("name:{},location:{}", name, location)
return SwaggerResource().apply {
this.name = name
this.location = location
this.swaggerVersion = "2.0"
}
}
companion object {
private const val SWAGGER_DOC_PATH = "v2/api-docs?"
private const val NAME_PREFIX = "pattern"
}
}
其中NacosRouteDefinitionRepository.routeDefinitions
为当前的provider提供了各微服务的路由信息列表,也可以根据项目实际情况替换为其他的微服务路由信息列表
三、定义资源访问控制器
暴露聚合文档的资源访问接口,此控制器的接口,在聚合文档的页面打开时被调用
/**
* 自定义Swagger的各个配置节点
* Created by macro on 2020/7/9.
*/
@RestController
class SwaggerHandler {
@Autowired
lateinit var swaggerResources: SwaggerResourcesProvider
@Autowired(required = false)
lateinit var securityConfiguration: SecurityConfiguration
@Autowired(required = false)
lateinit var uiConfiguration: UiConfiguration
/**
* Swagger安全配置,支持oauth和apiKey设置
*/
@GetMapping("/swagger-resources/configuration/security")
fun securityConfiguration(): Mono<ResponseEntity<SecurityConfiguration>> {
return Mono.just(
ResponseEntity(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()),
HttpStatus.OK
)
)
}
/**
* Swagger UI配置
*/
@GetMapping("/swagger-resources/configuration/ui")
fun uiConfiguration(): Mono<ResponseEntity<UiConfiguration>> {
return Mono.just(
ResponseEntity(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK
)
)
}
/**
* Swagger资源配置,微服务中这各个服务的api-docs信息
*/
@GetMapping("/swagger-resources")
fun swaggerResources(): Mono<ResponseEntity<*>> {
return Mono.just(ResponseEntity(swaggerResources.get(), HttpStatus.OK))
}
}
至此聚合完成,是不是很简单?一个配置文件 两个配置类 一个controller
启动各微服务和gateway服务,可以通过:gateway访问地址/doc.html 来访问已经聚合的文档,本例中访问:http://localhost:2000/doc.html,即可看到聚合后的接口文档
四、效果对比
以我编写的实际例子来对比效果:
gateway的端口为2000,server1-application的端口为4001
未聚合文档前需要这样访问(含有具体微服务的前缀或端口):
http://localhost:2000/server1-application/doc.html
或:
http://localhost:4001/doc.html
效果是这样的:
注意看左上角
聚合文档后,统一访问(无需微服务的前缀或具体端口):
http://localhost:2000/doc.html
无需再访问微服务各自文档接口
另外这个组别(provider中get()方法的返回值:List<SwaggerResource>
)可以自定义排序规则,当前实现默认使用了名称的自然顺序作为排序方式:
resources.sortedBy { it.name }
五、小结
以网关聚合的方式访问文档,对于比较依赖文档的人员,还是非常便捷的
不过比较遗憾的是不支持跨分组搜索,有兴趣的话可以拓展一下搜索相关的源码,这样就更方便了
聚合步骤也还是比较简单的,只需要跟文中操作步骤来就可以实现了
本文示例的代码地址:
springcloud-nacos-example tree:knif4j
欢迎在留言区发表你的看法意见与建议,共同探讨~~