Spring Cloud 项目中使用 Swagger
关于方案的选择
在 Spring Cloud 项目中使用 Swagger 有以下 4 种方式:方式一 :在网关处引入 Swagger ,去聚合各个微服务的 Swagger。未来是访问网关的 Swagger 原生界面。
方式二 :在网关处引入 knife4j,去聚合各个微服务的 Swagger。未来是访问网关的 knife4j 的美化版界面。
方式三 :使用 knife4j 去创建一个独立的聚合项目,去聚合各个微服务的 Swagger。未来是访问这个聚合项目的 knife4j 的美化版界面。
方式四 :使用独立的、另外的第三方工具(例如,Apifox),去聚合各个微服务的 Swagger。未来是访问这个第三方工具。
在上述 4 种方案中,我们选择的是方案四,原因在于:
- 对于
方案一
和方案二
而言,本质上是一样的,主要无非就是页面美不美观的问题。knife4j 的页面要强过 swagger 的原生界面的,但和第三方工具比起来,还是远不如第三方插件好看(和功能丰富)。- knife4j 对原生 Swagger 有所拓展,提升了便捷性,但是提升有效。如果抛开“页面美观”这个主要优点,其它有关“拓展”层面的优点并没有那么多的“非用不可”。
方案三
的 knife4j 的聚合项目虽然能够很方便的集合各个微服务,使用上要方便于网关聚合,但是,你是用它就意味着你的测试请求都绕过了网关,如果你在网管处有代码逻辑,那么这部分代码就“跳”过去了,不利于测试。所以,从美观、完善各方面综合考虑,我们采用上面的
方案四
。使用
方案四
意味着:
- 我们的项目中只需要引入 swagger 的包,不需要引入 knife4j 的包。
- 如果我们不需要在网关处看到原生的 swagger 页面,那么网关项目不需要有任何改动。原生的 swagger 页面,那么网关项目不需要有任何改动。
各个微服务的改动
改动一:引包和配置文件
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
<!--<version>2.0.9</version>-->
</dependency>
# knife4j公共配置
knife4j.enable=true
改动二:新增配置类
@Configuration
@EnableOpenApi
@RequiredArgsConstructor
public class SwaggerConfiguration {
private final OpenApiExtensionResolver openApiExtensionResolver;
@Value("${spring.application.name}")
private String applicationName;
@Bean
@Order(value = 1)
public Docket docDocket() {
return new Docket(DocumentationType.OAS_30)
.pathMapping("/" + applicationName) // ==> /department-service
.enable(true)
.apiInfo(groupApiInfo())
.select()
.apis(RequestHandlerSelectors.withMethodAnnotation(Operation.class))
.paths(PathSelectors.any())
.build()
.extensions(openApiExtensionResolver.buildExtensions("部门微服务"))
;
}
private ApiInfo groupApiInfo() {
return new ApiInfoBuilder()
.title("Knife4j接口文档")
.description("Knife4j接口文档")
.termsOfServiceUrl("https://doc.xiaominfo.com/")
.version("1.0.0")
.build();
}
}
关键项说明
上述内容,无论是 pom 引包,还是加配置类,绝大部分内容都是复制粘贴,无需改动的。
但是在配置类中,有一个信息必须注意,它必须和你的 spring-cloud 的配置相关:
.pathMapping("/" + applicationName)
.pathMapping() 方法的值表示的是:Swagger 对外暴露的测试功能中,在原本的(@RestController)的 URI 之外,额外加上一段什么样的前缀。
为什么会有这样的要求?
未来,无论是在 Apifox 这样的第三方工具中测试,还是在网关处的原生的 Swagger 页面上进行测试,我们的测试请求都是应该发送给网关的,再由网关将请求路由给微服务。
所以,在 Apifox 中,或者是网管处的原生的 Swagger 中,我们发送请求的“前一段”的 IP 和 Port 的组合是网关的 IP 和 Port 。而网关在默认情况下则是根据它所收到的“请求的 URI 中的前一段和微服务的服务名的匹配情况”作为依据来路由请求。
如果,各个微服务的 Swagger 的配置中没有主动的多“加上”一段能路由到自己的 URI 前缀,那么 Swagger 所暴露出来的请求测试功能所产生的拼接出来的 URI 就成了:网关的 IP 和 Port 拼上微服务的 URI,而没有微服务标识那一段。例如:
## 127.0.0.1:10000 是网关地址
http://127.0.0.1:10000/department/delete
但是,从上帝视角看,本应该是:
## 127.0.0.1:10000 是网关地址
http://127.0.0.1:10000/department-service/department/delete
只有这样,Swagger 所暴露出来的测试功能才能正常使用。
所以,这就需要 department-service 自己在它的 Swagger 配置中说明:在自己的 Swagger 暴露的测试功能中,需要在正常的 URI 前面“多”加上 /department-service
前缀,这样才能让 Swagger 暴露的 URI 经过网关后正常路由到自己这里来。
网关处的改动
注意
如果你不指望在网关处看到、访问原生的 Swagger 界面,那么,这一步操作就不是必须的,不用做。
引入 swagger 包
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
新增配置类
@Primary
@Configuration
@EnableOpenApi
@RequiredArgsConstructor
public class SwaggerConfig implements SwaggerResourcesProvider {
private static final String OAS_30_URL = "/v3/api-docs";
private final RouteLocator routeLocator;
private final GatewayProperties gatewayProperties;
/**
* 网关应用名称
*/
@Value("${spring.application.name}")
private String self;
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routeHosts = new ArrayList<>();
routeLocator.getRoutes()
.filter(route -> route.getUri().getHost() != null)
.filter(route -> Objects.equals(route.getUri().getScheme(), "lb"))
// 过滤掉网关自身的服务 uri 中的 host 就是服务 id
.filter(route -> !self.equalsIgnoreCase(route.getUri().getHost()))
.subscribe(route -> routeHosts.add(route.getUri().getHost()));
// 记录已经添加过的server,存在同一个应用注册了多个服务在注册中心上
Set<String> dealed = new HashSet<>();
routeHosts.forEach(instance -> {
// 拼接 url ,目标 swagger 的 url
String url = "/" + instance.toLowerCase() + OAS_30_URL;
System.out.println("url: " + url);
if (!dealed.contains(url)) {
dealed.add(url);
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setUrl(url);
swaggerResource.setName(instance);
//swaggerResource.setSwaggerVersion("3.0.3");
resources.add(swaggerResource);
}
});
return resources;
}
}