演示环境说明:

  • 开发工具:IDEA 2021.3
  • JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
  • Spring Boot版本:3.5.4(于25年7月24日发布)
  • Maven版本:3.8.2 (或更高)
  • Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
  • 操作系统:Windows 11

📝 前言

哎呀,说起API文档这个话题,我就忍不住要吐槽一下!🙄 作为一个在代码世界里摸爬滚打多年的老司机,我见过太多因为API文档不规范、不及时更新而导致的"血案"了。前端小伙伴拿着过时的文档调接口,后端同学忙着解释"这个参数已经改了",测试同学更是一脸懵逼地问"这个接口到底返回什么数据?"😂

不过呢,随着SpringBoot 3.x的横空出世,特别是它与OpenAPI 3.0的完美融合,这些痛点终于有了优雅的解决方案!今天我就来跟大家好好聊聊这个话题,保证让你看完之后直呼"原来如此"!💡

🎯 SpringBoot 3.x与OpenAPI的邂逅

说到SpringBoot 3.x,那可真是个里程碑式的版本啊!😍 它不仅拥抱了Java 17的新特性,还对整个生态进行了大幅度的升级。而OpenAPI(以前叫Swagger Specification)作为API文档的标准规范,在3.0版本中也是焕然一新!

🔍 什么是OpenAPI 3.0?

OpenAPI 3.0是一个用于描述REST API的规范标准,它就像是给你的API写了一份"身份证"📋。通过这个规范,你可以清晰地描述:

  • API的基本信息(版本、描述等)
  • 接口路径和HTTP方法
  • 请求参数和响应格式
  • 认证方式
  • 错误码定义

相比于OpenAPI 2.0(Swagger 2.0),3.0版本带来了不少好东西:

  • 更灵活的数据类型支持:支持oneOfanyOfallOf等复杂类型组合
  • 更强大的认证机制:支持OAuth 2.0、OpenID Connect等现代认证方式
  • 组件复用:通过components实现更好的复用性
  • 回调支持:支持异步API的回调定义

🤝 SpringBoot 3.x的变化

SpringBoot 3.x最大的变化就是全面拥抱了Jakarta EE!🎉 这意呀着:

  • 包名从javax.*变成了jakarta.*
  • 最低Java版本要求提升到17
  • 原生支持GraalVM
  • 更好的可观测性支持

这些变化对我们集成OpenAPI有什么影响呢?别急,咱们慢慢道来!

🔧 环境搭建:让一切准备就绪

俗话说"工欲善其事,必先利其器"!在开始我们的OpenAPI之旅之前,先把环境搭建好。😎

📦 项目依赖配置

首先,我们需要在pom.xml中添加相关依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>

<groupId>com.example</groupId>
<artifactId>springboot3-openapi-demo</artifactId>
<version>1.0.0</version>
<name>SpringBoot 3.x OpenAPI Demo</name>
<description>SpringBoot 3.x 集成 OpenAPI 示例项目</description>

<properties>
<java.version>17</java.version>
<springdoc.version>2.2.0</springdoc.version>
</properties>

<dependencies>
<!-- SpringBoot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- SpringBoot Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<!-- SpringDoc OpenAPI UI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>

<!-- SpringBoot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

看到这里,可能有小伙伴要问了:"诶,怎么没有看到springfox的依赖啊?"🤔 哈哈,这就是SpringBoot 3.x时代的变化啦!由于SpringFox项目已经很久没有更新了,并且不兼容SpringBoot 3.x的Jakarta命名空间,所以我们选择了更活跃的SpringDoc项目!

⚙️ 基础配置

接下来,在application.yml中添加一些基础配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 应用基础配置
server:
port: 8080
servlet:
context-path: /api

spring:
application:
name: springboot3-openapi-demo
profiles:
active: dev

# SpringDoc OpenAPI 配置
springdoc:
# 指定OpenAPI 3文档的路径,默认为/v3/api-docs
api-docs:
path: /v3/api-docs
enabled: true
# Swagger UI路径,默认为/swagger-ui.html
swagger-ui:
path: /swagger-ui.html
enabled: true
# 设置UI界面的一些配置
config-url: /v3/api-docs/swagger-config
url: /v3/api-docs
# 支持尝试调用
try-it-out-enabled: true
# 显示操作ID
display-operation-id: true
# 显示请求持续时间
display-request-duration: true
# 缓存时间设置(毫秒)
cache:
disabled: false
# 是否显示actuator接口
show-actuator: false

# 日志配置
logging:
level:
com.example: DEBUG
org.springframework.web: DEBUG
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'

这个配置可是我精心调试出来的,每一行都有它存在的意义!🎨 比如try-it-out-enabled: true就能让你直接在文档页面测试API,简直不要太爽!

📖 OpenAPI 3.0规范详解

在动手写代码之前,我们先来好好了解一下OpenAPI 3.0的规范结构。毕竟,知己知彼,百战不殆嘛!😄

🏗️ OpenAPI文档结构

一个完整的OpenAPI 3.0文档主要包含以下几个部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 这是一个OpenAPI文档的基本结构示例
openapi: 3.0.3
info:
title: "我的牛逼API"
description: "这是一个超级厉害的API文档"
version: "1.0.0"
contact:
name: "API支持团队"
email: "support@example.com"
servers:
- url: "https://api.example.com/v1"
description: "生产环境"
- url: "https://test-api.example.com/v1"
description: "测试环境"
paths:
/users:
get:
summary: "获取用户列表"
# ...详细定义
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string

🎪 核心概念解析

1️⃣ Info对象

这就是你的API的"名片",包含了标题、版本、描述等基本信息。

2️⃣ Servers对象

定义API服务器的地址,可以有多个环境(开发、测试、生产)。

3️⃣ Paths对象

这是重头戏!定义了所有的API路径和操作方法。

4️⃣ Components对象

这是复用的宝库,可以定义通用的数据模型、响应、参数等。

说到这里,我想起了刚开始学OpenAPI的时候,被这些概念绕得头晕转向的😵💫。不过别担心,咱们马上就通过实际代码来理解这些概念!

🚀 SpringDoc OpenAPI的集成实战

好了,理论知识铺垫够了,是时候撸起袖子干活了!💪 让我们从一个简单的用户管理API开始。

🎯 创建基础配置类

首先,我们创建一个OpenAPI配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.example.config;

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;

/**
* OpenAPI 3.0 配置类
* 这个类负责定义API文档的基本信息,相当于给我们的API做了个自我介绍!
*
* @author 你的名字
* @since 2024-01-01
*/
@Configuration
public class OpenApiConfig {

@Value("${spring.application.name}")
private String applicationName;

/**
* 创建OpenAPI实例
* 这个方法就像是在给我们的API写个人简历,把最重要的信息都写上!
*/
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
// 设置API基本信息
.info(new Info()
.title(applicationName + " API文档")
.description("🎉 这是基于SpringBoot 3.x和OpenAPI 3.0构建的超级API文档!" +
"\n\n### 主要功能\n" +
"- 🎯 用户管理:增删改查用户信息\n" +
"- 📝 数据验证:完整的参数校验\n" +
"- 🔒 权限控制:基于角色的访问控制\n" +
"- 📊 统计分析:用户行为数据统计\n\n" +
"### 技术栈\n" +
"- SpringBoot 3.2.0\n" +
"- OpenAPI 3.0.3\n" +
"- Jakarta Validation\n" +
"- SpringDoc 2.2.0")
.version("v1.0.0")
.contact(new Contact()
.name("开发团队")
.email("dev-team@example.com")
.url("https://www.example.com"))
.license(new License()
.name("MIT License")
.url("https://opensource.org/licenses/MIT")))
// 设置服务器信息
.servers(List.of(
new Server()
.url("http://localhost:8080/api")
.description("本地开发环境 🏠"),
new Server()
.url("https://test-api.example.com")
.description("测试环境 🧪"),
new Server()
.url("https://api.example.com")
.description("生产环境 🚀")
));
}
}

看到这个配置类,是不是感觉很贴心?😊 我特意在描述里用了Markdown格式,这样生成的文档会更加美观!而且还用了emoji,让冷冰冰的技术文档变得生动有趣。

👤 创建用户实体类

接下来,我们定义一个用户实体类,这里就能体现OpenAPI的强大之处了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package com.example.entity;

import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import java.time.LocalDateTime;

/**
* 用户实体类
* 这个类不仅定义了用户的基本属性,还通过注解描述了每个字段的含义
* 简直就是"人如其名"的完美体现!
*/
@Schema(name = "User", description = "用户信息实体类")
public class User {

@Schema(description = "用户ID,系统自动生成",
example = "1001",
accessMode = Schema.AccessMode.READ_ONLY)
private Long id;

@Schema(description = "用户名,必须唯一",
example = "zhangsan",
requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20字符之间")
@Pattern(regexp = "^[a-zA-Z0-9_]+$",
message = "用户名只能包含字母、数字和下划线")
private String username;

@Schema(description = "用户昵称",
example = "张三")
@Size(max = 50, message = "昵称长度不能超过50字符")
private String nickname;

@Schema(description = "邮箱地址",
example = "zhangsan@example.com",
requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;

@Schema(description = "手机号码",
example = "13812345678")
@Pattern(regexp = "^1[3-9]\\d{9}$",
message = "手机号码格式不正确")
private String phone;

@Schema(description = "用户年龄",
example = "25",
minimum = "1",
maximum = "150")
@Min(value = 1, message = "年龄必须大于0")
@Max(value = 150, message = "年龄不能超过150")
private Integer age;

@Schema(description = "用户性别",
example = "MALE",
allowableValues = {"MALE", "FEMALE", "UNKNOWN"})
private Gender gender;

@Schema(description = "用户状态",
example = "ACTIVE",
allowableValues = {"ACTIVE", "INACTIVE", "BANNED"})
private UserStatus status;

@Schema(description = "创建时间",
example = "2024-01-01 12:00:00",
accessMode = Schema.AccessMode.READ_ONLY)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;

@Schema(description = "最后更新时间",
example = "2024-01-01 12:00:00",
accessMode = Schema.AccessMode.READ_ONLY)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;

// 枚举类定义
@Schema(description = "性别枚举")
public enum Gender {
@Schema(description = "男性")
MALE,
@Schema(description = "女性")
FEMALE,
@Schema(description = "未知")
UNKNOWN
}

@Schema(description = "用户状态枚举")
public enum UserStatus {
@Schema(description = "正常状态")
ACTIVE,
@Schema(description = "非活跃状态")
INACTIVE,
@Schema(description = "被封禁状态")
BANNED
}

// 构造函数、getter、setter方法...
// 这里省略了这些方法,实际开发中记得加上哦!

public User() {}

public User(String username, String email) {
this.username = username;
this.email = email;
this.status = UserStatus.ACTIVE;
this.createTime = LocalDateTime.now();
this.updateTime = LocalDateTime.now();
}

// getter and setter methods...
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }

public String getNickname() { return nickname; }
public void setNickname(String nickname) { this.nickname = nickname; }

public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }

public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }

public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }

public Gender getGender() { return gender; }
public void setGender(Gender gender) { this.gender = gender; }

public UserStatus getStatus() { return status; }
public void setStatus(UserStatus status) { this.status = status; }

public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }

public LocalDateTime getUpdateTime() { return updateTime; }
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
}

哇,这个实体类是不是很丰富?😍 通过@Schema注解,我们不仅描述了每个字段的含义,还提供了示例值、验证规则等信息。这样生成的API文档就会非常详细和友好!

🎛️ 创建通用响应类

为了让API响应更加规范,我们定义一个通用的响应包装类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package com.example.common;

import io.swagger.v3.oas.annotations.media.Schema;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;

/**
* 统一响应结果封装类
* 这个类就像是给所有API响应穿了一套统一的"制服",让它们看起来整整齐齐的!
*
* @param <T> 响应数据的类型
*/
@Schema(name = "ApiResponse", description = "统一响应结果")
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 = "2024-01-01 12:00:00")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime timestamp;

// 私有构造函数,通过静态方法创建实例
private ApiResponse(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.timestamp = LocalDateTime.now();
}

/**
* 创建成功响应
* 这个方法就像是在说:"耶!一切都很顺利!"
*/
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "操作成功", data);
}

public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(200, message, data);
}

/**
* 创建失败响应
* 这个方法就像是在说:"哎呀,出了点小问题..."
*/
public static <T> ApiResponse<T> error(Integer code, String message) {
return new ApiResponse<>(code, message, null);
}

public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(500, message, null);
}

/**
* 参数验证失败响应
* 专门处理那些"不听话"的参数
*/
public static <T> ApiResponse<T> badRequest(String message) {
return new ApiResponse<>(400, message, null);
}

/**
* 资源未找到响应
* 当你要找的东西"人间蒸发"了的时候用这个
*/
public static <T> ApiResponse<T> notFound(String message) {
return new ApiResponse<>(404, message, null);
}

// getter methods
public Integer getCode() { return code; }
public String getMessage() { return message; }
public T getData() { return data; }
public LocalDateTime getTimestamp() { return timestamp; }

// 判断是否成功的便利方法
public boolean isSuccess() {
return code != null && code == 200;
}
}

这个响应类设计得还不错吧?😏 它不仅提供了统一的响应格式,还通过静态方法让创建响应变得超级简单!

🎨 定制化配置:让文档更贴心

现在我们来创建一个功能完整的用户控制器,这里可是展现OpenAPI威力的地方!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
package com.example.controller;

import com.example.common.ApiResponse;
import com.example.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.responses.ApiResponse as SwaggerApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
* 用户管理控制器
* 这个控制器就像是一个用户管理的"大管家",负责处理所有用户相关的请求
* 每个方法都经过精心设计,让API文档变得超级友好!
*/
@RestController
@RequestMapping("/users")
@Tag(name = "用户管理", description = "提供用户的增删改查功能,支持批量操作和高级查询 🎯")
@Validated
public class UserController {

// 模拟数据库,实际项目中应该注入Service
private final List<User> userDatabase = new ArrayList<>();
private Long idCounter = 1L;

// 构造函数中初始化一些测试数据
public UserController() {
// 添加一些测试数据,让文档演示更生动
User user1 = new User("admin", "admin@example.com");
user1.setId(idCounter++);
user1.setNickname("超级管理员");
user1.setAge(30);
user1.setGender(User.Gender.MALE);
user1.setPhone("13812345678");

User user2 = new User("alice", "alice@example.com");
user2.setId(idCounter++);
user2.setNickname("小爱同学");
user2.setAge(25);
user2.setGender(User.Gender.FEMALE);
user2.setPhone("13987654321");

userDatabase.add(user1);
userDatabase.add(user2);
}

/**
* 获取用户列表
* 这个接口支持分页和搜索,功能很强大哦!
*/
@GetMapping
@Operation(
summary = "获取用户列表",
description = "支持分页查询和关键字搜索的用户列表接口。" +
"\n\n**功能特点:**\n" +
"- 🔍 支持用户名和昵称模糊搜索\n" +
"- 📄 支持分页查询,避免数据量过大\n" +
"- ⚡ 查询性能优化,响应速度快\n" +
"- 📊 返回总数信息,方便前端分页处理",
tags = {"用户查询"}
)
@Parameters({
@Parameter(
name = "page",
description = "页码,从1开始",
example = "1",
in = ParameterIn.QUERY,
schema = @Schema(type = "integer", minimum = "1", defaultValue = "1")
),
@Parameter(
name = "size",
description = "每页大小,最大100",
example = "10",
in = ParameterIn.QUERY,
schema = @Schema(type = "integer", minimum = "1", maximum = "100", defaultValue = "10")
),
@Parameter(
name = "keyword",
description = "搜索关键字,支持用户名和昵称模糊搜索",
example = "张三",
in = ParameterIn.QUERY,
schema = @Schema(type = "string")
)
})
@ApiResponses({
@SwaggerApiResponse(
responseCode = "200",
description = "查询成功",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(
name = "成功示例",
summary = "查询成功的响应示例",
value = """
{
"code": 200,
"message": "查询成功",
"data": {
"list": [
{
"id": 1,
"username": "admin",
"nickname": "超级管理员",
"email": "admin@example.com",
"age": 30,
"gender": "MALE",
"status": "ACTIVE",
"createTime": "2024-01-01 12:00:00"
}
],
"total": 1,
"page": 1,
"size": 10
},
"timestamp": "2024-01-01 12:00:00"
}
"""
)
)
),
@SwaggerApiResponse(
responseCode = "400",
description = "参数错误",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ApiResponse.class)
)
)
})
public ResponseEntity<ApiResponse<PageResult<User>>> getUsers(
@RequestParam(defaultValue = "1") @Min(value = 1, message = "页码必须大于0") Integer page,
@RequestParam(defaultValue = "10") @Min(value = 1, message = "页大小必须大于0") Integer size,
@RequestParam(required = false) String keyword) {

// 模拟分页查询逻辑
List<User> filteredUsers = userDatabase;

// 关键字搜索
if (keyword != null && !keyword.trim().isEmpty()) {
filteredUsers = userDatabase.stream()
.filter(user -> user.getUsername().contains(keyword) ||
(user.getNickname() != null && user.getNickname().contains(keyword)))
.toList();
}

// 分页处理
int start = (page - 1) * size;
int end = Math.min(start + size, filteredUsers.size());
List<User> pagedUsers = filteredUsers.subList(start, end);

PageResult<User> result = new PageResult<>(pagedUsers, (long) filteredUsers.size(), page, size);

return ResponseEntity.ok(ApiResponse.success("查询成功", result));
}

/**
* 根据ID获取用户详情
* 通过用户ID获取详细信息,找不到会返回404
*/
@GetMapping("/{id}")
@Operation(
summary = "获取用户详情",
description = "根据用户ID获取用户的详细信息。\n\n" +
"**注意事项:**\n" +
"- 用户ID必须是有效的正整数\n" +
"- 如果用户不存在,会返回404错误\n" +
"- 返回的数据包含用户的所有可见字段",
tags = {"用户查询"}
)
@Parameter(
name = "id",
description = "用户ID",
example = "1",
required = true,
in = ParameterIn.PATH,
schema = @Schema(type = "integer", format = "int64", minimum = "1")
)
@ApiResponses({
@SwaggerApiResponse(
responseCode = "200",
description = "获取成功",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(
name = "用户详情",
value = """
{
"code": 200,
"message": "获取成功",
"data": {
"id": 1,
"username": "admin",
"nickname": "超级管理员",
"email": "admin@example.com",
"phone": "13812345678",
"age": 30,
"gender": "MALE",
"status": "ACTIVE",
"createTime": "2024-01-01 12:00:00",
"updateTime": "2024-01-01 12:00:00"
},
"timestamp": "2024-01-01 12:00:00"
}
"""
)
)
),
@SwaggerApiResponse(
responseCode = "404",
description = "用户不存在",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
value = """
{
"code": 404,
"message": "用户不存在",
"data": null,
"timestamp": "2024-01-01 12:00:00"
}
"""
)
)
)
})
public ResponseEntity<ApiResponse<User>> getUserById(
@PathVariable @NotNull(message = "用户ID不能为空") Long id) {

Optional<User> userOpt = userDatabase.stream()
.filter(user -> user.getId().equals(id))
.findFirst();

if (userOpt.isPresent()) {
return ResponseEntity.ok(ApiResponse.success("获取成功", userOpt.get()));
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.notFound("用户不存在"));
}
}

/**
* 创建新用户
* 这个接口可以创建一个全新的用户,支持完整的数据验证
*/
@PostMapping
@Operation(
summary = "创建用户",
description = "创建一个新的用户账户。\n\n" +
"**验证规则:**\n" +
"- 用户名:3-20字符,只能包含字母、数字和下划线\n" +
"- 邮箱:必须是有效的邮箱格式\n" +
"- 手机号:符合中国大陆手机号格式\n" +
"- 年龄:1-150之间的整数\n\n" +
"**注意:**\n" +
"- 用户名和邮箱必须唯一\n" +
"- 创建成功后会自动设置为ACTIVE状态\n" +
"- 系统会自动设置创建时间和更新时间",
tags = {"用户管理"}
)
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "用户信息",
required = true,
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = User.class),
examples = {
@ExampleObject(
name = "基础用户",
summary = "创建基础用户",
value = """
{
"username": "newuser",
"nickname": "新用户",
"email": "newuser@example.com",
"phone": "13911112222",
"age": 28,
"gender": "FEMALE"
}
"""
),
@ExampleObject(
name = "完整用户",
summary = "创建完整信息用户",
value = """
{
"username": "fulluser",
"nickname": "完整用户",
"email": "fulluser@example.com",
"phone": "13800138000",
"age": 35,
"gender": "MALE"
}
"""
)
}
)
)
@ApiResponses({
@SwaggerApiResponse(
responseCode = "201",
description = "创建成功",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ApiResponse.class)
)
),
@SwaggerApiResponse(
responseCode = "400",
description = "参数验证失败",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
value = """
{
"code": 400,
"message": "用户名已存在",
"data": null,
"timestamp": "2024-01-01 12:00:00"
}
"""
)
)
)
})
public ResponseEntity<ApiResponse<User>> createUser(@Valid @RequestBody User user) {

// 检查用户名是否已存在
boolean usernameExists = userDatabase.stream()
.anyMatch(u -> u.getUsername().equals(user.getUsername()));
if (usernameExists) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.badRequest("用户名已存在"));
}

// 检查邮箱是否已存在
boolean emailExists = userDatabase.stream()
.anyMatch(u -> u.getEmail().equals(user.getEmail()));
if (emailExists) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.badRequest("邮箱已存在"));
}

// 设置用户信息
user.setId(idCounter++);
user.setStatus(User.UserStatus.ACTIVE);
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());

// 保存到"数据库"
userDatabase.add(user);

return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success("用户创建成功", user));
}

// 分页结果包装类
@Schema(name = "PageResult", description = "分页查询结果")
public static class PageResult<T> {
@Schema(description = "数据列表")
private List<T> list;

@Schema(description = "总记录数", example = "100")
private Long total;

@Schema(description = "当前页码", example = "1")
private Integer page;

@Schema(description = "每页大小", example = "10")
private Integer size;

@Schema(description = "总页数", example = "10")
private Integer totalPages;

@Schema(description = "是否有下一页", example = "true")
private Boolean hasNext;

@Schema(description = "是否有上一页", example = "false")
private Boolean hasPrev;

public PageResult(List<T> list, Long total, Integer page, Integer size) {
this.list = list;
this.total = total;
this.page = page;
this.size = size;
this.totalPages = (int) Math.ceil((double) total / size);
this.hasNext = page < totalPages;
this.hasPrev = page > 1;
}

// getter methods
public List<T> getList() { return list; }
public Long getTotal() { return total; }
public Integer getPage() { return page; }
public Integer getSize() { return size; }
public Integer getTotalPages() { return totalPages; }
public Boolean getHasNext() { return hasNext; }
public Boolean getHasPrev() { return hasPrev; }
}
}

哇塞,这个控制器是不是很详细?😄 每个接口都有完整的文档说明、参数描述、响应示例等。通过这样的注解配置,生成的API文档会非常专业和友好!

💎 高级特性探索

好了,基础功能我们已经搞定了,现在让我们来探索一些更高级的特性!这些功能可是让我当初惊呼"卧槽,还能这样玩?"的存在!🤯

🔐 安全认证集成

在实际项目中,API安全是不可忽视的。让我们看看如何在OpenAPI中集成JWT认证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.example.config;

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;

/**
* API安全配置
* 这个配置让我们的API文档支持JWT认证,再也不用担心接口裸奔了!
*/
@Configuration
public class OpenApiSecurityConfig {

private static final String BEARER_TOKEN_SECURITY_SCHEME = "bearerAuth";

@Bean
public OpenAPI openAPIWithSecurity() {
return new OpenAPI()
.addSecurityItem(new SecurityRequirement()
.addList(BEARER_TOKEN_SECURITY_SCHEME))
.components(new Components()
.addSecuritySchemes(BEARER_TOKEN_SECURITY_SCHEME,
new SecurityScheme()
.name(BEARER_TOKEN_SECURITY_SCHEME)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
.description("输入JWT token,格式:Bearer {token}")));
}
}

然后在需要认证的接口上添加安全注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@RequestMapping("/admin")
@Tag(name = "管理员接口", description = "需要管理员权限的高级功能 👑")
public class AdminController {

@GetMapping("/users")
@Operation(
summary = "管理员获取所有用户",
description = "只有管理员才能访问的用户列表接口,包含敏感信息",
security = @SecurityRequirement(name = "bearerAuth")
)
@SecurityRequirement(name = "bearerAuth")
public ResponseEntity<ApiResponse<List<User>>> getAllUsersForAdmin() {
// 管理员专用接口逻辑
return ResponseEntity.ok(ApiResponse.success("获取成功", new ArrayList<>()));
}
}

🎪 自定义注解与切面

有时候我们想为某些特殊的接口添加统一的文档说明,这时候自定义注解就派上用场了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package com.example.annotation;

import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 标准API响应注解
* 这个注解就像是给接口贴上了"质量保证"的标签,统一了响应格式
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "操作成功",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
name = "成功响应",
value = """
{
"code": 200,
"message": "操作成功",
"data": {},
"timestamp": "2024-01-01 12:00:00"
}
"""
)
)
),
@ApiResponse(
responseCode = "400",
description = "参数错误",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
value = """
{
"code": 400,
"message": "参数验证失败",
"data": null,
"timestamp": "2024-01-01 12:00:00"
}
"""
)
)
),
@ApiResponse(
responseCode = "500",
description = "服务器内部错误",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
value = """
{
"code": 500,
"message": "服务器开小差了,请稍后重试",
"data": null,
"timestamp": "2024-01-01 12:00:00"
}
"""
)
)
)
})
public @interface StandardApiResponses {
/**
* 是否包含认证相关的错误响应
*/
boolean includeAuth() default false;
}

使用起来超级简单:

1
2
3
4
5
6
7
@GetMapping("/profile")
@StandardApiResponses(includeAuth = true)
@Operation(summary = "获取个人资料", description = "获取当前登录用户的个人资料信息")
public ResponseEntity<ApiResponse<User>> getProfile() {
// 接口逻辑
return ResponseEntity.ok(ApiResponse.success("获取成功", new User()));
}

🌈 多环境配置

不同环境下,我们的API文档可能需要不同的配置。来看看如何优雅地处理这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.example.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
* OpenAPI配置属性类
* 通过配置文件来控制API文档的行为,灵活性Max!
*/
@Configuration
@ConfigurationProperties(prefix = "app.openapi")
public class OpenApiProperties {

private boolean enabled = true;
private String title = "API文档";
private String description = "这是一个很棒的API";
private String version = "v1.0.0";
private Contact contact = new Contact();
private License license = new License();

// 嵌套配置类
public static class Contact {
private String name = "开发团队";
private String email = "dev@example.com";
private String url = "https://www.example.com";

// getter and setter...
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
}

public static class License {
private String name = "MIT";
private String url = "https://opensource.org/licenses/MIT";

// getter and setter...
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
}

// 主类的getter and setter
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }

public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }

public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }

public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }

public Contact getContact() { return contact; }
public void setContact(Contact contact) { this.contact = contact; }

public License getLicense() { return license; }
public void setLicense(License license) { this.license = license; }
}

然后在不同环境的配置文件中设置不同的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# application-dev.yml (开发环境)
app:
openapi:
enabled: true
title: "开发环境API文档 🛠️"
description: "这是开发环境的API,随时可能变动,请谨慎使用"
version: "v1.0.0-dev"
contact:
name: "开发团队"
email: "dev@example.com"

# application-prod.yml (生产环境)
app:
openapi:
enabled: false # 生产环境关闭文档
title: "生产环境API"
description: "生产环境API文档"
version: "v1.0.0"

这样配置之后,生产环境就不会暴露API文档了,安全性大大提升!😎

🛠️ 实际项目应用案例

理论说了这么多,让我们来看一个更贴近实际项目的完整示例!假设我们正在开发一个电商系统的商品管理模块。

🛍️ 商品管理模块

首先定义商品实体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package com.example.ecommerce.entity;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

@Schema(name = "Product", description = "商品信息")
public class Product {

@Schema(description = "商品ID", example = "1", accessMode = Schema.AccessMode.READ_ONLY)
private Long id;

@Schema(description = "商品名称", example = "iPhone 15 Pro Max", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "商品名称不能为空")
@Size(min = 2, max = 100, message = "商品名称长度必须在2-100字符之间")
private String name;

@Schema(description = "商品描述", example = "苹果最新旗舰手机,性能强悍,拍照出色")
@Size(max = 1000, message = "商品描述不能超过1000字符")
private String description;

@Schema(description = "商品价格", example = "9999.00", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "商品价格不能为空")
@DecimalMin(value = "0.01", message = "商品价格必须大于0.01")
@DecimalMax(value = "999999.99", message = "商品价格不能超过999999.99")
private BigDecimal price;

@Schema(description = "库存数量", example = "100", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "库存数量不能为空")
@Min(value = 0, message = "库存数量不能为负数")
private Integer stock;

@Schema(description = "商品分类ID", example = "1", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "商品分类不能为空")
private Long categoryId;

@Schema(description = "商品分类名称", example = "数码产品", accessMode = Schema.AccessMode.READ_ONLY)
private String categoryName;

@Schema(description = "商品图片URL列表")
private List<String> imageUrls;

@Schema(description = "商品状态", example = "ACTIVE", allowableValues = {"ACTIVE", "INACTIVE", "OUT_OF_STOCK"})
private ProductStatus status;

@Schema(description = "创建时间", accessMode = Schema.AccessMode.READ_ONLY)
private LocalDateTime createTime;

@Schema(description = "更新时间", accessMode = Schema.AccessMode.READ_ONLY)
private LocalDateTime updateTime;

// 商品状态枚举
public enum ProductStatus {
@Schema(description = "正常销售")
ACTIVE,
@Schema(description = "已下架")
INACTIVE,
@Schema(description = "缺货")
OUT_OF_STOCK
}

// 构造函数和getter/setter方法... (省略)
public Product() {}

// getter and setter methods
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

public String getName() { return name; }
public void setName(String name) { this.name = name; }

public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }

public BigDecimal getPrice() { return price; }
public void setPrice(BigDecimal price) { this.price = price; }

public Integer getStock() { return stock; }
public void setStock(Integer stock) { this.stock = stock; }

public Long getCategoryId() { return categoryId; }
public void setCategoryId(Long categoryId) { this.categoryId = categoryId; }

public String getCategoryName() { return categoryName; }
public void setCategoryName(String categoryName) { this.categoryName = categoryName; }

public List<String> getImageUrls() { return imageUrls; }
public void setImageUrls(List<String> imageUrls) { this.imageUrls = imageUrls; }

public ProductStatus getStatus() { return status; }
public void setStatus(ProductStatus status) { this.status = status; }

public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }

public LocalDateTime getUpdateTime() { return updateTime; }
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
}

🎯 商品控制器

然后创建一个功能丰富的商品控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
package com.example.ecommerce.controller;

import com.example.common.ApiResponse;
import com.example.ecommerce.entity.Product;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.responses.ApiResponse as SwaggerApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

/**
* 商品管理控制器
* 电商系统的核心模块之一,负责商品的全生命周期管理
*
* 这个控制器就像是商品的"生命管家",从商品的诞生到下架,
* 每一个环节都在这里得到精心呵护!🛍️
*/
@RestController
@RequestMapping("/products")
@Tag(name = "商品管理", description = "电商系统商品管理核心接口,支持CRUD、搜索、库存管理等功能 🛒")
public class ProductController {

/**
* 商品搜索接口
* 这个接口功能强大,支持多维度搜索,简直是商品查找的"瑞士军刀"!
*/
@GetMapping("/search")
@Operation(
summary = "商品搜索",
description = """
支持多维度商品搜索的强大接口。

### 🔍 搜索功能
- **关键字搜索**: 支持商品名称、描述模糊搜索
- **分类筛选**: 按商品分类进行筛选
- **价格区间**: 支持最低价和最高价筛选
- **状态筛选**: 支持按商品状态筛选
- **排序功能**: 支持按价格、创建时间、销量等排序

### 💡 使用技巧
- 多个条件可以组合使用,系统会智能匹配
- 支持分页查询,避免数据量过大影响性能
- 默认按相关度排序,也可以指定其他排序方式
""",
tags = {"商品查询"}
)
@Parameters({
@Parameter(name = "keyword", description = "搜索关键字", example = "iPhone"),
@Parameter(name = "categoryId", description = "商品分类ID", example = "1"),
@Parameter(name = "minPrice", description = "最低价格", example = "100.00"),
@Parameter(name = "maxPrice", description = "最高价格", example = "10000.00"),
@Parameter(name = "status", description = "商品状态", example = "ACTIVE"),
@Parameter(name = "sortBy", description = "排序字段", example = "price",
schema = @Schema(allowableValues = {"price", "createTime", "sales"})),
@Parameter(name = "sortOrder", description = "排序方向", example = "asc",
schema = @Schema(allowableValues = {"asc", "desc"})),
@Parameter(name = "page", description = "页码", example = "1"),
@Parameter(name = "size", description = "每页大小", example = "20")
})
@ApiResponses({
@SwaggerApiResponse(
responseCode = "200",
description = "搜索成功",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
name = "搜索结果",
value = """
{
"code": 200,
"message": "搜索成功",
"data": {
"list": [
{
"id": 1,
"name": "iPhone 15 Pro Max",
"description": "苹果最新旗舰手机",
"price": 9999.00,
"stock": 50,
"categoryId": 1,
"categoryName": "数码产品",
"status": "ACTIVE",
"imageUrls": ["http://example.com/image1.jpg"]
}
],
"total": 1,
"page": 1,
"size": 20
},
"timestamp": "2024-01-01 12:00:00"
}
"""
)
)
)
})
public ResponseEntity<ApiResponse<Object>> searchProducts(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) BigDecimal minPrice,
@RequestParam(required = false) BigDecimal maxPrice,
@RequestParam(required = false) Product.ProductStatus status,
@RequestParam(defaultValue = "createTime") String sortBy,
@RequestParam(defaultValue = "desc") String sortOrder,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer size) {

// 模拟搜索逻辑
Product mockProduct = new Product();
mockProduct.setId(1L);
mockProduct.setName("iPhone 15 Pro Max");
mockProduct.setDescription("苹果最新旗舰手机,性能强悍");
mockProduct.setPrice(new BigDecimal("9999.00"));
mockProduct.setStock(50);
mockProduct.setCategoryId(1L);
mockProduct.setCategoryName("数码产品");
mockProduct.setStatus(Product.ProductStatus.ACTIVE);
mockProduct.setImageUrls(Arrays.asList("http://example.com/image1.jpg"));
mockProduct.setCreateTime(LocalDateTime.now());

List<Product> products = Arrays.asList(mockProduct);

// 构造分页结果
Object result = new Object() {
public final List<Product> list = products;
public final long total = 1;
public final int page = 1;
public final int size = 20;
public final boolean hasNext = false;
public final boolean hasPrev = false;
};

return ResponseEntity.ok(ApiResponse.success("搜索成功", result));
}

/**
* 创建商品
* 这个接口就像是商品的"出生登记处",每个新商品都要在这里登记造册!
*/
@PostMapping
@Operation(
summary = "创建商品",
description = """
创建一个新的商品。

### ✅ 验证规则
- 商品名称:2-100字符,必填
- 商品价格:0.01-999999.99,必填
- 库存数量:不能为负数,必填
- 商品分类:必须选择有效分类,必填
- 商品描述:最多1000字符,选填

### 🎯 创建后
- 系统自动设置创建时间和更新时间
- 默认状态为ACTIVE(正常销售)
- 返回完整的商品信息,包括系统生成的ID
""",
tags = {"商品管理"}
)
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "商品信息",
required = true,
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = Product.class),
examples = @ExampleObject(
name = "创建商品示例",
value = """
{
"name": "MacBook Pro 16英寸",
"description": "苹果专业级笔记本电脑,适合开发和设计工作",
"price": 19999.00,
"stock": 30,
"categoryId": 2,
"imageUrls": [
"http://example.com/macbook1.jpg",
"http://example.com/macbook2.jpg"
]
}
"""
)
)
)
@ApiResponses({
@SwaggerApiResponse(
responseCode = "201",
description = "商品创建成功",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
value = """
{
"code": 200,
"message": "商品创建成功",
"data": {
"id": 1,
"name": "MacBook Pro 16英寸",
"description": "苹果专业级笔记本电脑",
"price": 19999.00,
"stock": 30,
"categoryId": 2,
"categoryName": "笔记本电脑",
"status": "ACTIVE",
"createTime": "2024-01-01 12:00:00",
"updateTime": "2024-01-01 12:00:00"
},
"timestamp": "2024-01-01 12:00:00"
}
"""
)
)
),
@SwaggerApiResponse(
responseCode = "400",
description = "参数验证失败",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
value = """
{
"code": 400,
"message": "商品名称不能为空",
"data": null,
"timestamp": "2024-01-01 12:00:00"
}
"""
)
)
)
})
public ResponseEntity<ApiResponse<Product>> createProduct(@Valid @RequestBody Product product) {

// 模拟创建逻辑
product.setId(System.currentTimeMillis()); // 模拟ID生成
product.setStatus(Product.ProductStatus.ACTIVE);
product.setCreateTime(LocalDateTime.now());
product.setUpdateTime(LocalDateTime.now());
product.setCategoryName("数码产品"); // 模拟分类名称

return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success("商品创建成功", product));
}

/**
* 批量更新库存
* 这个接口就像是仓库管理员,可以一次性调整多个商品的库存
*/
@PatchMapping("/stock/batch")
@Operation(
summary = "批量更新库存",
description = """
批量更新多个商品的库存数量。

### 📦 功能说明
- 支持同时更新多个商品的库存
- 支持增加或减少库存操作
- 会自动检查库存不能为负数
- 库存变动会记录日志,便于追踪

### ⚠️ 注意事项
- 减少库存时,不能使库存变为负数
- 商品ID必须存在,否则会跳过该商品
- 操作完成后会返回更新结果统计
""",
tags = {"库存管理"}
)
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "库存更新信息列表",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
name = "批量更新库存",
value = """
[
{
"productId": 1,
"quantity": 10,
"operation": "ADD"
},
{
"productId": 2,
"quantity": 5,
"operation": "SUBTRACT"
}
]
"""
)
)
)
public ResponseEntity<ApiResponse<Object>> batchUpdateStock(
@RequestBody List<StockUpdateRequest> requests) {

// 模拟批量更新逻辑
Object result = new Object() {
public final int totalRequests = requests.size();
public final int successCount = requests.size();
public final int failureCount = 0;
public final String message = "所有商品库存更新成功";
};

return ResponseEntity.ok(ApiResponse.success("批量更新完成", result));
}

// 库存更新请求DTO
@Schema(name = "StockUpdateRequest", description = "库存更新请求")
public static class StockUpdateRequest {

@Schema(description = "商品ID", example = "1", requiredMode = Schema.RequiredMode.REQUIRED)
private Long productId;

@Schema(description = "变动数量", example = "10", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer quantity;

@Schema(description = "操作类型", example = "ADD", allowableValues = {"ADD", "SUBTRACT"})
private StockOperation operation;

@Schema(description = "操作说明", example = "补货入库")
private String remark;

public enum StockOperation {
@Schema(description = "增加库存")
ADD,
@Schema(description = "减少库存")
SUBTRACT
}

// getter and setter
public Long getProductId() { return productId; }
public void setProductId(Long productId) { this.productId = productId; }

public Integer getQuantity() { return quantity; }
public void setQuantity(Integer quantity) { this.quantity = quantity; }

public StockOperation getOperation() { return operation; }
public void setOperation(StockOperation operation) { this.operation = operation; }

public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
}
}

看到这个商品控制器,是不是感觉很有实战价值?😍 它不仅包含了常见的CRUD操作,还有搜索、批量操作等高级功能,而且每个接口的文档都非常详细!

🎭 最佳实践与避坑指南

经过这么多年的摸爬滚打,我总结了一些使用OpenAPI的最佳实践,这些都是我踩过坑后的血泪经验啊!😭

✅ 最佳实践

1️⃣ 统一响应格式

一定要使用统一的响应格式!这不仅让API使用者感到舒适,也让文档看起来更专业。

1
2
3
4
5
6
7
8
9
10
11
// 好的做法 ✅
@PostMapping("/users")
public ResponseEntity<ApiResponse<User>> createUser(@RequestBody User user) {
return ResponseEntity.ok(ApiResponse.success("创建成功", user));
}

// 不好的做法 ❌
@PostMapping("/users")
public User createUser(@RequestBody User user) {
return user; // 没有统一包装,不利于错误处理
}
2️⃣ 详细的参数描述

参数描述要详细,要让使用者一看就明白:

1
2
3
4
5
6
7
8
9
10
// 好的做法 ✅
@Parameter(
name = "status",
description = "用户状态筛选条件。ACTIVE表示正常用户,INACTIVE表示禁用用户,BANNED表示封禁用户",
example = "ACTIVE",
schema = @Schema(allowableValues = {"ACTIVE", "INACTIVE", "BANNED"})
)

// 不好的做法 ❌
@Parameter(name = "status", description = "状态") // 太简单,不知道有哪些值
3️⃣ 合理使用示例

示例要贴近真实场景,不要用foobar这种无意义的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 好的做法 ✅
@ExampleObject(
name = "创建用户",
value = """
{
"username": "zhangsan",
"email": "zhangsan@example.com",
"age": 25,
"phone": "13812345678"
}
"""
)

// 不好的做法 ❌
@ExampleObject(
value = """
{
"username": "foo",
"email": "bar@baz.com",
"age": 123
}
"""
)

⚠️ 常见陷阱

1️⃣ 忘记处理不同响应码

很多同学只写200的情况,完全忘记了错误处理:

1
2
3
4
5
6
7
8
9
// 完整的做法 ✅
@ApiResponses({
@SwaggerApiResponse(responseCode = "200", description = "操作成功"),
@SwaggerApiResponse(responseCode = "400", description = "参数错误"),
@SwaggerApiResponse(responseCode = "401", description = "未认证"),
@SwaggerApiResponse(responseCode = "403", description = "权限不足"),
@SwaggerApiResponse(responseCode = "404", description = "资源不存在"),
@SwaggerApiResponse(responseCode = "500", description = "服务器错误")
})
2️⃣ 忽略数据验证注解

记住,Spring的验证注解和OpenAPI注解要配合使用:

1
2
3
4
5
// 正确的做法 ✅
@Schema(description = "用户年龄", example = "25", minimum = "1", maximum = "150")
@Min(value = 1, message = "年龄必须大于0")
@Max(value = 150, message = "年龄不能超过150")
private Integer age;
3️⃣ 生产环境暴露文档

生产环境一定要关闭API文档!这个坑我见过太多团队踩了:

1
2
3
4
5
6
7
8
9
10
# 生产环境配置
spring:
profiles:
active: prod

springdoc:
api-docs:
enabled: false # 生产环境关闭
swagger-ui:
enabled: false # 生产环境关闭

🔧 性能优化技巧

1️⃣ 懒加载配置

对于大型项目,可以配置懒加载来提升启动速度:

1
2
3
4
springdoc:
lazy-initialization: true
cache:
disabled: false
2️⃣ 排除不必要的接口

有些内部接口不需要出现在文档中:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@Hidden // 整个控制器都不在文档中显示
public class InternalController {
// 内部接口
}

// 或者单个方法
@GetMapping("/internal")
@Hidden
public String internalMethod() {
return "internal";
}

🌟 总结与展望

写到这里,我不禁感慨万千啊!😌 从最初的手写API文档,到现在的自动化生成,技术的发展真是日新月异!

🎯 核心要点回顾

让我们来回顾一下这篇文章的核心要点:

  1. SpringBoot 3.x的变革:全面拥抱Jakarta EE,最低Java 17,为我们带来了更现代化的开发体验

  2. OpenAPI 3.0的强大:相比2.0版本,3.0在数据类型、认证机制、组件复用等方面都有显著提升

  3. SpringDoc的选择:在SpringFox停止维护的情况下,SpringDoc成为了SpringBoot 3.x时代的最佳选择

  4. 注解的艺术:通过合理使用@Operation@Schema@Parameter等注解,我们可以生成非常专业的API文档

  5. 最佳实践的重要性:统一响应格式、详细参数描述、合理示例使用等实践让我们的API更加优雅

🚀 未来发展趋势

展望未来,API文档的发展还有很多令人兴奋的可能性:

🤖 AI驱动的文档生成
随着AI技术的发展,未来可能会有AI助手自动分析代码逻辑,生成更智能、更自然的API描述。想象一下,AI能理解你的业务逻辑,自动生成贴合场景的示例和说明,那该多酷啊!

🎨 更丰富的交互体验
未来的API文档可能不再是静态的页面,而是像游戏一样的交互式体验。用户可以通过可视化的方式探索API,实时看到数据流转,甚至可以"玩"API!

📱 移动端优先的设计
随着移动开发的普及,API文档也需要考虑移动端的使用体验。未来可能会有专门为移动端优化的文档界面。

🔗 更深度的集成
API文档与开发工具、测试框架、监控系统的集成会越来越深入,形成一个完整的API生态系统。

💝 最后的话

作为一个在技术路上摸爬滚打的老司机,我想对大家说:技术是工具,但态度决定一切!💪

写好API文档不仅仅是为了完成工作,更是对使用者的一种尊重和关爱。当别人使用你的API时能够轻松上手,当团队成员能够快速理解接口逻辑时,那种成就感是无法言喻的!

OpenAPI和SpringBoot的结合,为我们提供了强大的工具,但工具再强大,也需要我们用心去使用。希望这篇文章能够帮助到正在路上的你,让我们一起写出更优雅、更友好的API!🎉

如果你在实际使用过程中遇到任何问题,记住:Google是你的朋友,Stack Overflow是你的老师,而官方文档则是你最可靠的伙伴!当然,如果你有更好的实践经验,也欢迎分享出来,让我们一起进步!

最后的最后,记住一句话:代码是写给人看的,顺便给机器执行! 让我们一起写出更有温度的代码和文档吧!🌈