说实话,刚接手老项目或者从零搭建微服务接口文档时,很多人对 Swagger 的印象还停留在 swagger-bootstrap-ui 那些老古董上,或者是被 Springfox 2.x 版本的坑折磨得怀疑人生。自从 Springfox 宣布停止维护并转向 SpringDoc OpenAPI (即 Swagger 3.0) 后,很多开发者虽然换了新工具,但在实际落地时依然会遇到一堆“玄学”问题:中文乱码、多版本接口打架、前端接收到的参数校验注解失效导致文档里看不到必填项提示。
今天咱们不聊虚的理论,直接钻进代码里,看看怎么把这些坑一个个填平。我会以一个真实的、稍微有点复杂的业务场景为例,带你一步步把 Spring Boot 和 Swagger 3.0 磨合得像老夫老妻一样默契。
为什么我们要折腾 Swagger 3.0?
首先得明确一点,传统的 Swagger 2.x (Springfox) 是基于反射机制在运行时扫描类结构来生成文档的。这种方式有个致命弱点:它看不懂 Java 8 的新特性,更别提 Lombok 生成的 Getter/Setter 和复杂的泛型校验了。而且,随着 Spring Boot 升级到 2.6+ 甚至 3.x,Springfox 因为依赖库版本冲突,经常直接启动报错。
Swagger 3.0 (SpringDoc OpenAPI) 的出现就是为了解决这些痛点。它基于 OpenAPI 3.0 规范,支持 Spring MVC 和 WebFlux,更重要的是,它能很好地集成 Hibernate Validator,让接口文档里的“必填”、“长度限制”不再是摆设,而是实实在在能展示给前端看的东西。
第一步:清理现场,引入正确的依赖
很多乱码和冲突问题,根源在于依赖包打架。如果你的项目中还残留着 springfox-swagger2 或 springfox-boot-starter,请先把它们统统删掉。
我们需要引入的是 springdoc-openapi-ui。这里有个小技巧:如果你用的是 Spring Boot 2.6+,建议显式指定版本,避免自动版本升级带来的不可控风险。
<!-- pom.xml -->
<dependencies>
<!-- SpringDoc OpenAPI UI (Swagger 3.0) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.7.0</version> <!-- 建议使用稳定版,1.6.9 或 1.7.0 均可 -->
</dependency>
<!-- 数据校验依赖 (JSR 380) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Lombok (简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
注意,这里特意引入了 spring-boot-starter-validation。在 Spring Boot 2.3 之前,这个依赖是包含在 web starter 里的,但之后被移除了。如果不加这个,你的 @Valid 注解可能根本不起作用,文档自然也就读不到参数校验规则。
第二步:解决中文乱码——从根源配置
当你访问 http://localhost:8080/swagger-ui/index.html 时,如果发现 API 描述、字段注释全是问号或者乱码,这通常是因为 Jackson 序列化时的字符集设置问题,或者是 SpringDoc 内部处理字符串时的编码偏差。
最稳妥的办法不是在代码里硬编码 UTF-8,而是在 application.yml 中全局配置 Jackson 的编码行为,同时确保 SpringDoc 的配置类正确加载。
# application.yml
server:
port: 8080
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
# 关键配置:确保序列化时使用 UTF-8,防止中文乱码
default-property-inclusion: non_null
serialization:
write-dates-as-timestamps: false
# SpringDoc 配置
springdoc:
api-docs:
path: /v3/api-docs
enabled: true
swagger-ui:
path: /swagger-ui.html
enabled: true
# 显示标签排序,让文档更有条理
tags-sorter: alpha
# 显示操作排序
operations-sorter: alpha
# 关键:强制使用 UTF-8 编码
packages-to-scan: com.example.demo.controller
如果在某些极端情况下(比如使用了自定义的 MessageConverter),依然有乱码,你可以在配置类中手动指定:
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("用户服务接口文档")
.version("1.0")
.description("这是一个经过精心配置的 Swagger 3.0 示例,解决了乱码问题"));
}
}
真实案例提醒:有一次我在一个旧项目中遇到乱码,排查了半天发现是 Nginx 反向代理配置里漏了 charset utf-8;。所以,如果本地 Swagger 正常但线上访问乱码,记得检查网关层的字符集设置。
第三步:攻克参数校验缺失——让文档“懂”业务
这是最让人头疼的问题。很多开发者写了 @NotNull, @Size 等注解,但 Swagger 文档里依然看不到这些约束。这是因为 Swagger 默认只解析 POJO 的结构,而不深入解析 JSR-303/JSR-380 的校验注解。
SpringDoc 1.6.0 之后增加了一个关键配置:springdoc.model-converters.parameter-converters,默认是开启的,但有时会被其他配置覆盖。我们需要显式地告诉 Swagger:“嘿,请把 Bean Validation 的注解也翻译出来”。
1. Controller 层的优雅写法
假设我们有一个创建用户的接口,参数比较复杂:
@RestController
@RequestMapping("/api/users")
@Tag(name = "用户管理", description = "处理用户相关的 CRUD 操作")
public class UserController {
@Autowired
private UserService userService;
@Operation(summary = "创建新用户", description = "传入用户详细信息,系统将返回新生成的ID")
@PostMapping
public Result<UserVO> createUser(
@Parameter(description = "用户请求体", required = true)
@Valid @RequestBody CreateUserRequest request) {
UserVO user = userService.create(request);
return Result.success(user);
}
}
2. DTO 层加上校验注解(核心)
光有 @Valid 还不够,DTO 类必须带上具体的校验规则。注意,这里使用的是 jakarta.validation (Spring Boot 3) 或 javax.validation (Spring Boot 2)。为了兼容,下面以 Spring Boot 2.x 为例,如果是 3.x 请将 javax 改为 jakarta。
import lombok.Data;
import javax.validation.constraints.*;
import java.util.List;
@Data
public class CreateUserRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2到20个字符之间")
private String username;
@Email(message = "邮箱格式不正确")
@NotBlank(message = "邮箱不能为空")
private String email;
@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "必须年满18岁")
@Max(value = 100, message = "年龄不能超过100岁")
private Integer age;
// 复杂对象中的列表校验
@NotEmpty(message = "角色列表不能为空")
private List<RoleRequest> roles;
}
@Data
class RoleRequest {
@NotBlank(message = "角色名称不能为空")
private String roleName;
private String roleCode;
}
3. 确保 Schema 插件启用
在 application.yml 或配置类中,确认没有禁用模型转换。SpringDoc 默认会启用 BeanValidationParameterConverter。如果你发现依然没有校验信息,可以尝试在配置类中显式声明:
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info().title("User API").version("1.0"))
.components(new Components()
// 这里可以添加额外的组件定义,但通常不需要
);
}
}
关键点:很多时候校验不显示,是因为你直接在 Controller 方法参数上写 @RequestParam 并加校验注解,但 Swagger 对 @RequestParam 的校验注解支持不如 @RequestBody 那么完美。对于查询参数,建议使用 @Schema 注解配合 @Parameter 来补充描述。
@GetMapping("/search")
public Result<List<User>> searchUsers(
@Parameter(description = "用户名关键字", example = "zhangsan")
@RequestParam(required = false) String keyword,
@Parameter(description = "页码", example = "1")
@RequestParam(defaultValue = "1") int page,
@Parameter(description = "每页大小", example = "10")
@RequestParam(defaultValue = "10") int size
) {
// ...
}
第四步:解决版本冲突——多版本 API 共存
在实际工作中,我们不可能只维护一个版本的接口。V1 接口可能还在跑流量,V2 接口已经重构完成。SpringDoc 原生支持多版本 API,通过 groupedPackages 和 pathMapping 来实现。
假设我们有 com.example.v1.controller 和 com.example.v2.controller 两个包。
@Configuration
public class MultiVersionSwaggerConfig {
@Bean
public GroupedApi v1Api() {
return GroupedApi.builder()
.group("v1-api") // 文档分组名
.pathsToMatch("/api/v1/**") // 匹配的路径
.packagesToScan("com.example.v1.controller") // 扫描的包
.build();
}
@Bean
public GroupedApi v2Api() {
return GroupedApi.builder()
.group("v2-api")
.pathsToMatch("/api/v2/**")
.packagesToScan("com.example.v2.controller")
.build();
}
// 如果有更多版本,继续添加 Bean 即可
}
这样配置后,Swagger UI 页面左上角会出现一个下拉框,你可以选择查看 V1 还是 V2 的接口。这比过去那种把不同版本混在一起、全靠 URL 前缀区分的方式要清晰得多。
进阶技巧:如果你希望不同版本的接口共享相同的 DTO 定义,但 Controller 不同,你可以将 DTO 放在公共包中,然后在 packagesToScan 中只扫描 Controller 包,SpringDoc 会自动解析跨包的依赖关系。
第五步:实战中的“隐形”坑与解决方案
1. 泛型导致的类型丢失
Spring Boot 的泛型擦除是个老问题。如果你在返回结果中使用了 Result<T>,Swagger 可能会把 T 显示为 Object。
解决方法:使用 @Schema 注解在 VO 类上明确指定属性,或者在 Controller 方法返回类型上使用 @ApiResponse 明确指定响应内容。
@Operation(
summary = "获取用户详情",
responses = {
@ApiResponse(
responseCode = "200",
description = "成功",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = UserVO.class) // 显式指定实现类
)
)
}
)
@GetMapping("/{id}")
public Result<UserVO> getUser(@PathVariable Long id) {
// ...
}
2. 枚举值的展示
前端非常关心枚举的具体值。默认情况下,Swagger 可能只显示枚举的名字。
解决方法:在枚举类上使用 @Schema 注解。
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "用户状态", example = "ACTIVE")
public enum UserStatus {
@Schema(description = "激活状态")
ACTIVE,
@Schema(description = "冻结状态")
FROZEN,
@Schema(description = "注销状态")
DELETED
}
3. 文件上传的参数校验
文件上传通常涉及 MultipartFile,这部分在 Swagger 中容易显示不全。
@PostMapping("/upload")
@Operation(summary = "上传头像")
public Result<String> uploadAvatar(
@Parameter(description = "头像文件", required = true)
@RequestParam("file") MultipartFile file,
@Parameter(description = "用户ID", required = true)
@RequestParam("userId") Long userId) {
if (file.isEmpty()) {
throw new BusinessException("文件不能为空");
}
// ...
}
确保 springdoc.api-docs.enabled=true,并且在 application.yml 中没有禁用 form-data 的支持。SpringDoc 默认支持 multipart/form-data,只要你的参数标记了 @RequestParam,它就会在文档中渲染为一个 File 类型的输入框。
总结:如何让文档真正“活”起来
整合 Swagger 3.0 不仅仅是引入一个 jar 包那么简单,它更像是一次对代码规范的梳理。
- 乱码问题:靠的是全局 Jackson 配置和 Nginx 层面的字符集统一,不要试图在 Java 代码里打补丁。
- 校验缺失:靠的是
spring-boot-starter-validation的正确引入,以及 DTO 类上扎实的javax/jakarta.validation注解。记住,Controller 里的@Valid只是触发器,真正的规则定义在 DTO 里。 - 版本冲突:靠的是
GroupedApi的灵活拆分,让不同团队的接口文档隔离又共存。
最后,我想说,一份好的接口文档,应该是开发者和前端之间的“契约”,而不是事后补写的“说明书”。当你习惯了在写代码的同时,通过 @Operation 和 @Schema 来描述业务意图时,你会发现,Swagger 不再是一个负担,而是一个强大的沟通工具。
现在,重启你的 Spring Boot 应用,打开 http://localhost:8080/swagger-ui.html,看着那些清晰的中文、准确的校验规则和分门别类的接口,你应该能感受到一种秩序之美。如果有遇到其他奇怪的问题,欢迎随时回来探讨,毕竟,踩过的坑越多,技术底子越厚实。