SpringDoc OpenAPI:自动化接口文档与契约开发

使用 SpringDoc OpenAPI 自动生成 RESTful API 文档,掌握注解驱动、分组配置、离线导出与 Mock 服务的完整实践

在现代前后端分离的开发模式下,清晰、准确、实时同步的 API 文档是团队协作的基础。SpringDoc OpenAPI 作为 Spring Boot 生态中 Swagger 的继任者,提供了更简洁的注解体系和更好的 Spring Boot 3 兼容性。

一、SpringDoc vs SpringFox

特性SpringDoc OpenAPISpringFox Swagger
Spring Boot 3 / Jakarta EE原生支持不支持(已停止维护)
注解数量精简(复用 JSR-303)较多
OpenAPI 规范3.0 / 3.12.0
维护状态活跃更新已停止维护
方案推荐新项目首选建议迁移

二、基础集成

2.1 Maven 依赖

<dependencies>
    <!-- SpringDoc OpenAPI -->
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>2.3.0</version>
    </dependency>
</dependencies>

2.2 全局配置

@Configuration
public class OpenApiConfig {
    
    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
            .info(new Info()
                .title("电商订单服务 API")
                .version("v2.1.0")
                .description("提供订单创建、查询、支付等核心能力")
                .contact(new Contact()
                    .name("技术团队")
                    .email("tech@example.com")
                    .url("https://docs.example.com"))
                .license(new License()
                    .name("Apache 2.0")
                    .url("https://www.apache.org/licenses/LICENSE-2.0")))
            .externalDocs(new ExternalDocumentation()
                .description("详细设计文档")
                .url("https://wiki.example.com"))
            .addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
            .components(new Components()
                .addSecuritySchemes("bearerAuth", new SecurityScheme()
                    .type(SecurityScheme.Type.HTTP)
                    .scheme("bearer")
                    .bearerFormat("JWT")));
    }
}

2.3 分组配置

@Configuration
public class OpenApiGroupConfig {
    
    @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/**")
            .addOpenApiMethodFilter(method -> method.isAnnotationPresent(AdminAuth.class))
            .build();
    }
    
    @Bean
    public GroupedOpenApi internalApi() {
        return GroupedOpenApi.builder()
            .group("internal")
            .pathsToMatch("/api/internal/**")
            .addOpenApiCustomiser(openApi -> 
                openApi.info(new Info().title("内部服务 API").version("1.0")))
            .build();
    }
}

三、注解详解

3.1 接口层注解

@Tag(name = "订单管理", description = "订单创建、查询、取消等操作")
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    @Operation(
        summary = "创建订单",
        description = "根据购物车商品创建订单,支持优惠券和积分抵扣",
        tags = {"订单管理"}
    )
    @ApiResponses({
        @ApiResponse(responseCode = "201", description = "创建成功",
            content = @Content(schema = @Schema(implementation = OrderResponse.class))),
        @ApiResponse(responseCode = "400", description = "参数校验失败",
            content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
        @ApiResponse(responseCode = "409", description = "库存不足",
            content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
    })
    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(
        @Valid @RequestBody 
        @io.swagger.v3.oas.annotations.parameters.RequestBody(
            description = "订单创建请求",
            required = true,
            content = @Content(schema = @Schema(implementation = OrderCreateRequest.class))
        )
        OrderCreateRequest request
    ) {
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(orderService.create(request));
    }
    
    @Operation(summary = "查询订单详情")
    @Parameter(name = "orderId", description = "订单编号", required = true, example = "ORD-20240814-001")
    @GetMapping("/{orderId}")
    public OrderDetail getOrder(@PathVariable String orderId) {
        return orderService.findDetail(orderId);
    }
    
    @Operation(summary = "分页查询订单列表")
    @GetMapping
    public PageResult<OrderSummary> listOrders(
        @Parameter(description = "页码", example = "0") @RequestParam(defaultValue = "0") int page,
        @Parameter(description = "每页大小", example = "20") @RequestParam(defaultValue = "20") int size,
        @Parameter(description = "状态筛选") @RequestParam(required = false) OrderStatus status
    ) {
        return orderService.findPage(page, size, status);
    }
}

3.2 模型注解

@Schema(description = "订单创建请求")
public class OrderCreateRequest {
    
    @Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "10086")
    @NotNull
    private Long userId;
    
    @Schema(description = "收货地址ID", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull
    private Long addressId;
    
    @Schema(description = "商品列表", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotEmpty
    @Valid
    private List<OrderItemRequest> items;
    
    @Schema(description = "优惠券码", example = "SUMMER2024")
    private String couponCode;
    
    @Schema(description = "使用积分", minimum = "0", example = "500")
    @Min(0)
    private Integer usePoints;
    
    @Schema(description = "订单备注", maxLength = 500, example = "请发顺丰")
    @Size(max = 500)
    private String remark;
    
    @Schema(description = "订单来源", allowableValues = {"APP", "WEB", "MINI_PROGRAM"})
    private OrderSource source;
    
    // getters/setters
}

@Schema(description = "订单创建响应")
public class OrderResponse {
    
    @Schema(description = "订单编号", example = "ORD-20240814-001")
    private String orderId;
    
    @Schema(description = "订单状态", example = "PENDING_PAYMENT")
    private OrderStatus status;
    
    @Schema(description = "应付金额", example = "299.99")
    private BigDecimal amount;
    
    @Schema(description = "支付超时时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime expireTime;
    
    @Schema(description = "支付链接")
    private String paymentUrl;
}

@Schema(description = "统一错误响应")
public class ErrorResponse {
    
    @Schema(description = "错误码", example = "ORDER_STOCK_INSUFFICIENT")
    private String code;
    
    @Schema(description = "错误信息", example = "商品库存不足")
    private String message;
    
    @Schema(description = "错误详情")
    private List<FieldError> errors;
    
    @Schema(description = "时间戳")
    private Instant timestamp;
}

3.3 枚举映射

@Schema(description = "订单状态")
public enum OrderStatus {
    @Schema(description = "待付款")
    PENDING_PAYMENT,
    
    @Schema(description = "已付款")
    PAID,
    
    @Schema(description = "已发货")
    SHIPPED,
    
    @Schema(description = "已完成")
    COMPLETED,
    
    @Schema(description = "已取消")
    CANCELLED;
}

四、高级功能

4.1 离线文档导出

<build>
    <plugins>
        <plugin>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-maven-plugin</artifactId>
            <version>1.4</version>
            <executions>
                <execution>
                    <id>integration-test</id>
                    <goals><goal>generate</goal></goals>
                </execution>
            </executions>
            <configuration>
                <apiDocsUrl>http://localhost:8080/v3/api-docs</apiDocsUrl>
                <outputFileName>openapi.json</outputFileName>
                <outputDir>${project.build.directory}</outputDir>
            </configuration>
        </plugin>
    </plugins>
</build>

4.2 运行时隐藏字段

public class User {
    
    @Schema(description = "用户ID")
    private Long id;
    
    @Schema(description = "用户名")
    private String username;
    
    @Schema(description = "密码", accessMode = Schema.AccessMode.WRITE_ONLY)
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private String password;
    
    @Schema(description = "内部状态", hidden = true)
    private Integer internalStatus;
}

4.3 自定义扩展属性

@Schema(description = "API 版本信息",
    extensions = {
        @Extension(name = "x-internal", properties = @ExtensionProperty(name = "team", value = "order")),
        @Extension(name = "x-deprecated-since", properties = @ExtensionProperty(name = "version", value = "2.0"))
    })
public class ApiVersionInfo {
    // ...
}

五、测试集成

5.1 Mock Mvc 测试

@WebMvcTest(OrderController.class)
@AutoConfigureRestDocs
class OrderControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    void createOrder() throws Exception {
        OrderCreateRequest request = new OrderCreateRequest();
        request.setUserId(10086L);
        // ... set other fields
        
        mockMvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content(JsonUtils.toJson(request)))
            .andExpect(status().isCreated())
            .andDo(document("order-create",
                requestFields(
                    fieldWithPath("userId").description("用户ID"),
                    fieldWithPath("items").description("商品列表")
                ),
                responseFields(
                    fieldWithPath("orderId").description("订单编号"),
                    fieldWithPath("status").description("订单状态")
                )));
    }
}

5.2 契约测试

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class ContractTest {
    
    @Test
    void apiContractCompliance() {
        // 验证响应是否符合 OpenAPI 契约
        String openApiJson = restTemplate.getForObject("/v3/api-docs", String.class);
        OpenAPI openAPI = new OpenAPIParser().readContents(openApiJson, null, null).getOpenAPI();
        
        // 使用 swagger-request-validator 验证
        OpenApiValidationFilter validationFilter = new OpenApiValidationFilter(openAPI);
        
        given()
            .filter(validationFilter)
            .when()
            .get("/api/orders/ORD-001")
            .then()
            .statusCode(200);
    }
}

六、前端 Mock 服务

# docker-compose.yml,一键启动 Mock 服务
version: '3'
services:
  prism:
    image: stoplight/prism:4
    command: >
      mock -h 0.0.0.0 /api/openapi.json
      --dynamic
    volumes:
      - ./target/openapi.json:/api/openapi.json:ro
    ports:
      - "4010:4010"

七、最佳实践

7.1 注解层级策略

推荐层级:
├── Controller 层      → @Tag, @Operation, @ApiResponse
├── DTO/Request        → @Schema(description, required, example)
├── DTO/Response       → @Schema + @JsonFormat
├── 枚举               → @Schema(description) on each enum value
└── 通用字段           → 抽象基类复用

7.2 文档即契约

  • 接口变更时同步更新注解
  • 将 OpenAPI JSON 纳入版本控制
  • CI 中增加契约兼容性检查
  • 前端基于 OpenAPI 生成 TypeScript 类型

7.3 安全考虑

springdoc:
  show-actuator: false        # 不暴露 Actuator 端点
  show-login-endpoint: false
  api-docs:
    enabled: true
  swagger-ui:
    enabled: ${SWAGGER_ENABLED:true}   # 生产环境可关闭
    oauth:
      client-id: swagger-ui

八、总结

能力实现方式价值
自动文档注解驱动减少文档维护成本
接口契约OpenAPI 3.0 规范前后端并行开发
离线导出Maven 插件交付与归档
Mock 测试Prism前端独立开发
代码生成OpenAPI Generator类型安全

SpringDoc OpenAPI 不仅是文档工具,更是API 契约管理的基础设施。将文档嵌入代码、通过 CI 验证契约、让文档随版本演进,是现代 API 开发的核心实践。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java-enterprise」更多文章

  1. 限流算法深度解析:令牌桶、漏桶与滑动窗口计数
  2. Java 代码质量:SonarQube、Checkstyle 与 SpotBugs 工程化实践
  3. Spring IoC 容器与依赖注入原理深度剖析