1. 问题
项目环境
jdk:21
springboot:3.2.3
springcloud:2023.0.0
springdoc-openapi-starter-webmvc-ui:2.5.0
项目引入了springdoc,本地开发测试时,http://localhost:8080/swagger-ui/index.html
页面也能正常打开;发布到测试环境之后,通过网关(SpringCloud Gateway
)访问页面http://xxx.com/SERVICENAME/swagger-ui/index.html
,却无法打开。
2. 排查
通过F12
可以发现,是因为页面请求了swagger-config
文件,但是文件地址返回404
;404
的原因是index.html
里请求的地址是http://xxx.com/v3/api-docs/swagger-config
,而不是http://xxx.com/SERVICENAME/v3/api-docs/swagger-config
。
3. 解决方案
3.1. 方案1(成功,但是作为公共包不合适)
通过官方文档发现可以通过修改配置,自定义路径:
springdoc:
swagger-ui:
configUrl: /SERVICENAME/v3/api-docs/swagger-config
配置完之后/swagger-ui/index.html
请求swagger-config
返回正常,但是页面还是无法打开,原因是页面又访问了/v3/api-docs
,并且返回了404,原因跟swagger-config
是一样的。
一样可以通过配置修改路径:
springdoc:
api-docs:
path: /SERVICENAME/v3/api-docs
可以看到configUrl
的值其实就是在configUrl
的值后面再加上/swagger-config
,通过源码org.springdoc.core.utils.Constants
也可以证实:
所以只需配置path
即可,configUrl
可以不用配置。
如果以一个项目的角度来看,问题是解决了。但是在开发的是一个平台公共包,SERVICENAME
是未知的(不同的项目的值是不一样的),所以公共包里无法硬编码;而如果让每个项目引入公共包之后,在自己的项目上加上配置的话,增加了引入成本,并且请求走gateway时,路径上可能除了SERVICENAME
之外,还会加上其他的内容。
\( \color{red} 所以需要尝试看是否还有其他通用的方案。 \)
3.2. 方案2(失败)
参考:404 error with spring doc open api 2.1.0
在项目里手动引入最新的webjars-locator
:
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator</artifactId>
<version>0.52</version>
</dependency>
\( \color{red} 没有生效。 \)
3.3. 方案3(失败)
参考:Spring Boot 中访问 Swagger UI 报 404 not found 错误
在项目里添加addResourceHandler
:
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/swagger-ui/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/swagger-ui/5.13.0/");
super.addResourceHandlers(registry);
}
}
其中swagger-ui
后面的版本号,取自己依赖的webjars
版本:
\( \color{red} 没有生效。 \)
3.4. 方案4(理论可行,但是没有尝试)
参考:springboot 搭配filebeat springboot 搭配 springdoc
在SpringCloud Gateway
里添加转发规则:
\( \color{red} 考虑到要去调整网关,影响比较大,并且网关不应该去关注下游系统具体使用的组件,所以没做尝试。 \)
3.5. 方案5(失败)
参考:configUrl cache issue when using Swagger-UI
添加以下代码:
@Bean
public SwaggerIndexPageTransformer computeConfigUrlSwagger(SwaggerUiConfigProperties swaggerUiConfig,
SwaggerUiOAuthProperties swaggerUiOAuthProperties, SwaggerUiConfigParameters swaggerUiConfigParameters,
SwaggerWelcomeCommon swaggerWelcomeCommon, ObjectMapperProvider objectMapperProvider) {
return new SwaggerIndexPageTransformer(swaggerUiConfig, swaggerUiOAuthProperties, swaggerUiConfigParameters,
swaggerWelcomeCommon, objectMapperProvider) {
@Override
public Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain transformerChain)
throws IOException {
this.swaggerUiConfigParameters.setConfigUrl(null);
return super.transform(request, resource, transformerChain);
}
};
}
\( \color{red} 没有生效。 \) 从参考的文档上其他的人也反馈没有生效。
3.6. 方案6(成功)
通过方案5
,知道可以在代码里动态调整路径:每次打开/swagger-ui/index.html
页面时,都会调用SwaggerIndexPageTransformer
的transform
方法,方法返回的Resource
对象的内容是页面的html内容
。
3.6.1. 首先要获取从HOST
到URI
中间这一段Gateway
加上的路径
- 通过
request.getHeader("Host")
获取到请求的域名是IP:8080
,而不是期望中的域名
,因为从Gateway
转发到业务服务时,是从注册中心中取到了IP并且通过IP+端口来转发。 - 通过
request.getContextPath()
获取到应用的上下文路径是空的
,因为服务名是Gateway
转发时加上的,项目本身没有配置。 - 通过debug发现,
Gateway
在转发请求时,会在header
里加上一些x-forwarded-*
信息: - 非通过
Gateway
请求时(直接通过IP访问服务),header
信息如下: - 所以可以通过
x-forwarded-*
信息获取到Gateway
添加的前缀,并且还可以判断当次请求是否走了Gateway
。
3.6.2.
实现代码:
@Configuration
@ConditionalOnClass(SpringDocWebMvcConfiguration.class)
public class SwaggerConfiguration {
@Bean
public SwaggerIndexTransformer indexPageTransformer(SwaggerUiConfigProperties swaggerUiConfig,
SwaggerUiOAuthProperties swaggerUiOAuthProperties, SwaggerUiConfigParameters swaggerUiConfigParameters,
SwaggerWelcomeCommon swaggerWelcomeCommon, ObjectMapperProvider objectMapperProvider,
SpringDocConfigProperties springDocConfigProperties) {
return new RewritePathSwaggerIndexPageTransformer(swaggerUiConfig, swaggerUiOAuthProperties,
swaggerUiConfigParameters, swaggerWelcomeCommon, objectMapperProvider, springDocConfigProperties);
}
/**
* 重写swagger地址,通过浏览器访问/swagger-ui/index.html时,html页面会请求/v3/api-docs和/v3/api-docs/swagger-config
* 1)本地(localhost)访问时,能正常返回
* 2)测试环境&正式环境通过网关访问时,会出现404,因为访问的地址少了服务名:
* 比如应该是“http://host:port/SERVICE-NAME/v3/api-docs”,但实际访问的是“http://host:port//v3/api-docs”
*
* @author
* @date 2024/6/19 15:11
*/
public class RewritePathSwaggerIndexPageTransformer extends SwaggerIndexPageTransformer {
private SpringDocConfigProperties springDocConfigProperties;
private SwaggerUiConfigParameters swaggerUiConfigParameters;
/**
* 上一次请求获取到的前缀
*/
private String lastPrefix = Strings.EMPTY;
public RewritePathSwaggerIndexPageTransformer(SwaggerUiConfigProperties swaggerUiConfig,
SwaggerUiOAuthProperties swaggerUiOAuthProperties, SwaggerUiConfigParameters swaggerUiConfigParameters,
SwaggerWelcomeCommon swaggerWelcomeCommon, ObjectMapperProvider objectMapperProvider,
SpringDocConfigProperties springDocConfigProperties) {
super(swaggerUiConfig, swaggerUiOAuthProperties, swaggerUiConfigParameters, swaggerWelcomeCommon,
objectMapperProvider);
this.springDocConfigProperties = springDocConfigProperties;
this.swaggerUiConfigParameters = swaggerUiConfigParameters;
}
@Override
public Resource transform(HttpServletRequest request, Resource resource,
ResourceTransformerChain transformerChain) throws IOException {
ApiDocs apiDocs = springDocConfigProperties.getApiDocs();
String apiDocsPath = apiDocs.getPath();
String configUrl = swaggerUiConfigParameters.getConfigUrl();
String prefix = getPrefix(request);
// 考虑同一个服务,可能有时候会被“走网关访问”,有时候会被“走IP+端口直接访问”,所以这里做了区分判断
// 通过网关转发
if (StringUtils.isNotBlank(prefix)) {
// 上一次不是通过网关转发,即需要重新调整;如果上一次是通过网关转发,则不能调整,否则就叠加两次了
if (!StringUtils.equals(lastPrefix, prefix)) {
String newApiDocsPath = prefix + apiDocsPath;
apiDocs.setPath(newApiDocsPath);
String newConfigUrl = RegExUtils.replaceFirst(configUrl, apiDocsPath, newApiDocsPath);
swaggerUiConfigParameters.setConfigUrl(newConfigUrl);
lastPrefix = prefix;
}
} else {
// 不是通过网关转发
// 上一次是通过网关转发,即需要重新调整
if (!StringUtils.equals(lastPrefix, prefix)) {
// 去掉转发时,网关添加的路径
String newApiDocsPath = RegExUtils.replaceFirst(apiDocsPath, lastPrefix, "");
apiDocs.setPath(newApiDocsPath);
String newConfigUrl = RegExUtils.replaceFirst(configUrl, apiDocsPath, newApiDocsPath);
swaggerUiConfigParameters.setConfigUrl(newConfigUrl);
lastPrefix = prefix;
}
}
return super.transform(request, resource, transformerChain);
}
/**
* 获取网关转发时,url上添加的前缀
*
* @param request
* @return java.lang.String
* @author
* @date 2024/6/19 15:27
*/
public String getPrefix(HttpServletRequest request) {
// 获取网关转发的服务名
return Optional.ofNullable(request.getHeader("x-forwarded-prefix")).orElse(Strings.EMPTY);
}
}
}
\( \color{red} 方案生效。 \)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。