这件事我得先讲个背景。前阵子帮某朋友的旧项目做升级,技术栈从 Spring Boot 2.7 直接跳到 3.4,本来以为只是换几个依赖版本的事,结果文档工具和 ORM 这一层就卡了两天。网上随便一搜,十有八九还是 Springfox 配 Boot 2 的老教程,照着配要么启动直接报错,要么 Swagger 页面 404。真正动手把 Spring Boot 3.4、Swagger(准确说是 OpenAPI 3 的 springdoc 实现)和 MyBatis-Plus 组合跑通后,回头梳理了一下,踩的坑其实高度集中,完全能整理成一份可复用的清单。
这篇文章就围绕这个组合展开:版本怎么选、依赖怎么引、配置怎么写、哪些老经验已经完全作废,以及真正上线后会遇到的几个隐藏问题。如果你是正在升级到 Spring Boot 3.4,或者新建项目想一步到位,这份内容可以直接照着抄,最后再把我实际遇到过的故障排查记录一并列给你。
1. 版本选型:先搞懂 3.4 和旧版生态的适配关系
1.1 3.x 到底改了什么:Jakarta 生态与 Springfox 停更
Spring Boot 3.4 是 3.x 系列里的一个常规版本,但它的基础要求是 JDK 17 起跳,同时全面切换到了 Jakarta EE 规范。这意味着所有javax.*开头的包名都变成了jakarta.*。依赖层面的 servlet、validation、persistence 全部受影响,如果你还在用老的javax.servlet、javax.validation做自定义封装,编译阶段就会直接红一片。
比这个更隐蔽的是文档工具生态的变化。很多老教程里推荐的 Springfox 3.0.0,项目维护实际已经停摆多年,它对 OpenAPI 3 规范的支持支离破碎,而且明确没有适配 Spring Boot 3.x。Springfox 内部的springfox-boot-starter在 Boot 3 下会因为扫描旧的 spring-plugin 机制和springdoc重复定义/v3/api-docs冲突,启动时直接报BeanDefinitionStoreException这类错误。所以结论很明确:Spring Boot 3.x 项目想用 Swagger 能力,必须转向 springdoc-openapi 项目,它才是当前社区对 OpenAPI 3 支持最完整、维护最活跃的开源实现。
这意味着你之前写的 Springfox 配置类、DocketBean 全都要废弃。思想可以保留——比如按分组生成不同业务的文档——但代码必须迁移到 springdoc 的模式上,后面会详细说。
1.2 直接可用的版本矩阵
先给出一份我实测下来能稳定工作的版本组合。Spring Boot 3.4 刚刚发布时,配套生态有些滞后,但现在主流的几个库都已经跟上了:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 17 或 21 | 3.4 官方基线,21 更好 |
| Spring Boot | 3.4.x | 父 POM 管理即可 |
| springdoc-openapi | 2.7.0(或更高 2.x) | 使用 webmvc starter |
| mybatis-plus | 3.5.7+ | 必须用 boot3 starter |
| MySQL 驱动 | 由 Boot BOM 管理 | 使用com.mysql:mysql-connector-j |
| Lombok | 由 Boot BOM 管理 | 不需要单独指定 |
需要特别留意的是 MyBatis-Plus 的 starter 命名。旧项目用的mybatis-plus-boot-starter只适配 Spring Boot 2.x,Boot 3 项目要引入的是mybatis-plus-spring-boot3-starter。从 3.5.5 开始这个 boot3 专用 starter 成为稳定选择,3.5.7 之后对 Spring Boot 3.3/3.4 的兼容性已经比较干净。
另外补充一个 2024 年后比较容易踩的新坑:如果你的 mybatis-plus 版本较新(3.5.9+),分页插件依赖的jsqlparser被拆成了独立模块mybatis-plus-jsqlparser,只引入 starter 会导致分页拦截器运行时找不到类。我在后面常见问题表格里会再列一次。
1.3 为什么不推荐继续用 Springfox
除了技术上的不兼容,Springfox 还有一个很实际的问题:它生成的文档基于 OpenAPI 2.0(Swagger 2),到今天的前端工具链、代码生成器大多已经默认使用 OpenAPI 3.0+。你继续用 Springfox,等于把整个团队的 API 契约停留在上一个年代,后续接入接口自动化测试、客户端代码生成都会非常别扭。
当然,如果你是从 Spring Boot 2.6/2.7 项目升级过来,老代码里可能已经积累了大量 Springfox 注解,比如@Api、@ApiOperation、@ApiParam。springdoc 并不直接兼容这些注解,你需要批量替换为@Tag、@Operation、@Parameter。不要试图用双依赖兼容的方式过渡,Springfox 和 springdoc 同时躺在 classpath 里几乎必炸。老老实实写一个简单脚本做替换,成本远低于排查诡异冲突。
2. Swagger(OpenAPI 3)整合实操:从依赖到可用文档页
2.1 springdoc 依赖与默认访问路径
在pom.xml里只需要引入一个依赖,剩下交给 Spring Boot 自动配置。以 Maven 为例:
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.7.0</version> </dependency>如果你的项目是 WebFlux,那就用springdoc-openapi-starter-webflux-ui,名字只差一个字母,但绝不能混装。引入依赖后,Spring Boot 3.4 启动时会自动装配文档端点,无需任何额外代码就能访问:
- OpenAPI 规范 JSON:
http://localhost:8080/v3/api-docs - Swagger UI 页面:
http://localhost:8080/swagger-ui.html
这个默认行为已经足够日常联调用,但真实项目几乎不会满足于“能打开”。你可以通过application.yml把页面调得更顺手:
springdoc: api-docs: enabled: true path: /v3/api-docs swagger-ui: path: /swagger-ui.html display-request-duration: true tags-sorter: alpha operations-sorter: alpha packages-to-scan: com.example.demo.controller paths-to-exclude: /error,/actuator/**packages-to-scan和paths-to-exclude这两个配置强烈建议写上。不写的话,springdoc 会扫描整个 Spring 容器里所有@RestController,包括 Spring Boot 内部的BasicErrorController、各种 Actuator 端点,文档里会冒出很多你没写过的接口,前端看着一头雾水。限包扫描属于文档整洁的第一步。
2.2 定制文档信息与鉴权配置
默认的文档标题是OpenAPI definition,页面顶栏显示的是v3/api-docs这种原始路径,毫无项目辨识度。自定义信息只需要一个OpenAPIBean:
@Configuration public class OpenApiConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() .title("某系统 API 文档") .description("这是 Spring Boot 3.4 + springdoc 的接口文档") .version("v1.0.0") .contact(new Contact() .name("后端开发组") .email("dev@example.com"))) .components(new Components() .addSecuritySchemes("BearerAuth", new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT"))); } }如果项目里接了 Spring Security,UI 右上角的 Authorize 按钮就需要这个 SecurityScheme 来支撑。填好之后,用户可以在 Swagger 页面点授权按钮,输入Bearer xxx后,每个请求自动带上 Authorization 头,调试带鉴权的接口方便很多。
不过这里有个容易漏的点:光定义 SecurityScheme 不够,默认不会全局应用。你需要在每个需要鉴权的@Operation上声明,或者在类上配置@SecurityRequirement(name = "BearerAuth")。想全局生效,可以通过动态修改 Spring 容器里的OpenAPI实例,逐个给接口追加安全要求,这块代码稍复杂,但值得为团队沉淀。
2.3 常用注解与多环境开关
springdoc 的核心注解和 Springfox 时代差别很大,我列一个对应关系,方便老项目迁移:
| Springfox 旧注解 | springdoc 新注解 | 用途 |
|---|---|---|
@Api | @Tag | 标注 Controller 分类 |
@ApiOperation | @Operation | 标注接口方法 |
@ApiParam | @Parameter | 标注参数 |
@ApiModel | @Schema | 标注模型类 |
@ApiModelProperty | @Schema(description=...) | 标注模型字段 |
实际项目中我的习惯是:Controller 类加@Tag(name = "用户管理", description = "用户相关接口"),接口方法加@Operation(summary = "分页查询用户列表", description = "按条件分页查询")。模型字段加@Schema(description = "用户姓名")。这些注解即文档,比维护 Word 文档高效得多。
生产环境关闭文档是另一个容易被忽略的点。直接在 yaml 中设置:
springdoc: api-docs: enabled: false swagger-ui: enabled: false但我不建议把关闭逻辑写死在同一个配置里。更稳妥的做法是用 profile 区分:开发环境application-dev.yml打开,生产环境application-prod.yml关闭。如果你的 Spring 配置类里有OpenAPIBean,可以给它加@Profile("dev"),Spring Boot 3.4 的配置优先级完全支持这种切换。文档工具本质是内部开发资源,没必要暴露到公网,即使要暴露也不要裸奔在生产环境。
2.4 文档分组与限扫范围
很多项目是单体应用里塞了管理端、用户端、开放平台等多套接口,揉在一个文档里很痛苦。springdoc 原生支持分组:
springdoc: group-configs: - group: admin packages-to-scan: com.example.demo.controller.admin - group: app packages-to-scan: com.example.demo.controller.app这样 UI 顶部会出现下拉选择分组,管理端和用户端接口完全隔离。比起用@Tag做软分类,分组能从物理扫描路径上隔离,文档页面的响应速度和可读性都更好。
3. MyBatis-Plus 整合实操:从依赖到分页与自动填充
3.1 依赖坐标与老项目迁移注意
在 Spring Boot 3.4 里引入 MyBatis-Plus,依赖坐标已经是 boot3 专用版:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> </dependency>数据源直接由 Spring Boot 自动配置接管,默认用 HikariCP,不需要额外引连接池。如果要用 MySQL,按 Boot BOM 版本引入com.mysql:mysql-connector-j即可。注意不要在项目里同时出现mybatis-plus-boot-starter和mybatis-plus-spring-boot3-starter两个依赖,否则会冲突到怀疑人生。
启动类上记得加@MapperScan("com.example.demo.mapper"),不加的话,你的 Mapper 接口不会被扫描注册,启动不报错,但一调用就注入失败。另外一个不起眼但很重要的配置是 SQL 日志,开发阶段看不到 SQL 就没法调试:
mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl生产环境记得改回org.apache.ibatis.logging.nologging.NoLoggingImpl,否则 SQL 会全量打印到日志文件,性能上是个隐患。
3.2 分页插件配置:为什么必须手动注册
MyBatis-Plus 的分页能力依赖于拦截器,但它不会自动注册,这是无数人翻车的地方。很多人写完了Page<User>查询,发现返回的total永远是 0,或者压根没拼 LIMIT 语句,根源就是拦截器缺失。
正确做法是建立一个配置类:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }DbType一定要和你当前数据库一致。MySQL 写成DbType.MYSQL,PostgreSQL 写成DbType.POSTGRE_SQL。如果你在分页插件里配错了类型,SQL 方言生成就会出问题,轻则分页语句别扭,重则直接语法错误。
这里再强调一次新版依赖拆分问题。如果你用的 mybatis-plus 是 3.5.9 之后的版本,且刚才启动没有报错但分页不生效,检查 classpath 里是否有mybatis-plus-jsqlparser。新版本把分页拦截器依赖的 jsqlparser 拆出去了,需要额外引入:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-jsqlparser</artifactId> <version>3.5.7</version> </dependency>这个坑在官方升级文档里写得很清楚,但不少人是踩了NoClassDefFoundError之后才回头看的。
3.3 自动填充与逻辑删除
真实业务表几乎都有create_time、update_time这两个字段。MyBatis-Plus 的自动填充能省掉大量手动赋值代码。实体类上先声明字段:
@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;再写一个填充处理器:
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }需要注意strictInsertFill方法名和你实体字段名要完全一致,大小写都不能错。如果你字段名是create_time但 Java 属性是createTime,这里写的是属性名而不是列名。我曾经在一次项目重构中把实体字段改成createdAt,忘了同步处理器,结果所有 insert 都少了创建时间,排查半天。
逻辑删除也属于高频配置。在 yaml 中宣告全局逻辑删除字段后,所有 select 都会自动带上WHERE deleted = 0:
mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这个配置同样会影响 Swagger 文档:如果你的实体里暴露了deleted字段,前端会看到这个内部标记,建议在返回给前端的 DTO 上直接去掉此类字段,或者加@Schema(hidden = true)隐藏。
3.4 代码生成器与实体设计建议
MyBatis-Plus 官方代码生成器在 3.5.x 里依然可用,但它和你当前项目里引入的mybatis-plus-spring-boot3-starter实际上是两个模块,需要单独引入mybatis-plus-generator。如果你只是写几个表,手写实体反而更快;表多到十几张以上,用生成器能省不少时间。
整体设计建议是:数据库表对应的实体类不要直接对外返回。我见过很多项目直接把User实体放到 Controller 返回值里,MyBatis-Plus 的字段注解、逻辑删除、填充等元信息全暴露给了前端,Swagger 文档也会把这些内部字段渲染出来,非常难看。合理的做法是单独建 VO/DTO 层,Controller 和 Service 之间通过 DTO 传递,只在最外层返回给前端时才组装 VO。这个分层在集成 Swagger 时收益特别明显,文档干净程度完全不同。
4. 两者一起用时最容易踩的序列化坑
4.1 Page 入参在文档里怎么处理
当你在 Controller 里接收分页参数时,最常见的是直接用Page<T>作为查询入参:
@GetMapping("/page") @Operation(summary = "分页查询用户") public Result<Page<UserVO>> page(@ParameterObject Page<UserQuery> page) { return Result.ok(userService.pageUser(page)); }这里必须注意@ParameterObject注解。不加的话,springdoc 默认可能把Page当作一个复杂模型,要求前端以 JSON body 传入,但 MyBatis-Plus 的Page实际上是用于绑定 Query 参数的,不加这个注解,前端用 query 方式传current=1&size=10也能跑,但 Swagger UI 里的参数展示会一团糟。加了@ParameterObject后,Page的current、size等属性会展开成独立的 query 参数,UI 上可以直接操作。
Page自带的orders、optimizeCountSql、searchCount也会一并显示出来。这些属于框架内部参数,前端其实不太需要关心。如果文档洁癖比较严重,建议自己定义一个轻量分页入参对象,只包含current、size两个字段,再在 service 层转换成Page对象。
4.2 Page 返回对象在 Swagger 中的泛型问题
这是集成后最容易让前端吐槽的点。假设你返回的是Result<Page<UserVO>>,springdoc 确实能解析出records是UserVO[],但Page本身继承和实现了一堆框架层面的字段,比如total、pages、current、size、orders、optimizeCountSql、optimizeJoinOfCountSql、maxLimit、searchCount等等,这些全都会被渲染到 Swagger 文档里。
解决办法有两种。第一种是接受这些字段存在,但给核心字段补上@Schema(description = "...")说明,前端通过注释理解。第二种是干脆不用Page作为返回类型,定义一个干净的分页 DTO:
@Schema(description = "分页返回结果") public class PageResult<T> { @Schema(description = "当前页码") private long current; @Schema(description = "每页条数") private long size; @Schema(description = "总记录数") private long total; @Schema(description = "数据列表") private List<T> records; }在 Service 层把Page<User>转换成PageResult<UserVO>,虽然多几行代码,但文档干净很多。我在真实项目中更推荐第二种,因为分页结果本质只需要那四个字段,框架内部字段对调用方全是噪音。这个思路配合统一返回体Result<T>一起用,文档基本就是标准的“业务码 + 消息 + 数据”结构。
4.3 统一返回体设计与文档可读性提升
团队协作时,接口返回结构不统一是最影响效率的事。强烈建议所有接口都走同一个返回类型:
@Schema(description = "统一响应体") public class Result<T> { @Schema(description = "业务状态码") private int code; @Schema(description = "提示信息") private String message; @Schema(description = "响应数据") private T data; }springdoc 对泛型返回类型的解析能力相当好,Result<PageResult<UserVO>>能正确渲染出嵌套结构。前端拿到文档后,点开任意接口都能看到完整的数据结构,配合@Schema注解,基本不需要再找后端问“这个字段什么意思”。这里有个细节:泛型占位符命名有讲究。如果写的是Result<T>,文档里会显示T,写得太随意会影响可读性;建议直接用T但通过@Schema描述清楚,或者给泛型起名T即可,springdoc 会正确替换为实际类型。
5. 常见问题速查与性能调优实录
5.1 实际问题速查表
以下是我在实践过程中遇到或者身边团队踩过的典型问题,整理成表格方便快速定位:
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
/swagger-ui.html返回 404 | 依赖未引入;或路径被 Spring Security 拦截 | 检查 springdoc starter;在 Security 放行/v3/api-docs/**、/swagger-ui/** |
| 页面打开但显示空分组 | packages-to-scan路径写错 | 核对包名,Controller 是否在该包下 |
启动报BeanDefinitionStoreException | Springfox 和 springdoc 同时存在 | 移除 springfox 依赖及相关配置类 |
分页total始终为 0 | 未注册PaginationInnerInterceptor;或DbType配错 | 补配置类,核对数据库类型 |
分页运行时报NoClassDefFoundError: jsqlparser | mybatis-plus 3.5.9+ 缺少独立 jsqlparser 模块 | 引入mybatis-plus-jsqlparser |
| 自动填充字段不生效 | 填充字段名写错;处理器没被扫描 | 核对属性名;确认@Component已加 |
| 逻辑删除不生效 | 全局配置未声明logic-delete-field | 补 yaml 配置,实体加注解 |
接口文档里出现/error、/actuator等内部端点 | 扫描范围过广 | 配置paths-to-exclude和packages-to-scan |
| 数据库 SQL 日志无法输出 | 未配置log-impl | 开发环境设为StdOutImpl |
| Swagger 页面无法调带鉴权接口 | 缺少 SecurityScheme;或 Authorization 头未全局应用 | 定义 SecurityScheme;在文档 UI 中手动授权 |
这个表格里最容易被忽略的是第三行和第六行。第三行的 Springfox 残留问题在升级项目里几乎必现,很多老项目还有自定义的@Configuration类里注入了Docket,升级时只删了依赖没删代码,结果 springdoc 的自动配置被旧配置干扰;第六行的字段名问题,NPE 不会出现,但数据表里就是没有值,属于特别难定位的静默故障。
5.2 启动变慢与生产环境关闭
集成 springdoc 后,启动时间变长是正常现象,因为应用启动时要扫描所有 Controller 和方法反射信息来构建文档。项目包数量一大,这种扫描耗时就会变明显。我这里有几个优化方向:
第一,缩小扫描范围。前面讲的packages-to-scan不只为了文档整洁,更是性能的刚需。如果你不限制,springdoc 会扫描所有带@RestController的类,哪怕那个 Controller 在某个无关的 jar 包里。第二,生产环境直接关闭文档端点,收益最直接。第三,如果你们的接口特别多,比如几千个,可以考虑把文档按业务拆组,页面加载速度会快很多。
另外提一个和 Spring Security 联动的常见场景:如果你设置了所有请求必须鉴权,但没有放行/v3/api-docs/**和/swagger-ui/**,那么即使开发环境开着 springdoc,前端也打不开文档页。需要在 Security 配置里加白名单。做到这里,Swagger UI 才能真正成为团队日常协作的入口。
5.3 升级遗留项目的迁移要点
如果你是从老的 Spring Boot 2.x 项目直接升级,不要指望一把梭。我建议按这个顺序操作:
先升级 Spring Boot 到 3.4,保证项目在没有任何 Springfox 和 mybatis-plus 老依赖的情况下能启动。这一步主要解决javax.*到jakarta.*的包替换、SDK 版本、以及第三方库兼容。然后引入 springdoc 依赖,把 Controller 上的@Api系列注解批量替换为@Tag系列。替换后立即打开/swagger-ui.html确认接口数量和参数展示。接着再换 MyBatis-Plus 依赖,从mybatis-plus-boot-starter换成mybatis-plus-spring-boot3-starter,补上分页插件和全局配置。最后再处理业务代码里的Page返回类型和文档字段显示问题。
之所以把 Springfox 替换放在 MyBatis-Plus 之前,是因为文档相关的报错更直观、更容易定位。MyBatis-Plus 的兼容问题往往把异常藏在运行时,比如分页 SQL 生成错误、字段填充异常,排查成本高。分开做,每个阶段的问题边界清晰,不至于两个库的报错混在一起。
最后说一个我自己的习惯:无论项目多赶,Spring Boot 3.4 项目里我都要求团队在 Controller 层坚持写@Operation(summary = ...)和@Tag(name = ...)。这两个注解的成本几乎为零,但受益者是整个团队,尤其是前端。除此之外,在application.yml里把所有能限的扫描范围限住,把生产环境的文档关闭,把PageResult这种轻量返回对象用起来,这套组合稳定跑下来后,我几乎没再为接口文档和数据访问问题操过心。