告别手写接口文档!SpringBoot + Swagger3.0自动化方案

16 阅读17分钟

每天5分钟,掌握一个SpringBoot核心知识点。大家好,我是SpringBoot指南的小坏。前三天我们解锁了配置管理的高级玩法,今天来解决每个开发者都头疼的问题——接口文档维护!

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

💥 真实痛点:你的接口文档还在"人肉同步"吗?

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

先看几个让你血压飙升的场景:

场景1:前端:"接口又变了?文档没更新啊!"
场景2:测试:"这个参数是必填还是可选?文档没写清楚..."
场景3:联调:"字段名又改了?说好的文档呢!"
场景4:交接:"这文档是3年前写的,现在代码早改了10遍了..."

如果你也是接口文档的"受害者",那么今天这篇文章就是你的救星!我将带你用SpringDoc OpenAPI 3.0实现真正的文档自动化,让接口文档与代码"同生共死",永不同步!

一、为什么选择SpringDoc OpenAPI 3.0?

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

1.1 Swagger的演进史

graph LR
    A[Swagger 1.2] --> B[Swagger 2.0]
    B --> C[OpenAPI 3.0]
    C --> D[SpringDoc 1.x]
    D --> E[SpringDoc 2.x]
    
    style A fill:#f9f,stroke:#333
    style E fill:#ccf,stroke:#333

传统痛点

<!-- 老旧的SpringFox配置 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version> <!-- 已停止维护! -->
</dependency>

<!-- 问题1:与SpringBoot 3不兼容 -->
<!-- 问题2:配置复杂,注解繁琐 -->
<!-- 问题3:不支持OpenAPI 3.0规范 -->

SpringDoc优势

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.5.0</version> <!-- 持续更新! -->
</dependency>
  • ✅ 支持SpringBoot 3+和Java 17+
  • ✅ 零配置启动,开箱即用
  • ✅ 支持OpenAPI 3.0最新规范
  • ✅ 更好的性能,自动懒加载
  • ✅ 与Spring Security无缝集成

1.2 基础集成:1分钟搞定

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

// 只需要一个依赖,无需任何配置!
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

// 启动后访问:
// http://localhost:8080/swagger-ui.html
// http://localhost:8080/v3/api-docs

是的,你没看错!零配置就能生成这样的文档页面:

OpenAPI文档包含:
 所有Controller接口
 请求参数说明
 响应数据结构
 在线测试功能
 一键导出多种格式

二、核心注解详解:从基础到高级

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

2.1 控制器层注解

@RestController
@RequestMapping("/api/users")
@Tag(name = "用户管理", description = "用户相关的所有操作")
public class UserController {
    
    @Operation(
        summary = "创建用户",
        description = "创建一个新用户,需要管理员权限",
        tags = {"用户管理"}
    )
    @ApiResponses({
        @ApiResponse(
            responseCode = "201",
            description = "用户创建成功",
            content = @Content(
                mediaType = "application/json",
                schema = @Schema(implementation = UserVO.class)
            )
        ),
        @ApiResponse(
            responseCode = "400",
            description = "参数校验失败",
            content = @Content(
                mediaType = "application/json",
                schema = @Schema(implementation = ErrorResponse.class)
            )
        ),
        @ApiResponse(
            responseCode = "409",
            description = "用户已存在"
        )
    })
    @PostMapping
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<UserVO> createUser(
            @Valid @RequestBody @Parameter(
                description = "用户创建请求",
                required = true
            ) UserCreateRequest request) {
        // 业务逻辑
        return ResponseEntity.status(HttpStatus.CREATED).body(userService.create(request));
    }
    
    @Operation(
        summary = "分页查询用户",
        description = "根据条件分页查询用户列表"
    )
    @Parameters({
        @Parameter(name = "page", description = "页码,从0开始", example = "0"),
        @Parameter(name = "size", description = "每页大小", example = "20"),
        @Parameter(name = "sort", description = "排序字段,格式:field,asc|desc", 
                  example = "createdAt,desc"),
        @Parameter(name = "username", description = "用户名模糊查询"),
        @Parameter(name = "enabled", description = "是否启用")
    })
    @GetMapping
    public Page<UserVO> getUsers(
            @Parameter(hidden = true) @PageableDefault Pageable pageable,
            @RequestParam(required = false) String username,
            @RequestParam(required = false) Boolean enabled) {
        return userService.findByPage(username, enabled, pageable);
    }
    
    @Operation(
        summary = "获取用户详情",
        description = "根据ID获取用户详细信息"
    )
    @GetMapping("/{id}")
    public UserVO getUserById(
            @Parameter(
                description = "用户ID",
                required = true,
                example = "123"
            ) @PathVariable Long id) {
        return userService.findById(id);
    }
    
    @Operation(
        summary = "更新用户",
        description = "更新用户信息"
    )
    @PutMapping("/{id}")
    public UserVO updateUser(
            @Parameter(description = "用户ID") @PathVariable Long id,
            @Valid @RequestBody UserUpdateRequest request) {
        return userService.update(id, request);
    }
    
    @Operation(
        summary = "删除用户",
        description = "根据ID删除用户(逻辑删除)"
    )
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable Long id) {
        userService.delete(id);
    }
}

2.2 DTO/VO对象注解

@Schema(description = "用户创建请求")
@Data
public class UserCreateRequest {
    
    @Schema(
        description = "用户名",
        requiredMode = Schema.RequiredMode.REQUIRED,
        minLength = 3,
        maxLength = 20,
        example = "zhangsan"
    )
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度3-20位")
    private String username;
    
    @Schema(
        description = "密码",
        requiredMode = Schema.RequiredMode.REQUIRED,
        minLength = 6,
        maxLength = 20,
        example = "123456"
    )
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度6-20位")
    private String password;
    
    @Schema(
        description = "邮箱",
        requiredMode = Schema.RequiredMode.REQUIRED,
        example = "zhangsan@example.com"
    )
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @Schema(
        description = "手机号",
        requiredMode = Schema.RequiredMode.REQUIRED,
        pattern = "^1[3-9]\\d{9}$",
        example = "13800138000"
    )
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;
    
    @Schema(
        description = "年龄",
        minimum = "0",
        maximum = "150",
        example = "25"
    )
    @Min(value = 0, message = "年龄不能小于0")
    @Max(value = 150, message = "年龄不能大于150")
    private Integer age;
    
    @Schema(
        description = "角色列表",
        allowableValues = {"ADMIN", "USER", "GUEST"},
        defaultValue = "USER"
    )
    @NotNull(message = "角色不能为空")
    private List<UserRole> roles = Arrays.asList(UserRole.USER);
    
    @Schema(
        description = "扩展信息",
        implementation = Map.class,
        example = "{\"department\": \"研发部\", \"position\": \"工程师\"}"
    )
    private Map<String, Object> extras;
}

@Schema(description = "用户响应对象")
@Data
public class UserVO {
    
    @Schema(description = "用户ID", example = "123")
    private Long id;
    
    @Schema(description = "用户名", example = "zhangsan")
    private String username;
    
    @Schema(description = "邮箱", example = "zhangsan@example.com")
    private String email;
    
    @Schema(description = "手机号", example = "13800138000")
    private String phone;
    
    @Schema(description = "年龄", example = "25")
    private Integer age;
    
    @Schema(
        description = "账户状态", 
        allowableValues = {"ENABLED", "DISABLED", "LOCKED"},
        example = "ENABLED"
    )
    private UserStatus status;
    
    @Schema(description = "创建时间", example = "2024-01-15 10:30:00")
    private LocalDateTime createdAt;
    
    @Schema(description = "更新时间", example = "2024-01-15 10:30:00")
    private LocalDateTime updatedAt;
}

2.3 枚举类型注解

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

@Schema(description = "用户角色")
public enum UserRole {
    
    @Schema(description = "系统管理员", example = "ADMIN")
    ADMIN("系统管理员"),
    
    @Schema(description = "普通用户", example = "USER")
    USER("普通用户"),
    
    @Schema(description = "访客", example = "GUEST")
    GUEST("访客");
    
    private final String description;
    
    UserRole(String description) {
        this.description = description;
    }
    
    public String getDescription() {
        return description;
    }
}

@Schema(description = "用户状态")
public enum UserStatus {
    
    @Schema(description = "已启用")
    ENABLED,
    
    @Schema(description = "已禁用")
    DISABLED,
    
    @Schema(description = "已锁定")
    LOCKED
}

三、高级配置:打造企业级文档系统

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

3.1 完整配置类示例

@Configuration
@OpenAPIDefinition(
    info = @Info(
        title = "用户中心 API",
        version = "1.0.0",
        description = "用户管理系统接口文档",
        contact = @Contact(
            name = "小坏",
            email = "xiaohuai@example.com",
            url = "https://blog.xiaohuai.com"
        ),
        license = @License(
            name = "Apache 2.0",
            url = "https://www.apache.org/licenses/LICENSE-2.0"
        ),
        termsOfService = "https://example.com/terms"
    ),
    externalDocs = @ExternalDocumentation(
        description = "项目Wiki",
        url = "https://github.com/xiaohuai/user-center/wiki"
    ),
    servers = {
        @Server(
            url = "http://localhost:8080",
            description = "开发环境"
        ),
        @Server(
            url = "https://test.example.com",
            description = "测试环境"
        ),
        @Server(
            url = "https://api.example.com",
            description = "生产环境"
        )
    },
    security = @SecurityRequirement(name = "BearerAuth"),
    tags = {
        @Tag(name = "用户管理", description = "用户相关的操作"),
        @Tag(name = "权限管理", description = "角色和权限管理"),
        @Tag(name = "系统管理", description = "系统级别的操作")
    }
)
@SecurityScheme(
    name = "BearerAuth",
    type = SecuritySchemeType.HTTP,
    scheme = "bearer",
    bearerFormat = "JWT",
    description = "JWT认证令牌,格式: Bearer {token}"
)
@SecurityScheme(
    name = "BasicAuth",
    type = SecuritySchemeType.HTTP,
    scheme = "basic",
    description = "基础认证(用户名/密码)"
)
public class OpenApiConfig {
    
    @Bean
    @Profile("dev")  // 只在开发环境开启
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                    .title("用户中心 API - 开发环境")
                    .version("1.0.0")
                    .description("开发环境接口文档,包含所有调试信息"))
                .addSecurityItem(new SecurityRequirement().addList("BearerAuth"))
                .components(new Components()
                    .addSecuritySchemes("BearerAuth", new SecurityScheme()
                        .type(SecurityScheme.Type.HTTP)
                        .scheme("bearer")
                        .bearerFormat("JWT")))
                .externalDocs(new ExternalDocumentation()
                    .description("开发指南")
                    .url("https://dev.example.com/docs"));
    }
    
    @Bean
    @Profile("prod")  // 生产环境配置
    public OpenAPI prodOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                    .title("用户中心 API")
                    .version("1.0.0")
                    .description("生产环境接口文档"))
                .servers(List.of(
                    new Server()
                        .url("https://api.example.com")
                        .description("生产服务器")
                ));
    }
    
    @Bean
    public GroupedOpenApi publicApi() {
        return GroupedOpenApi.builder()
                .group("public")
                .pathsToMatch("/api/public/**")
                .build();
    }
    
    @Bean
    public GroupedOpenApi adminApi() {
        return GroupedOpenApi.builder()
                .group("admin")
                .pathsToMatch("/api/admin/**")
                .addOpenApiCustomizer(openApi -> {
                    openApi.info(new Info()
                        .title("管理端 API")
                        .version("1.0.0")
                        .description("需要管理员权限的接口"));
                })
                .build();
    }
    
    @Bean
    public GroupedOpenApi userApi() {
        return GroupedOpenApi.builder()
                .group("user")
                .pathsToMatch("/api/user/**")
                .pathsToExclude("/api/user/admin/**")
                .build();
    }
}

3.2 配置文件详解

# application.yml
springdoc:
  api-docs:
    path: /api-docs  # OpenAPI JSON路径,默认/v3/api-docs
    enabled: true
  swagger-ui:
    path: /swagger-ui.html  # 文档页面路径
    enabled: true
    operations-sorter: method  # 按HTTP方法排序:method|alpha
    tags-sorter: alpha        # 按标签名排序
    default-models-expand-depth: 2  # 模型展开深度
    doc-expansion: none       # 文档展开方式:none|list|full
    persist-authorization: true  # 保持授权信息
    display-request-duration: true  # 显示请求时长
    filter: true              # 启用搜索过滤
    try-it-out-enabled: true  # 启用"试一试"功能
    # 自定义CSS主题
    # css-path: /css/swagger-custom.css
  show-actuator: false  # 是否显示Actuator端点
  packages-to-scan: com.example.controller  # 指定扫描包
  paths-to-match: /api/**  # 匹配路径
  paths-to-exclude: /api/internal/**  # 排除路径
  
  # 全局参数配置
  default-consumes-media-type: application/json
  default-produces-media-type: application/json
  
  # 模型配置
  model-and-view-allowed: false
  override-with-generic-response: false
  
  # 缓存配置
  cache:
    disabled: false
    ttl: 5m

四、安全集成:保护你的API文档

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

4.1 Spring Security集成

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                // 允许访问API文档相关资源
                .antMatchers(
                    "/swagger-ui.html",
                    "/swagger-ui/**",
                    "/v3/api-docs/**",
                    "/swagger-resources/**",
                    "/webjars/**"
                ).permitAll()
                // 其他API需要认证
                .antMatchers("/api/**").authenticated()
                .anyRequest().permitAll()
            .and()
            .formLogin().disable()
            .httpBasic().disable()
            .csrf().disable()
            // 添加API文档的认证支持
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .oauth2ResourceServer()
                .jwt();
    }
    
    // 配置Swagger UI的Bearer Token输入
    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
            .components(new Components()
                .addSecuritySchemes("BearerAuth", new SecurityScheme()
                    .type(SecurityScheme.Type.HTTP)
                    .scheme("bearer")
                    .bearerFormat("JWT")
                    .in(SecurityScheme.In.HEADER)
                    .name("Authorization")))
            .security(List.of(new SecurityRequirement().addList("BearerAuth")));
    }
}

4.2 基于角色的文档访问控制

@Configuration
@Profile("!prod")  // 非生产环境才启用文档
public class SwaggerSecurityConfig {
    
    @Value("${swagger.username:admin}")
    private String username;
    
    @Value("${swagger.password:admin123}")
    private String password;
    
    @Bean
    public SecurityFilterChain swaggerSecurityFilterChain(HttpSecurity http) throws Exception {
        return http
            .antMatcher("/swagger-ui/**")
            .authorizeHttpRequests(auth -> auth
                .anyRequest().hasRole("SWAGGER_ADMIN")
            )
            .httpBasic(Customizer.withDefaults())
            .csrf().disable()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .build();
    }
    
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withUsername(username)
            .password(passwordEncoder().encode(password))
            .roles("SWAGGER_ADMIN")
            .build();
        return new InMemoryUserDetailsManager(user);
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

五、高级特性:自定义与扩展

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

5.1 自定义Operation参数

@Configuration
public class OpenApiCustomizerConfig {
    
    @Bean
    public OpenApiCustomizer globalHeaderCustomizer() {
        return openApi -> {
            // 添加全局请求头
            openApi.getPaths().values().forEach(pathItem -> {
                pathItem.readOperations().forEach(operation -> {
                    // 添加TraceId请求头
                    operation.addParametersItem(new HeaderParameter()
                        .name("X-Trace-Id")
                        .description("请求追踪ID")
                        .required(false)
                        .schema(new StringSchema()));
                    
                    // 添加语言请求头
                    operation.addParametersItem(new HeaderParameter()
                        .name("Accept-Language")
                        .description("客户端语言")
                        .required(false)
                        .schema(new StringSchema()
                            .addEnumItem("zh-CN")
                            .addEnumItem("en-US")
                            .addEnumItem("ja-JP"))
                        .example("zh-CN"));
                });
            });
            
            // 添加全局响应头
            openApi.getPaths().values().forEach(pathItem -> {
                pathItem.readOperations().forEach(operation -> {
                    operation.getResponses().values().forEach(apiResponse -> {
                        apiResponse.addHeaderObject("X-Request-Id", 
                            new Header()
                                .description("请求ID")
                                .schema(new StringSchema())
                                .example(UUID.randomUUID().toString()));
                    });
                });
            });
        };
    }
    
    @Bean
    public OpenApiCustomizer globalResponseCustomizer() {
        return openApi -> {
            // 添加全局响应
            openApi.getComponents()
                .addSchemas("ApiResponse", new Schema<>()
                    .type("object")
                    .addProperty("code", new IntegerSchema().example(200))
                    .addProperty("message", new StringSchema().example("成功"))
                    .addProperty("data", new Schema<>())
                    .addProperty("timestamp", new IntegerSchema().example(1640995200))
                );
            
            openApi.getComponents()
                .addSchemas("ErrorResponse", new Schema<>()
                    .type("object")
                    .addProperty("code", new IntegerSchema().example(400))
                    .addProperty("message", new StringSchema().example("参数错误"))
                    .addProperty("timestamp", new IntegerSchema().example(1640995200))
                    .addProperty("path", new StringSchema().example("/api/users"))
                    .addProperty("detail", new StringSchema().example("用户名不能为空"))
                );
        };
    }
}

5.2 自动生成DTO示例数据

@Component
public class ExampleDataCustomizer {
    
    @Bean
    public OpenApiCustomizer exampleDataCustomizer() {
        return openApi -> {
            openApi.getComponents().getSchemas().forEach((name, schema) -> {
                // 为每个Schema设置示例数据
                if (schema.getExample() == null) {
                    Object example = generateExample(schema);
                    if (example != null) {
                        schema.setExample(example);
                    }
                }
            });
        };
    }
    
    private Object generateExample(Schema<?> schema) {
        if (schema instanceof ObjectSchema) {
            Map<String, Object> example = new HashMap<>();
            
            // 根据属性类型生成示例数据
            if (schema.getProperties() != null) {
                schema.getProperties().forEach((propName, propSchema) -> {
                    example.put(propName, generatePropertyExample(propSchema));
                });
            }
            
            return example;
        } else if (schema instanceof StringSchema) {
            // 根据可能的枚举值生成
            if (schema.getEnum() != null && !schema.getEnum().isEmpty()) {
                return schema.getEnum().get(0);
            }
            return "example-string";
        } else if (schema instanceof IntegerSchema) {
            return 123;
        } else if (schema instanceof NumberSchema) {
            return 123.45;
        } else if (schema instanceof BooleanSchema) {
            return true;
        } else if (schema instanceof ArraySchema) {
            List<Object> list = new ArrayList<>();
            Schema<?> items = ((ArraySchema) schema).getItems();
            list.add(generatePropertyExample(items));
            return list;
        }
        
        return null;
    }
    
    private Object generatePropertyExample(Schema<?> schema) {
        // 简单类型示例生成
        if (schema.getType() == null) return null;
        
        switch (schema.getType()) {
            case "string":
                if (schema.getFormat() != null) {
                    switch (schema.getFormat()) {
                        case "date": return "2024-01-15";
                        case "date-time": return "2024-01-15T10:30:00Z";
                        case "email": return "user@example.com";
                        case "phone": return "13800138000";
                        case "uuid": return "550e8400-e29b-41d4-a716-446655440000";
                    }
                }
                return "string-example";
            case "integer":
                if (schema.getFormat() != null && schema.getFormat().equals("int64")) {
                    return 1234567890123L;
                }
                return 123;
            case "number":
                return 123.45;
            case "boolean":
                return true;
            default:
                return null;
        }
    }
}

六、代码生成:自动生成客户端SDK

6.1 使用OpenAPI Generator

# openapi-generator-config.yaml
generatorName: java
inputSpec: http://localhost:8080/v3/api-docs
outputDir: generated-sdk
apiPackage: com.example.api
modelPackage: com.example.model
invokerPackage: com.example.invoker
groupId: com.example
artifactId: user-service-sdk
artifactVersion: 1.0.0

# 配置选项
configOptions:
  dateLibrary: java8
  java8: true
  useBeanValidation: true
  serializableModel: true
  serializationLibrary: jackson
  library: resttemplate
  
# 模板自定义
templateDir: custom-templates
<!-- pom.xml插件配置 -->
<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>6.6.0</version>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>${project.basedir}/src/main/resources/api/openapi.yaml</inputSpec>
                <generatorName>java</generatorName>
                <configOptions>
                    <sourceFolder>src/gen/java/main</sourceFolder>
                    <dateLibrary>java8</dateLibrary>
                    <java8>true</java8>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>

6.2 生成多语言客户端

# 生成Java客户端
npx @openapitools/openapi-generator-cli generate \
  -i http://localhost:8080/v3/api-docs \
  -g java \
  -o ./generated/java-client

# 生成TypeScript客户端
npx @openapitools/openapi-generator-cli generate \
  -i http://localhost:8080/v3/api-docs \
  -g typescript-axios \
  -o ./generated/typescript-client

# 生成Python客户端
npx @openapitools/openapi-generator-cli generate \
  -i http://localhost:8080/v3/api-docs \
  -g python \
  -o ./generated/python-client

# 生成Go客户端
npx @openapitools/openapi-generator-cli generate \
  -i http://localhost:8080/v3/api-docs \
  -g go \
  -o ./generated/go-client

七、文档导出与分享

7.1 导出为HTML/PDF

@RestController
@RequestMapping("/api/docs")
public class DocExportController {
    
    @Autowired
    private OpenAPI openAPI;
    
    /**
     * 导出为HTML文档
     */
    @GetMapping("/export/html")
    public void exportAsHtml(HttpServletResponse response) throws IOException {
        String html = generateHtml(openAPI);
        
        response.setContentType("text/html");
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-Disposition", 
            "attachment; filename=\"api-documentation.html\"");
        
        response.getWriter().write(html);
    }
    
    /**
     * 导出为Markdown文档
     */
    @GetMapping("/export/markdown")
    public void exportAsMarkdown(HttpServletResponse response) throws IOException {
        String markdown = generateMarkdown(openAPI);
        
        response.setContentType("text/markdown");
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-Disposition", 
            "attachment; filename=\"API_DOCUMENTATION.md\"");
        
        response.getWriter().write(markdown);
    }
    
    /**
     * 导出为OpenAPI YAML文件
     */
    @GetMapping("/export/openapi.yaml")
    public void exportAsYaml(HttpServletResponse response) throws IOException {
        Yaml yaml = new Yaml();
        String yamlStr = yaml.dump(convertToMap(openAPI));
        
        response.setContentType("application/x-yaml");
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-Disposition", 
            "attachment; filename=\"openapi.yaml\"");
        
        response.getWriter().write(yamlStr);
    }
    
    private String generateHtml(OpenAPI openAPI) {
        // 使用模板引擎生成HTML
        return """
            <!DOCTYPE html>
            <html>
            <head>
                <title>API文档 - %s</title>
                <style>
                    body { font-family: Arial, sans-serif; margin: 40px; }
                    .endpoint { border: 1px solid #ddd; padding: 20px; margin-bottom: 20px; }
                    .method { font-weight: bold; color: #007bff; }
                    .path { font-family: monospace; }
                </style>
            </head>
            <body>
                <h1>%s</h1>
                <p>%s</p>
                <!-- 动态生成API文档 -->
            </body>
            </html>
            """.formatted(
                openAPI.getInfo().getTitle(),
                openAPI.getInfo().getTitle(),
                openAPI.getInfo().getDescription()
            );
    }
    
    private String generateMarkdown(OpenAPI openAPI) {
        StringBuilder markdown = new StringBuilder();
        
        markdown.append("# ").append(openAPI.getInfo().getTitle()).append("\n\n");
        markdown.append(openAPI.getInfo().getDescription()).append("\n\n");
        
        if (openAPI.getServers() != null) {
            markdown.append("## 服务器\n\n");
            openAPI.getServers().forEach(server -> {
                markdown.append("- ").append(server.getUrl());
                if (server.getDescription() != null) {
                    markdown.append(" - ").append(server.getDescription());
                }
                markdown.append("\n");
            });
            markdown.append("\n");
        }
        
        // 继续生成各个接口的文档...
        return markdown.toString();
    }
}

7.2 集成到CI/CD流水线

# .gitlab-ci.yml 或 Jenkinsfile
stages:
  - build
  - test
  - deploy
  - docs

generate-docs:
  stage: docs
  image: openjdk:17
  script:
    # 1. 启动应用
    - nohup java -jar target/app.jar &
    - sleep 30  # 等待应用启动
    
    # 2. 获取OpenAPI定义
    - curl -s http://localhost:8080/v3/api-docs > openapi.json
    
    # 3. 生成HTML文档
    - npx @redocly/cli build-docs openapi.json -o api-docs.html
    
    # 4. 生成客户端SDK
    - npx @openapitools/openapi-generator-cli generate -i openapi.json -g java -o sdk/java
    - npx @openapitools/openapi-generator-cli generate -i openapi.json -g typescript-axios -o sdk/typescript
    
    # 5. 上传到文档服务器
    - scp api-docs.html user@docs-server:/var/www/html/
    - scp -r sdk/ user@docs-server:/var/www/sdk/
  artifacts:
    paths:
      - openapi.json
      - api-docs.html
      - sdk/

八、最佳实践与常见问题

8.1 最佳实践总结

// 1. 统一响应格式
@Schema(description = "统一API响应")
@Data
public class ApiResponse<T> {
    @Schema(description = "状态码", example = "200")
    private Integer code;
    
    @Schema(description = "消息", example = "成功")
    private String message;
    
    @Schema(description = "数据")
    private T data;
    
    @Schema(description = "时间戳", example = "1640995200000")
    private Long timestamp;
    
    @Schema(description = "请求ID", example = "550e8400-e29b-41d4-a716-446655440000")
    private String requestId;
}

// 2. 统一异常响应
@Schema(description = "统一错误响应")
@Data
public class ErrorResponse {
    @Schema(description = "错误码", example = "400")
    private Integer code;
    
    @Schema(description = "错误消息", example = "参数验证失败")
    private String message;
    
    @Schema(description = "时间戳", example = "1640995200000")
    private Long timestamp;
    
    @Schema(description = "请求路径", example = "/api/users")
    private String path;
    
    @Schema(description = "错误详情")
    private Object detail;
}

// 3. 分页响应包装
@Schema(description = "分页响应")
@Data
public class PageResponse<T> {
    @Schema(description = "数据列表")
    private List<T> content;
    
    @Schema(description = "总条数", example = "100")
    private Long totalElements;
    
    @Schema(description = "总页数", example = "10")
    private Integer totalPages;
    
    @Schema(description = "当前页码", example = "0")
    private Integer pageNumber;
    
    @Schema(description = "每页大小", example = "10")
    private Integer pageSize;
    
    @Schema(description = "是否第一页", example = "true")
    private Boolean first;
    
    @Schema(description = "是否最后一页", example = "false")
    private Boolean last;
}

8.2 常见问题解决

问题1:枚举类型显示为字符串

// ❌ 错误方式
public enum Status {
    ACTIVE, INACTIVE
}

// ✅ 正确方式
@Schema(description = "状态枚举")
public enum Status {
    @Schema(description = "活跃")
    ACTIVE("活跃"),
    
    @Schema(description = "未活跃")
    INACTIVE("未活跃");
    
    private final String description;
    
    Status(String description) {
        this.description = description;
    }
}

问题2:泛型类型信息丢失

// ❌ 泛型信息丢失
public ApiResponse<User> getUser() { ... }

// ✅ 使用@Schema注解指定类型
@Operation(responses = @ApiResponse(
    content = @Content(schema = @Schema(implementation = ApiResponse.class))
))
public ApiResponse<User> getUser() { ... }

// 或者使用包装类
public class UserResponse extends ApiResponse<User> { ... }
public UserResponse getUser() { ... }

问题3:循环引用导致StackOverflow

// ❌ 循环引用
@Data
public class User {
    private List<Order> orders;
}

@Data
public class Order {
    private User user;  // 循环引用!
}

// ✅ 使用DTO打破循环
@Data
public class UserDTO {
    private List<OrderDTO> orders;
}

@Data
public class OrderDTO {
    private Long userId;  // 只存ID,不存完整对象
}

// 或者在@Schema注解中排除
@Data
public class User {
    @Schema(hidden = true)  // 隐藏这个字段
    private List<Order> orders;
}

九、实战:电商系统接口文档完整示例

9.1 订单模块文档

@RestController
@RequestMapping("/api/orders")
@Tag(name = "订单管理", description = "订单相关接口")
public class OrderController {
    
    @Operation(
        summary = "创建订单",
        description = "用户下单,创建新订单",
        requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
            description = "订单创建请求",
            required = true,
            content = @Content(
                mediaType = "application/json",
                schema = @Schema(implementation = OrderCreateRequest.class),
                examples = @ExampleObject(
                    name = "普通商品订单",
                    value = """
                        {
                          "userId": 123,
                          "items": [
                            {
                              "productId": 1,
                              "quantity": 2,
                              "price": 99.99
                            }
                          ],
                          "shippingAddress": "北京市朝阳区",
                          "remark": "请尽快发货"
                        }
                        """
                )
            )
        )
    )
    @ApiResponses({
        @ApiResponse(
            responseCode = "201",
            description = "订单创建成功",
            headers = @Header(
                name = "Location",
                description = "新订单的URL",
                schema = @Schema(type = "string")
            )
        ),
        @ApiResponse(
            responseCode = "400",
            description = "参数验证失败"
        ),
        @ApiResponse(
            responseCode = "409",
            description = "库存不足"
        )
    })
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public ResponseEntity<OrderVO> createOrder(
            @Valid @RequestBody OrderCreateRequest request,
            @RequestHeader("X-User-Id") Long userId) {
        // 业务逻辑
        OrderVO order = orderService.createOrder(request, userId);
        return ResponseEntity
            .created(URI.create("/api/orders/" + order.getId()))
            .body(order);
    }
    
    @Operation(
        summary = "查询订单列表",
        description = "分页查询用户订单,支持多种查询条件"
    )
    @Parameters({
        @Parameter(name = "page", description = "页码,从0开始", example = "0"),
        @Parameter(name = "size", description = "每页大小", example = "20"),
        @Parameter(name = "status", description = "订单状态",
                  schema = @Schema(implementation = OrderStatus.class)),
        @Parameter(name = "startTime", description = "开始时间,格式:yyyy-MM-dd HH:mm:ss",
                  example = "2024-01-01 00:00:00"),
        @Parameter(name = "endTime", description = "结束时间,格式:yyyy-MM-dd HH:mm:ss",
                  example = "2024-01-31 23:59:59")
    })
    @GetMapping
    public PageResponse<OrderVO> listOrders(
            @ParameterObject @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable,
            @RequestParam(required = false) OrderStatus status,
            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime) {
        return orderService.listOrders(pageable, status, startTime, endTime);
    }
    
    @Operation(
        summary = "导出订单",
        description = "导出订单数据为Excel文件"
    )
    @GetMapping("/export")
    public void exportOrders(
            @RequestParam(required = false) OrderStatus status,
            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate,
            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate,
            HttpServletResponse response) throws IOException {
        
        List<OrderVO> orders = orderService.findOrdersForExport(status, startDate, endDate);
        
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-Disposition", 
            "attachment;filename=orders.xlsx");
        
        // 生成Excel文件
        // ...
    }
}

总结

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

今天我们全面掌握了SpringDoc OpenAPI 3.0的强大功能:

  1. 零配置集成:1分钟搞定文档生成
  2. 丰富注解:通过注解生成完整文档
  3. 安全集成:与Spring Security完美结合
  4. 代码生成:自动生成多语言客户端SDK
  5. 导出分享:支持HTML/PDF/Markdown多种格式

核心价值

  • 提高效率:文档与代码同步更新,永不落后
  • 提升质量:接口定义清晰,减少联调问题
  • 规范协作:前后端基于文档进行开发
  • 自动化:集成CI/CD,自动发布文档

资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。