• 从 JDK 8 到 JDK 17:Swagger 升级迁移指南

随着 Java 生态向 JDK 17 及 Jakarta EE 的演进,许多项目面临从 JDK 8 升级的挑战,其中 Swagger(API 文档工具)的兼容性调整尤为关键。本文将从 技术栈差异、升级迁移步骤、常见问题 等多个维度,解析 JDK 8(SpringFox)向 JDK 17(SpringDoc/Knife4j)的升级路径。

blog-thumb

随着 Java 生态向 JDK 17 及 Jakarta EE 的演进,许多项目面临从 JDK 8 升级的挑战,其中 Swagger(API 文档工具)的兼容性调整尤为关键。本文将从 技术栈差异、升级迁移步骤、常见问题 等多个维度,解析 JDK 8(SpringFox)向 JDK 17(SpringDoc/Knife4j)的升级路径。

1、背景

1.1 技术演进

  • JDK 版本演进:

    JDK 17 是继 JDK 8 后的首个 LTS 版本,支持模块化、Records 等新特性,但移除了部分旧 API(如 javax.servlet)。

    直接影响:基于 JDK 8 构建的 SpringFoxSwagger 2.x)因依赖旧规范无法兼容新版本。

  • Jakarta EE 的崛起:

    Java EE 移交 Eclipse 基金会后更名为 Jakarta EE,包名从 javax.* 改为 jakarta.*

    核心冲突:Spring Boot 3.xSpringDocSwagger 3.x)强制依赖 Jakarta EE 9+,导致旧项目升级时需全局替换包名。

1.2 升级的必要性

  • 安全风险:SpringFox 已停止维护,存在未修复漏洞(如 CVE-2021-28170)。

  • 功能需求:SpringDoc 支持 OpenAPI 3.0 规范,提供更灵活的文档定义和响应示例。

  • 生态兼容:微服务、云原生场景下,JDK 17 的容器化支持更优。

2、技术栈对比与选型

2.1 JDK 8 与 JDK 17 的 Swagger 技术栈对比

特性 JDK 8 + SpringFox (Swagger 2.x) JDK 17 + SpringDoc/Knife4j (Swagger 3.x)
核心框架 SpringFox 2.x(已停止维护) SpringDoc OpenAPI 3.x(官方推荐)
JDK 兼容性 仅支持 JDK 8~11 支持 JDK 17+ 的模块化特性
Spring Boot 支持 Spring Boot 2.x Spring Boot 3.x(兼容 2.7.x
Servlet 规范 基于 javax.servlet 迁移至 jakarta.servletJakarta EE 9+
注解库 io.swagger.annotations io.swagger.v3.oas.annotations
注解风格 @Api, @ApiOperation @Tag, @Operation(更符合 OpenAPI 3.0
依赖管理 需手动管理版本,易冲突 通过 Spring Boot Starter 简化依赖
文档生成 需配置 Docket 自动扫描,通过 OpenAPI Bean 全局配置
文档规范 OpenAPI 2.0 OpenAPI 3.0
UI 工具 Swagger UI(基础功能) Knife4j(增强功能,支持离线文档、权限控制、接口分组等)
维护状态 停止维护(最后版本 3.0.0 活跃维护(SpringDoc 2.x + Knife4j 4.x

2.2 版本兼容性矩阵

技术栈 JDK 8 JDK 11 JDK 17 Spring Boot 2.7.x Spring Boot 3.x
SpringFox 2.x ⚠️ 部分兼容
SpringDoc 1.x
SpringDoc 2.x

3、快速升级步骤

3.1 依赖管理升级

  1. 移除旧依赖

    <!-- 删除 SpringFox 相关依赖 -->
    <dependency>
       <groupId>io.springfox</groupId>
       <artifactId>springfox-swagger2</artifactId>
       <version>3.0.0</version>
    </dependency>
    <dependency>
       <groupId>io.springfox</groupId>
       <artifactId>springfox-swagger-ui</artifactId>
       <version>3.0.0</version>
    </dependency>
    
  2. 添加新依赖

    <!-- SpringDoc 核心 -->
    <dependency>
       <groupId>org.springdoc</groupId>
       <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
       <!-- 最新版本见:https://springdoc.org/ -->
       <version>2.5.0</version>
    </dependency>
    
    <!-- Knife4j 增强 UI(可选) -->
    <dependency>
       <groupId>com.github.xiaoymin</groupId>
       <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
       <!-- 最新版本见:https://doc.xiaominfo.com/ -->
       <version>4.5.0</version>
    </dependency>
    
  3. 排除冲突依赖

    <!-- 检查并排除其他库中的 javax.servlet 依赖 -->
    <dependency>
       <groupId>org.apache.tomcat.embed</groupId>
       <artifactId>tomcat-embed-core</artifactId>
       <exclusions>
          <exclusion>
                <groupId>javax.servlet</groupId>
                <artifactId>javax.servlet-api</artifactId>
          </exclusion>
       </exclusions>
    </dependency>
    

3.2 代码层迁移(注解更新)

控制器注解迁移:

SpringFox (Swagger 2.x) SpringDoc (OpenAPI 3.x) 用途 示例
@Api @Tag 标记控制器类的作用 @Tag(name = "用户管理", description = "用户接口")
@ApiOperation @Operation 描述接口方法的功能 @Operation(summary = "创建用户", description = "根据DTO创建用户")
@ApiParam @Parameter 描述接口参数(路径、查询、表单参数等) @Parameter(name = "id", description = "用户ID", required = true)
@ApiResponse @ApiResponse 定义接口的响应状态码和描述 @ApiResponse(responseCode = "404", description = "用户不存在")
@ApiIgnore @Hidden@Parameter(hidden = true) 隐藏接口或参数 @Hidden // 隐藏整个接口方法
@ApiImplicitParams @Parameters + @Parameter 描述非直接声明的参数(如 Header 参数) @Parameters({ @Parameter(name = "token", in = HEADER, description = "认证令牌") })
@ApiImplicitParam @Parameter 单个隐式参数定义 同上
// JDK 8(SpringFox)
@Api(tags = "用户管理", description = "用户接口")
@RestController
public class UserController {
    @ApiOperation("创建用户")
    @PostMapping("/users")
    public User createUser(@ApiParam("用户DTO") @RequestBody UserDTO dto) {
        // ...
    }

   @ApiImplicitParams({
      @ApiImplicitParam(name = "token", value = "认证令牌", paramType = "header")
   })
   @GetMapping("/profile")
   public UserProfile getProfile() {
      //...
   }
}
// JDK 17(SpringDoc)
@Tag(name = "用户管理", description = "用户接口")
@RestController
public class UserController {
    @Operation(summary = "创建用户", description = "根据DTO创建用户")
    @PostMapping("/users")
    public User createUser(@Parameter(description = "用户DTO", required = true)
        @RequestBody UserDTO dto) {
        // ...
    }

   @Parameters({
      @Parameter(name = "token", description = "认证令牌", in = ParameterIn.HEADER)
   })
   @GetMapping("/profile")
   public UserProfile getProfile() {
      // ...
   }
}

模型类注解迁移:

SpringFox (Swagger 2.x) SpringDoc (OpenAPI 3.x) 用途 示例
@ApiModel @Schema 描述数据模型类 @Schema(name = "UserDTO", description = "用户传输对象")
@ApiModelProperty @Schema 描述模型字段的详细信息 @Schema(description = "用户名", example = "张三", requiredMode = REQUIRED)
// JDK 8(SpringFox)
@ApiModel(value = "User", description = "用户实体")
public class User {
    @ApiModelProperty(value = "用户名", required = true, example = "张三")
    private String name;
}
// JDK 17(SpringDoc)
@Schema(name = "User", description = "用户实体")
public class User {
    @Schema(description = "用户名", example = "张三", requiredMode = Schema.RequiredMode.REQUIRED)
    private String name;
}

3.3 全局配置调整

是否还需要传统 SwaggerConfig?

  • 不需要Knife4j OpenAPI3 基于 SpringDoc,无需配置 DocketSwagger2Markup

  • 必要配置:仅需定义 OpenAPI Bean(如上文的 OpenApiConfig)即可。

3.3.1 SpringDoc 配置类

@Configuration
public class OpenApiConfig {
    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("API 文档")
                        .version("1.0")
                        .description("JDK 17 迁移示例")
                        .contact(new Contact().name("xcbeyond技术支持").email("support@example.com"))
                        .license(new License().name("Apache 2.0").url("https://springdoc.org")))
                .externalDocs(new ExternalDocumentation()
                        .description("详细文档")
                        .url("https://xcbeyond.com"))
                .components(new Components()
                        .addSecuritySchemes("BearerAuth", new SecurityScheme()
                                .type(SecurityScheme.Type.HTTP)
                                .scheme("bearer")
                                .bearerFormat("JWT")));
    }
}

3.3.2 分组配置(多模块场景)

在微服务架构中,API 文档分组配置的核心管理需求是:

  • 模块化展示:将不同业务域(用户/订单/支付)分离展示。

  • 权限隔离:区分公共 API 和管理 API。

  • 版本管理:同时维护 v1 和 v2 接口。

  • 依赖解耦:避免单个文档过大导致加载性能问题。

分组配置参数详解:

配置方法 参数说明 默认值 示例
.group(String group) 分组唯一标识(显示在 UI 中) 必填 .group("用户管理")
.pathsToMatch(String... paths) 路径匹配规则(支持 Ant 风格) 可选 .pathsToMatch("/api/user/**")
.packagesToScan(String... pkgs) 扫描的包路径 可选 .packagesToScan("com.example")
.pathsToExclude(String... paths) 排除的路径 空数组 .pathsToExclude("/internal/**")
.addOpenApiMethodFilter(Predicate) 自定义方法过滤逻辑 见下面示例
.displayName(String name) 显示名称(覆盖 group 的显示) 同 group 值 .displayName("用户模块")
.addOperationCustomizer(...) 自定义操作处理器 见下面示例

通过合理的分组配置,可在 JDK 17 环境下构建清晰、安全、易维护的 API 文档体系,充分发挥 SpringDocKnife4j 的现代化文档能力。

基础分组配置:

import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OpenApiGroupConfig {

    // 用户管理分组
    @Bean
    public GroupedOpenApi userApi() {
        return GroupedOpenApi.builder()
                .group("用户管理")                   // 分组显示名称
                .pathsToMatch("/api/user/**")        // 路径匹配规则
                .packagesToScan("com.example.user") // 包扫描路径
                .build();
    }

    // 订单管理分组
    @Bean
    public GroupedOpenApi orderApi() {
        return GroupedOpenApi.builder()
                .group("订单管理")
                .pathsToMatch("/api/order/**")
                .packagesToScan("com.example.order")
                .build();
    }

    // 分组自定义排序
    @Bean
    public GroupedOpenApi firstGroup() {
      return GroupedOpenApi.builder()
               .group("01-核心接口")
               .order(1)  // 分组排序(数值越小越靠前)
               .pathsToMatch("/core/**")
               .build();
    }

    @Bean
    public GroupedOpenApi secondGroup() {
      return GroupedOpenApi.builder()
               .group("02-辅助接口")
               .order(2)
               .pathsToMatch("/support/**")
               .build();
    }
}

按安全权限分组:

@Bean
public GroupedOpenApi adminApi() {
    return GroupedOpenApi.builder()
            .group("管理员接口")
            .pathsToMatch("/api/admin/**")
            // 只包含带有 @PreAuthorize("hasRole('ADMIN')") 的接口
            .addOpenApiMethodFilter(method -> 
                method.isAnnotationPresent(PreAuthorize.class) && 
                method.getAnnotation(PreAuthorize.class).value().contains("ADMIN")
            )
            .build();
}

多版本 API 分组:

@Bean
public GroupedOpenApi v1Api() {
    return GroupedOpenApi.builder()
            .group("API-v1")
            .pathsToMatch("/api/v1/**")
            .displayName("版本 1.0 (已弃用)")
            .build();
}

@Bean
public GroupedOpenApi v2Api() {
    return GroupedOpenApi.builder()
            .group("API-v2")
            .pathsToMatch("/api/v2/**")
            .displayName("版本 2.0 (最新)")
            .build();
}

第三方接口分组:

@Bean
public GroupedOpenApi paymentApi() {
    return GroupedOpenApi.builder()
            .group("支付网关")
            .pathsToMatch("/payment/**")
            // 排除内部实现类
            .packagesToExclude("com.example.internal.payment") 
            .build();
}

3.4 包名迁移与模块化适配

3.4.1 全局替换包名

  • IDE 操作

    使用 IntelliJ/Eclipse 的全局替换功能(Ctrl+Shift+R),将以下包名替换:

    • javax.servletjakarta.servlet

    • javax.validationjakarta.validation

    • javax.persistencejakarta.persistence

  • Maven 插件辅助

    使用 maven-replacer-plugin 自动化替换:

    <plugin>
        <groupId>com.google.code.maven-replacer-plugin</groupId>
        <artifactId>replacer</artifactId>
        <version>1.5.3</version>
        <executions>
            <execution>
                <phase>process-sources</phase>
                <goals><goal>replace</goal></goals>
            </execution>
        </executions>
        <configuration>
            <includes>**/*.java</includes>
            <replacements>
                <replacement>
                    <token>javax.servlet</token>
                    <value>jakarta.servlet</value>
                </replacement>
            </replacements>
        </configuration>
    </plugin>
    

3.4.2 模块化配置(JDK 17+)

// src/main/java/module-info.java
open module com.example.api {
    requires spring.boot;
    requires spring.boot.autoconfigure;
    requires spring.web;
    requires springdoc.openapi.common;
    requires com.fasterxml.jackson.databind;
    exports com.example.api.controller;
    exports com.example.api.model;
}

4、迁移后的验证与优化

4.1 验证访问

启动应用后,访问以下地址:

Knife4j UI 文档:http://localhost:8080/doc.html

OpenAPI JSON:http://localhost:8080/v3/api-docs

4.2 文档增强

响应示例

@Operation(summary = "创建用户")
@ApiResponses({
    @ApiResponse(
        responseCode = "201",
        content = @Content(
            mediaType = "application/json",
            schema = @Schema(implementation = User.class),
            examples = @ExampleObject(
                name = "successExample",
                value = """
                    {
                      "id": 1,
                      "name": "张三"
                    }
                    """
            )
        )
    ),
    @ApiResponse(
        responseCode = "400",
        content = @Content(
            examples = @ExampleObject(
                name = "errorExample",
                value = """
                    {
                      "code": "INVALID_REQUEST",
                      "message": "用户名不能为空"
                    }
                    """
            )
        )
    )
})
public ResponseEntity<User> createUser(@RequestBody User user) { ... }

离线文档导出:

Knife4j 导出:访问 http://localhost:8080/doc.html#/home,点击“下载 Markdown”或“下载 OpenAPI JSON”。

4.3 性能与安全优化

  • 生产环境禁用 UI

     springdoc:
        swagger-ui:
           enabled: false  # 禁用 UI
        api-docs:
           enabled: true   # 保留 JSON 生成(供内部系统使用)
    
  • 启用 OAuth2 支持

     @Bean
     SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
           .authorizeHttpRequests(auth -> auth
                 .requestMatchers("/v3/api-docs/**").hasRole("DEVOPS")
                 .anyRequest().authenticated()
           )
           .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
        return http.build();
     }
    

5、常见问题与解决方案

5.1. 错误:Type javax.servlet.http.HttpServletRequest not present

错误日志:java.lang.TypeNotPresentException: Type javax.servlet.http.HttpServletRequest not present

原因:未迁移到 Jakarta EE 包名。

解决步骤

  1. 检查是否遗漏包名替换(使用 IDE 全局搜索 javax.servlet),更新依赖至 Jakarta 版本。
  2. 全局替换代码中的 javax.servletjakarta.servlet
  3. 运行 mvn dependency:tree | grep javax.servlet 确认无冲突依赖。
  4. 更新第三方库至 Jakarta 兼容版本(如 Hibernate 6.x、Tomcat 10.x)。

5.2. Knife4j 访问 /doc.html 报 404

原因:静态资源被拦截或未正确映射。

解决步骤

  1. 确认使用的是 Knife4j OpenAPI3 的 Spring Boot Starter(knife4j-openapi3-jakarta-spring-boot-starter),而非旧版 Knife4jSpringFox

  2. 检查静态资源路径(若自定义了 WebMvcConfigurer):

    Knife4j 的静态资源默认位于 classpath:/META-INF/resources/webjars/knife4j-openapi3-ui/,需确保资源未被拦截或覆盖。

    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
       @Override
       public void addResourceHandlers(ResourceHandlerRegistry registry) {
          // 添加 Knife4j 的静态资源映射
          registry.addResourceHandler("/doc.html")
                   .addResourceLocations("classpath:/META-INF/resources/");
          registry.addResourceHandler("/webjars/**")
                   .addResourceLocations("classpath:/META-INF/resources/webjars/");
       }
    }
    
  3. 检查 Spring Security 配置是否放行 /doc.html/webjars/**

    如果项目集成了 Spring Security,需放行 Knife4j 的静态资源和 API 文档接口。

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.web.SecurityFilterChain;
    
    @Configuration
    public class SecurityConfig {
    
       @Bean
       public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
          http
                .authorizeHttpRequests(auth -> auth
                   // 放行 Knife4j 相关路径
                   .requestMatchers(
                      "/doc.html",
                      "/webjars/**",
                      "/v3/api-docs/**",
                      "/favicon.ico"
                   ).permitAll()
                   .anyRequest().authenticated()
                )
                .csrf(csrf -> csrf.disable()); // 如果不需要 CSRF 防护
          return http.build();
       }
    }
    

5.3 注解不生效或文档无内容

检查项

  1. 确保控制器添加 @Tag,方法添加 @Operation

  2. 模型类字段未标注 @Schema

  3. 包扫描路径未覆盖(通过 @ComponentScanspringdoc.packagesToScan 配置)。

  4. 排除旧版 Swagger 依赖冲突:

    mvn dependency:tree -Dincludes=io.springfox
    

6、总结

从 JDK 8 迁移到 JDK 17 不仅是版本的升级,更是技术栈向现代 Java 生态的过渡。通过 依赖替换、注解迁移、包名调整、模块化适配 四步核心操作,可高效完成 Swagger 升级。Knife4j 和 SpringDoc 的组合,不仅解决了兼容性问题,还提供了更强大的 API 文档管理能力。升级后,建议通过自动化测试和持续监控,确保系统的稳定性和可维护性。