前言
2020年7月份Springfox 发布了3.0.0,增加了对Webflux、OpenApi 3的支持,适应Gateway。在微服务系统中,每个业务模块使用swagger管理接口文档,同时,可以使用业务网关聚合管理各微服务的接口文档,本文重点说明Spring Cloud Gateway集成Springfox swagger 3.0,以供参考。
框架版本
-
Spring Boot 2.4.4
-
Spring Cloud 2020.0.2
-
Spring Cloud Alibaba 2021.1 (注册中心和配置中心使用Nacos 2021.1)
-
Spring Cloud Gateway 3.0.2
-
Springfox 3.0.0
注意:这里使用的版本比较新,特别是Spring Cloud 2020和Springfox 3.0.0,这两个都是大版本升级,使用方式和以前存在差异,网上很多教程都是基于以前的版本。
开始整合
微服务端
网关聚合管理是基于微服务的接口实现的,所以在启动网关之前,必须先配置各微服务的接口和swagger。
关于Spring Boot如何整合SpirngFox Swagger 3.0,可以查看另一篇文章:Spring Boot:整合Swagger3.0,这里不做拓展。
引入依赖
<!-- 只需要引入一个依赖即可,2.x版本需要引入两个 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${springfox-boot-starter.version}</version>
</dependency>
添加配置
Springfox 3.0移除了@EnableSwagger2WebMvc和@EnableSwagger2WebFlux等注解,统一使用@EnableOpenApi
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({OpenApiDocumentationConfiguration.class})
public @interface EnableOpenApi {
}
实现统一配置文件,下面做了自定义配置的封装,重点是SwaggerAutoConfiguration#api方法:
@Data
@ConfigurationProperties("swagger")
public class SwaggerProperties {
/**
* 是否开启swagger
*/
private Boolean enabled;
/**
* 分组名
**/
private String groupName = "default";
/**
* swagger会解析的包路径
**/
private String basePackage = "";
/**
* swagger会解析的url规则
**/
private List<String> basePath = new ArrayList<>();
/**
* 在basePath基础上需要排除的url规则
**/
private List<String> excludePath = new ArrayList<>();
/**
* 标题
**/
private String title = "";
/**
* 描述
**/
private String description = "";
/**
* 版本
**/
private String version = "";
/**
* 许可证
**/
private String license = "";
/**
* 许可证URL
**/
private String licenseUrl = "";
/**
* 服务条款URL
**/
private String termsOfServiceUrl = "";
/**
* host信息
**/
private String host = "";
/**
* 联系人信息
*/
private Contact contact = new Contact();
@Data
@NoArgsConstructor
public static class Contact {
/**
* 联系人
**/
private String name = "";
/**
* 联系人url
**/
private String url = "";
/**
* 联系人email
**/
private String email = "";
}
}
@EnableOpenApi
@Configuration
@ConditionalOnProperty(name = "swagger.enabled", matchIfMissing = true)
public class SwaggerAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SwaggerProperties swaggerProperties() {
return new SwaggerProperties();
}
/**
* 配置swagger2的一些基本的内容,比如扫描的包等等
*
* @return Docket
*/
@Bean
public Docket api(SwaggerProperties swaggerProperties) {
// base-package处理
if (swaggerProperties.getBasePackage().isEmpty()) {
swaggerProperties.setBasePackage(SwaggerConstants.BASE_PACKAGE);
}
// base-path处理
if (swaggerProperties.getBasePath().isEmpty()) {
swaggerProperties.getBasePath().add(SwaggerConstants.DEFAULT_BASE_PATH);
}
// noinspection unchecked
List<Predicate<String>> basePath = new ArrayList<>();
swaggerProperties.getBasePath().forEach(path -> basePath.add(PathSelectors.ant(path)));
// exclude-path处理
if (swaggerProperties.getExcludePath().isEmpty()) {
swaggerProperties.getExcludePath().addAll(SwaggerConstants.DEFAULT_EXCLUDE_PATH);
}
List<Predicate<String>> excludePath = new ArrayList<>();
swaggerProperties.getExcludePath().forEach(path -> excludePath.add(PathSelectors.ant(path)));
ApiSelectorBuilder builder = new Docket(DocumentationType.SWAGGER_2)
.enable(swaggerProperties.getEnabled())
.host(swaggerProperties.getHost())
.apiInfo(apiInfo(swaggerProperties))
.select()
//此包路径下的类,才生成接口文档
.apis(RequestHandlerSelectors.basePackage(swaggerProperties.getBasePackage()))
//加了Api注解的类,才生成接口文档
.apis(RequestHandlerSelectors.withClassAnnotation(Api.class));
swaggerProperties.getBasePath().forEach(p -> builder.paths(PathSelectors.ant(p)));
swaggerProperties.getExcludePath().forEach(p -> builder.paths(PathSelectors.ant(p).negate()));
return builder
.build()
.securitySchemes(securitySchemes())
.securityContexts(securityContexts())
.pathMapping("/");
}
/**
* 安全模式,这里指定token通过Authorization头请求头传递
*/
private List<SecurityScheme> securitySchemes() {
SecurityScheme scheme = new ApiKey(SwaggerConstants.ACCESS_TOKEN_KEY, SwaggerConstants.ACCESS_TOKEN_KEY, "header");
return Collections.singletonList(scheme);
}
/**
* 配置默认的全局鉴权策略的开关,通过正则表达式进行匹配;默认匹配所有URL
*/
private List<SecurityContext> securityContexts() {
SecurityContext context = SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex("^(?!auth).*$"))
.build();
return Collections.singletonList(context);
}
/**
* 默认的全局鉴权策略
*
* @return
*/
private List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
return Collections.singletonList(
new SecurityReference(SwaggerConstants.ACCESS_TOKEN_KEY, authorizationScopes));
}
/**
* api文档的详细信息函数,注意这里的注解引用的是哪个
*
* @return
*/
private ApiInfo apiInfo(SwaggerProperties swaggerProperties) {
return new ApiInfoBuilder()
.title(swaggerProperties.getTitle())
.description(swaggerProperties.getDescription())
.license(swaggerProperties.getLicense())
.licenseUrl(swaggerProperties.getLicenseUrl())
.termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl())
.contact(new Contact(swaggerProperties.getContact().getName(), swaggerProperties.getContact().getUrl(), swaggerProperties.getContact().getEmail()))
.version(swaggerProperties.getVersion())
.build();
}
}
然后只需要在启动程序添加@EnableOpenApi即可,访问地址:http://ip:port/swagger-ui/index.html
网关端
引入依赖
<!--gateway 网关依赖,内置webflux 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${springfox-boot-starter.version}</version>
</dependency>
... 其他依赖,如Nacos注册中心
添加gateway路由配置
spring:
application:
# 应用名称
name: spring2go-gateway
profiles:
# 环境配置
active: dev
cloud:
#nacos
nacos:
discovery:
# 服务注册地址
server-addr: 127.0.0.1:8848
config:
# 配置中心地址
server-addr: ${spring.cloud.nacos.discovery.server-addr}
# 配置文件格式
file-extension: yml
gateway:
enabled: true
# @link:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-loadbalancerclient-filter
# 当service实例不存在时默认返回503,显示配置返回404
loadbalancer:
use404: true
# 根据注册中心自动配置routes
# @link :https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-discoveryclient-route-definition-locator
# 效果:/serviceId/path1/args ==> /path1/args
# discovery:
# locator:
# lowerCaseServiceId: true
# enabled: true
routes:
# 系统模块
- id: spring2go-upms
uri: lb://spring2go-upms
predicates:
- Path=/upms/**
filters:
# - SwaggerHeaderFilter
- StripPrefix=1
由于我们需要聚合管理各微服务的接口,所以我们可以通过自定义SwaggerProvider获取Api-doc(即SwaggerResources)来获取各服务的接口。
注意:Springfox 3.0开始支持webflux,所以想管理网关自身的接口,只需要在官网启动程序添加@EnableOpenApi即可。
//标记primary,让网关默认使用自定义provider
@Primary
@Component
public class SwaggerProvider implements SwaggerResourcesProvider {
/**
* Swagger2默认的url后缀
*/
public static final String SWAGGER2URL = "/v2/api-docs";
/**
* 网关路由
*/
@Autowired
private RouteLocator routeLocator;
@Autowired
private GatewayProperties gatewayProperties;
/**
* 聚合其他服务接口
*
* @return
*/
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routes = new ArrayList<>();
//取出gateway的route
routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
//结合配置的route-路径(Path),和route过滤,只获取有效的route节点
gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId()))
.forEach(routeDefinition -> routeDefinition.getPredicates().stream()
.filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
.forEach(predicateDefinition -> resources.add(swaggerResource(routeDefinition.getId(),
predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
.replace("/**", SWAGGER2URL)))));
return resources;
}
private SwaggerResource swaggerResource(String name, String url) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(url);
swaggerResource.setSwaggerVersion("2.0");
return swaggerResource;
}
}
注意:这里有个坑,swagger的文档类型必须使用SWAGGER_2(即:/v2/api-docs),后面会讲解原因。
然后启动微服务,启动网关服务,访问地址:http://ip:port/swagger-ui/index.html即可看到聚合的接口。
常见问题
设置X-Forwarded-Prefix
如果网关路由为upms/test/{a},在swagger会显示为test/{a,缺少了upms这个路由节点。断点源码时发现在Swagger中会根据X-Forwarded-Prefix这个Header来获取BasePath,将它添加至接口路径与host中间,这样才能正常做接口测试,而Gateway在做转发的时候并没有这个Header添加进Request,所以发生接口调试的404错误。解决思路是在Gateway里加一个过滤器来添加这个header。
注意:Spring Boot2.0.6版本Spring将给我们添加上了这个Header,不再需要人为添加,如果重复添加将导致baseurl出现两个路由节点名称
@Component
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {
private static final String HEADER_NAME = "X-Forwarded-Prefix";
private static final String SWAGGER_URI = "/v2/api-docs";
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if (!StringUtils.endsWithIgnoreCase(path, SWAGGER_URI)) {
return chain.filter(exchange);
}
String basePath = path.substring(0, path.lastIndexOf(SWAGGER_URI));
ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
return chain.filter(newExchange);
};
}
}
Swagger调试返回404
Springfox升级3.0以后,提供了新的文档类型,DocumentationType.OAS_30,如果使用OAS_30,将指导BaseURL默认不加上节点名称,例如:[ Base URL: localhost:9000/upms ] ==> [ Base URL: localhost:9000 ],在这种情况下,如果网关路由使用了StripPrefix过滤器,将导致路由命中失败,我认为这是springfox的一个bug。