每天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分钟搞定文档生成
- 丰富注解:通过注解生成完整文档
- 安全集成:与Spring Security完美结合
- 代码生成:自动生成多语言客户端SDK
- 导出分享:支持HTML/PDF/Markdown多种格式
核心价值:
- ✅ 提高效率:文档与代码同步更新,永不落后
- ✅ 提升质量:接口定义清晰,减少联调问题
- ✅ 规范协作:前后端基于文档进行开发
- ✅ 自动化:集成CI/CD,自动发布文档
资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。