博客 / 詳情

返回

【小知識】springdoc的swagger-config顯示404問題

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文件,但是文件地址返回404404的原因是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頁面時,都會調用SwaggerIndexPageTransformertransform方法,方法返回的Resource對象的內容是頁面的html內容

3.6.1. 首先要獲取從HOSTURI中間這一段Gateway加上的路徑

  1. 通過request.getHeader("Host")獲取到請求的域名是IP:8080,而不是期望中的域名,因為從Gateway轉發到業務服務時,是從註冊中心中取到了IP並且通過IP+端口來轉發。
  2. 通過request.getContextPath()獲取到應用的上下文路徑是空的,因為服務名是Gateway轉發時加上的,項目本身沒有配置。
  3. 通過debug發現,Gateway在轉發請求時,會在header里加上一些x-forwarded-*信息:
  4. 非通過Gateway請求時(直接通過IP訪問服務),header信息如下:
  5. 所以可以通過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} 方案生效。 \)

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.