Spring Boot API 文档与 OpenAPI 集成最佳实践
Spring Boot API 文档与 OpenAPI 集成最佳实践
引言
API 文档是现代软件开发中不可或缺的一部分,它不仅帮助前端开发者理解如何调用后端接口,也是团队协作和维护的重要参考。Spring Boot 提供了丰富的工具来自动生成 API 文档,其中最流行的是基于 OpenAPI 规范的 Swagger UI。本文将深入探讨如何在 Spring Boot 项目中集成 OpenAPI,创建高质量的 API 文档。
一、OpenAPI 与 Swagger 概述
1.1 OpenAPI 规范
OpenAPI 规范(前身为 Swagger 规范)是一个用于描述 RESTful API 的标准化格式。它允许开发者:
- 定义 API 的结构和行为
- 自动生成客户端 SDK
- 生成交互式文档
- 进行 API 测试和验证
1.2 Swagger UI
Swagger UI 是一个基于 OpenAPI 规范的交互式文档工具,提供:
- 可视化的 API 文档展示
- 在线接口测试功能
- 请求/响应示例展示
- 支持多种认证方式
二、SpringDoc OpenAPI 集成
2.1 添加依赖
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.3.0</version> </dependency>2.2 基础配置
springdoc: api-docs: path: /api-docs enabled: true swagger-ui: path: /swagger-ui.html enabled: true tags-sorter: alpha operations-sorter: alpha info: title: Order Service API description: 订单服务 RESTful API 文档 version: 1.0.0 contact: name: API Support email: support@example.com2.3 配置类
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; import io.swagger.v3.oas.models.servers.Server; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.List; @Configuration public class OpenApiConfig { @Value("${server.port:8080}") private String serverPort; @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() .title("订单服务 API") .version("1.0.0") .description("订单服务 RESTful API 文档,提供订单创建、查询、更新和删除功能") .contact(new Contact() .name("技术支持") .email("support@example.com") .url("https://example.com")) .license(new License() .name("Apache 2.0") .url("https://www.apache.org/licenses/LICENSE-2.0"))) .servers(List.of( new Server().url("http://localhost:" + serverPort).description("本地开发环境"), new Server().url("http://staging.example.com").description("预生产环境"), new Server().url("https://api.example.com").description("生产环境") )); } }三、API 文档注解详解
3.1 控制器级别注解
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/orders") @Tag(name = "订单管理", description = "订单的 CRUD 操作接口") public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService = orderService; } }3.2 方法级别注解
@Operation( summary = "创建订单", description = "根据订单请求创建新订单,返回创建的订单详情", tags = {"订单管理"}, security = {@SecurityRequirement(name = "bearerAuth")} ) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "订单创建成功", content = @Content(schema = @Schema(implementation = OrderResponse.class))), @ApiResponse(responseCode = "400", description = "请求参数无效"), @ApiResponse(responseCode = "401", description = "未授权访问"), @ApiResponse(responseCode = "500", description = "服务器内部错误") }) @PostMapping public ResponseEntity<OrderResponse> createOrder( @Valid @RequestBody OrderRequest request) { OrderResponse response = orderService.createOrder(request); return ResponseEntity.status(HttpStatus.CREATED).body(response); }3.3 参数级别注解
@Operation(summary = "查询订单详情", description = "根据订单ID查询订单详细信息") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "查询成功"), @ApiResponse(responseCode = "404", description = "订单不存在") }) @GetMapping("/{orderId}") public ResponseEntity<OrderResponse> getOrderById( @Parameter(description = "订单ID", required = true, example = "12345") @PathVariable Long orderId) { OrderResponse response = orderService.getOrderById(orderId); return ResponseEntity.ok(response); }3.4 分页查询示例
@Operation(summary = "分页查询订单列表", description = "根据条件分页查询订单列表") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "查询成功") }) @GetMapping public ResponseEntity<PageResponse<OrderResponse>> listOrders( @Parameter(description = "页码(从0开始)", example = "0") @RequestParam(defaultValue = "0") int page, @Parameter(description = "每页大小", example = "10") @RequestParam(defaultValue = "10") int size, @Parameter(description = "订单状态", example = "PENDING") @RequestParam(required = false) String status, @Parameter(description = "客户ID", example = "cust-001") @RequestParam(required = false) String customerId) { Page<OrderResponse> orders = orderService.listOrders(page, size, status, customerId); return ResponseEntity.ok(PageResponse.from(orders)); }四、数据模型定义
4.1 请求模型
import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; import javax.validation.constraints.Size; import java.math.BigDecimal; import java.util.List; @Data @Schema(description = "订单创建请求") public class OrderRequest { @NotBlank(message = "客户ID不能为空") @Size(max = 50, message = "客户ID长度不能超过50") @Schema(description = "客户ID", example = "cust-001", requiredMode = Schema.RequiredMode.REQUIRED) private String customerId; @NotNull(message = "订单金额不能为空") @Positive(message = "订单金额必须大于0") @Schema(description = "订单金额", example = "99.99", requiredMode = Schema.RequiredMode.REQUIRED) private BigDecimal amount; @Schema(description = "订单备注", example = "加急订单") private String remark; @Size(max = 100, message = "商品数量不能超过100") @Schema(description = "订单项列表") private List<OrderItemRequest> items; @Data @Schema(description = "订单项请求") public static class OrderItemRequest { @NotBlank(message = "商品ID不能为空") @Schema(description = "商品ID", example = "prod-001", requiredMode = Schema.RequiredMode.REQUIRED) private String productId; @NotNull(message = "数量不能为空") @Positive(message = "数量必须大于0") @Schema(description = "数量", example = "2", requiredMode = Schema.RequiredMode.REQUIRED) private Integer quantity; @NotNull(message = "单价不能为空") @Positive(message = "单价必须大于0") @Schema(description = "单价", example = "49.99", requiredMode = Schema.RequiredMode.REQUIRED) private BigDecimal unitPrice; } }4.2 响应模型
import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; @Data @Schema(description = "订单响应") public class OrderResponse { @Schema(description = "订单ID", example = "ord-12345") private String id; @Schema(description = "客户ID", example = "cust-001") private String customerId; @Schema(description = "订单金额", example = "99.99") private BigDecimal amount; @Schema(description = "订单状态", example = "PENDING", allowableValues = {"PENDING", "PAID", "SHIPPED", "COMPLETED", "CANCELLED"}) private String status; @Schema(description = "订单备注", example = "加急订单") private String remark; @Schema(description = "创建时间") private LocalDateTime createdAt; @Schema(description = "更新时间") private LocalDateTime updatedAt; @Schema(description = "订单项列表") private List<OrderItemResponse> items; @Data @Schema(description = "订单项响应") public static class OrderItemResponse { @Schema(description = "订单项ID", example = "item-001") private String id; @Schema(description = "商品ID", example = "prod-001") private String productId; @Schema(description = "商品名称", example = "iPhone 15 Pro") private String productName; @Schema(description = "数量", example = "2") private Integer quantity; @Schema(description = "单价", example = "49.99") private BigDecimal unitPrice; @Schema(description = "小计", example = "99.98") private BigDecimal subtotal; } }4.3 分页响应模型
import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.springframework.data.domain.Page; import java.util.List; import java.util.function.Function; @Data @Schema(description = "分页响应") public class PageResponse<T> { @Schema(description = "数据列表") private List<T> content; @Schema(description = "当前页码", example = "0") private int page; @Schema(description = "每页大小", example = "10") private int size; @Schema(description = "总元素数", example = "100") private long totalElements; @Schema(description = "总页数", example = "10") private int totalPages; @Schema(description = "是否为第一页", example = "true") private boolean first; @Schema(description = "是否为最后一页", example = "false") private boolean last; public static <T, R> PageResponse<R> from(Page<T> page, Function<T, R> converter) { PageResponse<R> response = new PageResponse<>(); response.setContent(page.getContent().stream().map(converter).toList()); response.setPage(page.getNumber()); response.setSize(page.getSize()); response.setTotalElements(page.getTotalElements()); response.setTotalPages(page.getTotalPages()); response.setFirst(page.isFirst()); response.setLast(page.isLast()); return response; } public static <T> PageResponse<T> from(Page<T> page) { return from(page, t -> t); } }五、安全认证配置
5.1 Bearer Token 认证
import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class OpenApiSecurityConfig { @Bean public OpenAPI customOpenAPI() { final String securitySchemeName = "bearerAuth"; return new OpenAPI() .addSecurityItem(new SecurityRequirement().addList(securitySchemeName)) .components(new Components() .addSecuritySchemes(securitySchemeName, new SecurityScheme() .name(securitySchemeName) .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT"))); } }5.2 API Key 认证
@Configuration public class OpenApiSecurityConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .addSecurityItem(new SecurityRequirement().addList("apiKey")) .components(new Components() .addSecuritySchemes("apiKey", new SecurityScheme() .name("X-API-Key") .type(SecurityScheme.Type.APIKEY) .in(SecurityScheme.In.HEADER))); } }5.3 多认证方式
@Configuration public class OpenApiSecurityConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .addSecurityItem(new SecurityRequirement().addList("bearerAuth")) .addSecurityItem(new SecurityRequirement().addList("apiKey")) .components(new Components() .addSecuritySchemes("bearerAuth", new SecurityScheme() .name("bearerAuth") .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT")) .addSecuritySchemes("apiKey", new SecurityScheme() .name("X-API-Key") .type(SecurityScheme.Type.APIKEY) .in(SecurityScheme.In.HEADER))); } }六、高级配置
6.1 全局响应示例
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.examples.Example; import io.swagger.v3.oas.models.media.Content; import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.responses.ApiResponse; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class OpenApiResponseConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .components(new Components() .addResponses("BadRequest", createErrorResponse("400", "请求参数无效")) .addResponses("Unauthorized", createErrorResponse("401", "未授权访问")) .addResponses("Forbidden", createErrorResponse("403", "禁止访问")) .addResponses("NotFound", createErrorResponse("404", "资源不存在")) .addResponses("InternalServerError", createErrorResponse("500", "服务器内部错误"))); } private ApiResponse createErrorResponse(String code, String message) { Example example = new Example(); example.setValue("{\"code\": \"" + code + "\", \"message\": \"" + message + "\"}"); MediaType mediaType = new MediaType(); mediaType.addExamples("application/json", example); Content content = new Content(); content.addMediaType("application/json", mediaType); return new ApiResponse() .description(message) .content(content); } }6.2 自定义过滤器
import org.springdoc.core.customizers.OpenApiCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class OpenApiCustomConfig { @Bean public OpenApiCustomizer customOpenApiCustomizer() { return openApi -> { // 自定义操作 openApi.getPaths().values().forEach(pathItem -> pathItem.readOperations().forEach(operation -> { // 添加统一的响应头 operation.getResponses().values().forEach(response -> { if (response.getHeaders() == null) { response.headers(new HashMap<>()); } response.getHeaders().put("X-Request-Id", new Header().description("请求ID")); }); }) ); }; } }6.3 隐藏敏感接口
import io.swagger.v3.oas.annotations.Hidden; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/internal") @Hidden public class InternalController { @GetMapping("/health") public String health() { return "OK"; } }七、API 文档生成
7.1 生成 JSON/YAML 格式文档
# 访问 API 文档 JSON 格式 curl http://localhost:8080/api-docs # 访问 API 文档 YAML 格式 curl http://localhost:8080/api-docs.yaml7.2 生成静态文档
添加依赖:
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-api</artifactId> <version>2.3.0</version> </dependency>配置 Maven 插件:
<plugin> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-maven-plugin</artifactId> <version>1.3</version> <executions> <execution> <id>generate-docs</id> <goals> <goal>generate</goal> </goals> <configuration> <apiDocsUrl>http://localhost:8080/api-docs</apiDocsUrl> <outputFileName>openapi.json</outputFileName> <outputDir>${project.build.directory}</outputDir> </configuration> </execution> </executions> </plugin>7.3 生成客户端 SDK
使用 OpenAPI Generator 生成客户端代码:
# 安装 OpenAPI Generator npm install @openapitools/openapi-generator-cli -g # 生成 Java 客户端 openapi-generator-cli generate \ -i http://localhost:8080/api-docs \ -g java \ -o ./client \ --api-package com.example.api \ --model-package com.example.model \ --group-id com.example \ --artifact-id order-client \ --artifact-version 1.0.0八、测试集成
8.1 使用 MockMvc 测试 API
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc class OrderControllerTest { @Autowired private MockMvc mockMvc; @Test void testCreateOrder() throws Exception { String requestBody = """ { "customerId": "cust-001", "amount": 99.99, "items": [ {"productId": "prod-001", "quantity": 2, "unitPrice": 49.99} ] } """; mockMvc.perform(post("/api/orders") .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.customerId").value("cust-001")) .andExpect(jsonPath("$.amount").value(99.99)); } }九、最佳实践
9.1 文档组织策略
- 按功能模块分组:使用
@Tag注解将相关接口分组 - 统一命名规范:保持 API 路径和参数命名风格一致
- 提供示例数据:为每个参数和响应提供真实的示例值
- 版本控制:使用 API 版本号(如
/api/v1/orders)
9.2 文档维护
- 代码即文档:通过注解定义文档,避免文档与代码脱节
- 自动化测试:确保 API 文档与实际实现一致
- 定期审查:定期检查和更新文档内容
9.3 性能优化
- 延迟加载:对于大型 API,使用延迟加载减少初始加载时间
- 缓存策略:缓存 API 文档,减少重复生成开销
- 按需加载:根据用户权限只展示可访问的接口
结语
SpringDoc OpenAPI 为 Spring Boot 项目提供了强大的 API 文档生成能力。通过合理使用注解和配置,可以创建高质量的交互式 API 文档,提升团队协作效率和 API 可维护性。在实际项目中,应结合安全认证、版本控制和自动化测试,构建完整的 API 文档体系。
