尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot 3.4整合Swagger与Mybatis-plus实战:版本选型与踩坑

Spring Boot 3.4整合Swagger与Mybatis-plus实战:版本选型与踩坑 Spring Boot 3.4 发布之后我第一次升级手头项目就卡在了 Swagger 上旧的 springfox 依赖直接起不来Mybatis-plus 的 starter 也反复报版本冲突。折腾了两天最后把整套整合方案从依赖到配置重新理了一遍才稳定落地。这篇文章就按我实际操作的顺序把 Spring Boot 3.4 下整合 Swagger实际是 springdoc-openapi和 Mybatis-plus 的完整过程讲清楚包括版本选型、分页插件配置、通用 CRUD 封装、批量写入优化以及几个我踩过的“必炸坑”。如果你是 Java 后端准备升级老项目或者新项目刚开始搭骨架这份配置可以直接抄作业中间的坑也基本能帮你避开。1. 版本选型先把兼容性问题解决掉1.1 为什么 Spring Boot 3.x 必须放弃 Springfox先说结论Spring Boot 3.4 就是不能用 Springfox不是配置问题是底层包名全换了。Spring Boot 3 开始官方把 Jakarta EE 的命名空间从javax.*换成了jakarta.*Springfox 最后维护版本还停留在 Servlet API 3.1 时代里面的类直接引用旧的javax.servlet包在 Spring Boot 3.4 环境里一启动就报NoClassDefFoundError或者Failed to start bean documentationPluginsBootstrapper。替代方案很明确springdoc-openapi。它不是 Swagger 2而是直接实现了 OpenAPI 3 规范内置 Swagger UI只是底层引擎换了一批类。从代码层面看注解从Api、ApiModelProperty变成Operation、Schema刚开始写起来不习惯但用顺了发现信息量更足对泛型、文件流、安全配置的支持也比 springfox 好。我把两个方案的对比整理成了一张表升级或者新建项目可以直接按这个判断对比项springfox-swagger2springdoc-openapiSpring Boot 3.x 支持不支持支持底层规范Swagger 2.0OpenAPI 3.0常用注解Api、ApiModelPropertyOperation、Parameter、SchemaUI 入口/swagger-ui.html/swagger-ui/index.html接口分组配置 DocketGroupedOpenApi Bean官方维护状态停滞持续更新1.2 版本对照表与工程基础配置Spring Boot 3.4 对 Java 版本有硬性要求最低 JDK 17。我本机用的是 JDK 17 Maven 3.9生产环境准备用 JDK 21编译都没问题。如果你还在 JDK 8 或者 11那得先升级 Java 环境否则后面所有依赖都拉不动。当前这套组合我在项目里实测稳定的版本号如下组件推荐版本说明Spring Boot3.4.x当前最新稳定线JDK17 或 21低于 17 无法运行springdoc-openapi2.7.0官方明确适配 Spring Boot 3.4Mybatis-plus3.5.9 及以上必须用 spring-boot3-startermysql-connector-j由 Spring Boot 管理无需写版本号pom.xml 里最核心的依赖就是下面这几段注意 Mybatis-plus 的 artifactId 一定要带spring-boot3老项目里常见的mybatis-plus-boot-starter是给 Spring Boot 2 用的搬到 3.4 上会直接冲突。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-starter-webmvc-ui/artifactId version2.7.0/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.9/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency另外提醒一句不要在这个基础上再引入mybatis-spring-boot-starter那是 Mybatis 官方的 starter和 Mybatis-plus 功能重叠两个一起上会出现 SqlSessionFactory 冲突启动时偶尔能过运行期分页和批量操作都可能出诡异问题。我见过同事不仔细看依赖树踩了这个排查了整整半天。2. Swagger 整合从依赖到完整可访问的配置2.1 最小化配置三步走依赖已经放进去了接下来要让 Swagger UI 能访问其实只需要三处设置。第一步写一个 OpenAPI 配置类把文档标题、版本号、全局鉴权信息配好。这里我用的是OpenAPI这个官方对象替代 Springfox 里的Docket。package com.example.demo.config; import io.swagger.v3.oas.models.Components; 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.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class OpenApiConfig { Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() .title(示例项目 API 文档) .description(Spring Boot 3.4 Swagger Mybatis-plus 整合示例) .version(v1.0.0) .contact(new Contact().name(开发团队).email(devexample.com))) .addSecurityItem(new SecurityRequirement().addList(BearerAuth)) .components(new Components() .addSecuritySchemes(BearerAuth, new SecurityScheme() .name(Authorization) .type(SecurityScheme.Type.HTTP) .scheme(bearer) .bearerFormat(JWT))); } }第二步在 application.yml 里指定 Swagger UI 的访问路径和分组信息。springdoc: swagger-ui: path: /swagger-ui.html operations-sorter: method tags-sorter: alpha api-docs: path: /v3/api-docs第三步启动项目浏览器访问http://localhost:8080/swagger-ui.html能看到 Swagger UI 页面就说明配置成功了。注意 3.x 的 springdoc 默认真实路径是/swagger-ui/index.html但我把springdoc.swagger-ui.path设成了/swagger-ui.html这其实是便于前端和网关记住一个固定的入口重定向到实际页面。2.2 接口分组把管理端和客户端文档拆开项目一大了所有接口挤在一个文档里很难看而且测试同学找接口也费劲。springdoc 的GroupedOpenApi可以把 Controller 按包路径拆成多份独立文档这样 Swagger UI 右上角会多一个下拉框可以切换“管理端”和“客户端”。package com.example.demo.config; import org.springdoc.core.models.GroupedOpenApi; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class GroupedOpenApiConfig { Bean public GroupedOpenApi adminApi() { return GroupedOpenApi.builder() .group(admin) .pathsToMatch(/admin/**) .packagesToScan(com.example.demo.controller.admin) .build(); } Bean public GroupedOpenApi clientApi() { return GroupedOpenApi.builder() .group(client) .pathsToMatch(/client/**) .packagesToScan(com.example.demo.controller.client) .build(); } }分组之后每个组的文档地址变成/v3/api-docs/admin、/v3/api-docs/clientSwagger UI 会自动加载这些分组。这个机制本身不复杂但对前后端协作帮助很大后端只要把不同分组的文档链接发给对应前端就不会出现“接口太多找不到”的情况。2.3 有 Spring Security 时必须做的放行配置如果你的项目引入了 Spring SecuritySwagger 的静态资源和 api-docs 路径默认都会被拦截。3.x 的 SecurityFilterChain 配置方式和 2.x 差别很大直接看代码package com.example.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; Configuration public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf - csrf.disable()) .sessionManagement(session - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth - auth .requestMatchers( /swagger-ui/**, /swagger-ui.html, /v3/api-docs/**, /webjars/**, /favicon.ico ).permitAll() .anyRequest().authenticated() ); return http.build(); } }这里最关键的几个路径是/v3/api-docs/**和/swagger-ui/**漏掉任何一个 Swagger UI 页面都打不开。我见过一种情况是放行了/swagger-ui/index.html但 CSS、JS 静态资源被拦截页面打开后只剩一个白屏检查浏览器控制台全是 403。/webjars/**也别漏Swagger UI 的资源文件放在这里。3. Mybatis-plus 整合插件、分页与通用 CRUD3.1 starter 依赖与数据源配置Mybatis-plus 的依赖在上面的 pom 里已经放好了数据源配置在 yml 里设置。我建议在 JDBC URL 里直接加上rewriteBatchedStatementstrue这个参数对后面讲批量操作优化非常关键提前写进去能省掉后面改配置的事。spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghairewriteBatchedStatementstrue username: root password: 123456 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: banner: false db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0logic-delete-field这段是逻辑删除配置。字段名默认叫deleted如果你的表里叫is_deleted就把logic-delete-field改成对应的属性名。用了逻辑删除后Mybatis-plus 的deleteById会自动转成UPDATE ... SET deleted 1 WHERE id ?而不是真正的 DELETE 语句这个防误删的效果很好建议每个表都加这个字段。3.2 MybatisPlusInterceptor 与分页插件注意事项很多新手直接把PaginationInnerInterceptor注册成一个 Bean然后发现分页不生效其实是注册方式不对。Mybatis-plus 的分页、乐观锁、防全表更新都是通过MybatisPlusInterceptor这个总拦截器串起来的必须把它注册成 Bean再把具体的内置拦截器通过addInnerInterceptor加进去。package com.example.demo.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); pagination.setOverflow(false); interceptor.addInnerInterceptor(pagination); return interceptor; } }setMaxLimit(500L)的意义是防止有人传一个pageSize99999直接把数据库打趴。setOverflow(false)表示超出最大页数时返回空而不是回到第一页这个看业务场景我习惯保持 false让前端更早发现分页参数传错了。分页插件的执行逻辑是当你调用Page作为第一个参数的 selectPage 时它会自动生成一条SELECT COUNT(*)统计总数再生成真正的分页 SQL。注意不要对Page对象手动 set 的searchCount乱赋值默认 true 就是最优的只有在大数据量且明确不需要 total 的场景才考虑关掉 count 查询。3.3 通用 CRUD 服务无状态增删改查的实践Mybatis-plus 自带的IServiceT和ServiceImplM, T已经很强大但实际项目中我们通常还要统一返回结构、统一异常处理。我这里做了一层非常薄的封装把增删改查全做成泛型模板新模块只需要继承基类几乎不用写 CRUD 代码这就对应了热搜里说的“基于 mybatis-plus 实现无状态增删改查”。先看返回结构和分页结构这个后面在 Swagger 文档中也会暴露给前端。package com.example.demo.common; import lombok.Data; Data public class ResultT { private int code; private String message; private T data; public static T ResultT ok(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }再写一个通用 Controller 基类每张表对应的 Controller 继承它之后自动具备 save、delete、update、getById 四个基础能力。package com.example.demo.common; import com.baomidou.mybatisplus.extension.service.IService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; public abstract class BaseControllerS extends IServiceT, T { Autowired protected S service; PostMapping public ResultBoolean save(RequestBody T entity) { return Result.ok(service.save(entity)); } DeleteMapping(/{id}) public ResultBoolean delete(PathVariable Long id) { return Result.ok(service.removeById(id)); } PutMapping public ResultBoolean update(RequestBody T entity) { return Result.ok(service.updateById(entity)); } GetMapping(/{id}) public ResultT get(PathVariable Long id) { return Result.ok(service.getById(id)); } }这里的“无状态”体现在哪就是整个增删改查过程不依赖任何 Session、ThreadLocal 或上下文对象请求带着参数进来方法内部根据参数执行完就返回不残留任何中间状态。状态全部由数据库事务控制天然适合水平扩展。实际业务接口继承这个基类后再补充查询方法即可像这样RestController RequestMapping(/admin/user) public class UserController extends BaseControllerUserService, User { GetMapping(/page) public ResultPageResultUser page( RequestParam(defaultValue 1) long pageNum, RequestParam(defaultValue 10) long pageSize, RequestParam(required false) String status) { PageUser page new Page(pageNum, pageSize); service.page(page, new LambdaQueryWrapperUser() .eq(StringUtils.hasText(status), User::getStatus, status) .orderByDesc(User::getCreateTime)); return Result.ok(PageResult.of(page)); } }4. 接口文档、分页返回与批量写入的协同设计4.1 文档字段别直接暴露数据库实体把 Controller 里返回类型写成数据库实体类确实省事但 Swagger 文档里会把实体所有字段都展示给前端包括deleted、createTime、updateTime这类不应该让前端看的字段甚至某些表里还有内部备注字段直接暴露出去有风险。我的建议是对内管理后台可以直接用实体但对外 API 一定要做 DTO / VO 转换。DTO 上配合Schema注解把字段含义、示例值都写清楚。这不仅是规范问题更是安全边界。package com.example.demo.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; Data Schema(description 用户信息返回对象) public class UserVO { Schema(description 用户ID, example 1) private Long id; Schema(description 用户名, example zhangsan) private String username; Schema(description 昵称, example 张三) private String nickname; Schema(description 状态1启用 0禁用, example 1) private Integer status; }这里有个细节实体里如果用了 Mybatis-plus 的TableField(exist false)标注非表字段在 Swagger 文档中也会正常显示因为 Annontation 解析是从对象属性上拿的。你要是觉得看不到字段不放心可以先去/v3/api-docs看看生成的原生 JSON字段缺失或过多都能直观发现。4.2 统一分页返回结构文档展示更友好Mybatis-plus 的IPage接口直接返回给前端字段名是records、total、size、current、pages结构其实还算清晰。但很多老项目前端已经习惯了自定义格式比如把列表叫list而不是records。为了统一我写了PageResultT进行二次封装。package com.example.demo.common; import com.baomidou.mybatisplus.core.metadata.IPage; import lombok.Data; import java.util.List; Data public class PageResultT { private ListT records; private long total; private long current; private long size; private long pages; public static T PageResultT of(IPageT page) { PageResultT result new PageResult(); result.setRecords(page.getRecords()); result.setTotal(page.getTotal()); result.setCurrent(page.getCurrent()); result.setSize(page.getSize()); result.setPages(page.getPages()); return result; } }封装之后Swagger 文档里展示的分页结构就是固定的前端拿到文档后可以一次性把类型定义写死。重点提示如果你在分页查询时传的是orderBy字符串一定要做白名单校验否则用户传入orderByid;delete from user这类值拼接进 SQL 会有注入风险。Mybatis-plus 的LambdaQueryWrapper都是参数化拼接天然安全所以能用 lambda 写法就不要手动拼 SQL。4.3 Mybatis-plus 批量操作与真实性能热搜词里提到了“mybatis-plus 批量”这个话题实际项目里批量插入、批量更新确实比单条循环快很多但它的快是有前提条件的。Mybatis-plus 的saveBatch默认按 1000 条一批执行底层是调用 JDBC 的addBatch和executeBatch。但注意MySQL 驱动默认情况下并不一定会把这批 SQL 真正的合并执行要拿到真实批量性能必须在 JDBC URL 上带上rewriteBatchedStatementstrue。我在 3.1 节的连接串里已经加上了这个参数这里专门解释原因没有这个参数时MySQL 驱动会把每个批次的 SQL 当成单条语句逐条发送性能提升微乎其微加上之后驱动会把多条 INSERT 重写成一条INSERT INTO ... VALUES (...), (...), (...)网络往返次数大幅下降插入几万条数据从几十秒降到几秒这是我自己压测过的。public boolean batchInsertUser(ListUser userList) { return userService.saveBatch(userList, 2000); }第二个参数可以手动指定分批大小我一般设为 1000 或 2000。分得太小批量优势不明显分得太大单条 SQL 过长MySQL 的max_allowed_packet可能报错。项目里如果是几万条以上的导入还会配合ExecutorType.BATCH使用那个复杂度高一些这里先不展开。批量更新同样可以用updateBatchById但它内部也是逐条生成 UPDATE 语句如果更新大量数据且逻辑相同更推荐先查出主键列表手写一条UPDATE ... WHERE id IN (...)。工具类适合通用场景性能临界点需要自己写 SQL。5. 常见问题与排查实录5.1 启动报错 documentationPluginsBootstrapper 或 jakarta 冲突这是 Spring Boot 3.x 升级时最经典的报错Failed to start bean documentationPluginsBootstrapper看到这个基本可以确定项目里还挂着 springfox。排查思路是先查依赖树看看是显式依赖还是被别的包传递引入的mvn dependency:tree -Dincludesio.springfox:springfox-swagger2找到之后全部排除掉换成 springdoc-openapi。还有一种情况是项目里同时存在javax.servlet-api和jakarta.servlet-api启动时类加载冲突日志表现为各种 servlet 相关 NoClassDefFoundError。解决办法是检查所有第三方依赖把强制指定 javax.servlet 的包排除或升级版本。5.2 Mapper 报 Invalid bound statement 或找不到 MapperMybatis-plus 项目最常见的运行期错误就是Invalid bound statement (not found): com.example.demo.mapper.UserMapper.selectList这个报错有 80% 是mapper-locations配错了。检查一下你的 XML 文件路径和 yml 里的classpath*:/mapper/**/*.xml是否匹配。另一个常见情况是启动类或配置类上没有加MapperScan(com.example.demo.mapper)或者扫描路径写错了包名。如果用了Mapper注解一个个标也能生效但项目大了容易漏。还有一个容易被忽略的点XML 文件如果是用 Windows 记事本编辑后保存为 UTF-8 with BOMMybatis 解析 XML 时第一个字符就是 BOM 头会报Content is not allowed in prolog把文件改成 UTF-8 无 BOM 编码即可。5.3 Swagger 导出 Excel 文件损坏的常见原因热搜里有个“swagger 导出 excel 损坏”这个问题我确实在多个项目里遇到过。现象是接口本身能跑通在浏览器或 Postman 里下载 Excel 完全正常但从 Swagger UI 的“Try it out”下载下来后文件打不开或者下载下来的是一段 JSON 字符串。原因基本就三种接口返回类型写错了、produces没有声明、以及 Swagger UI 对二进制响应的处理机制。第一种错误写法是把 Excel 写进byte[]后直接作为String返回这样 Spring MVC 会按 JSON 序列化客户端拿到的是数组文本完全不是文件字节流。第二种是没有显式声明produces MediaType.APPLICATION_OCTET_STREAM_VALUESpring MVC 有可能内容协商后返回 JSON。第三种是 Swagger UI 下载二进制时的渲染机制。正确做法是用ResponseEntitybyte[]并在响应头里设置Content-Disposition。我验证过一份可以直接用的模板GetMapping(value /export/user, produces MediaType.APPLICATION_OCTET_STREAM_VALUE) public ResponseEntitybyte[] exportUserExcel() throws IOException { byte[] data buildUserExcelBytes(); // 业务方法返回 Excel 字节数组 String fileName URLEncoder.encode(用户列表.xlsx, StandardCharsets.UTF_8) .replace(, %20); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, attachment; filename*UTF-8 fileName) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(data); }如果项目里有的老接口是用voidHttpServletResponse直接往输出流里写文件Swagger UI 里会显示“Response Body: no content”这是正常的但前端同学看到容易以为接口坏了。新代码一律用ResponseEntitybyte[]Swagger UI 上会直接出现 DownLoad 按钮下载行为最稳定。5.4 部署到服务器后访问不到 swagger-ui 的排查开发环境 Swagger UI 正常部署到服务器访问 404这是另一类高频问题。先确认能不能访问/v3/api-docs这个 JSON 接口如果能返回一大段 JSON说明接口层正常问题在 Swagger UI 静态资源或路由。常见原因有几个Nginx 配置没有把/swagger-ui/**转发到后端后端设置了server.servlet.context-path但没有在 yml 里同步调整 springdoc 的路径或者部署在网关后面路由规则把/swagger-ui.html拦截了。我的排查顺序是先 curl 后端服务器的/v3/api-docs通了再看/swagger-ui/index.html。如果是网关转发注意去掉前缀之后再转发给后端比如网关把/demo/swagger-ui/**转发到后端/swagger-ui/**别把/demo也带过去。5.5 分页查询的边界问题分页的坑比较隐蔽我列两个最常见的。第一个是前端页码从 0 开始而后端Page的 current 从 1 开始导致第一页数据永远查不到。统一约定后在后端或前端拦截器里做一次 1 转换并把约定写进接口文档。第二个是total查询在 left join 大表场景下特别慢Mybatis-plus 默认会执行SELECT COUNT(*)如果有多表关联count 语句也会 join 全部表这时候可以手动把 count 优化为只查主表或者单独写一条优化过的 count 语句。第三个容易被忽视的是page.setSearchCount(false)关闭后 total 会变成 0前端分页组件可能不展示总页数。这个要按场景决定不是全局开关。6. 最后把整个方案串起来的一些经验项目升级到 Spring Boot 3.4 之后这套组合我已经在一个内部管理系统和一个小型电商后端上跑过一个多月结论是稳定。真正让我觉得值回票价的不是 Swagger 页面漂不漂亮而是 springdoc 对 OpenAPI 3 的原生支持让接口文档可以直接导入 Apifox、Postman前后端联调用一份文档就够了。我个人的习惯是每加一个 Controller先看一眼/v3/api-docs生成的 JSON确认字段和示例值都没问题再提交代码。另外 Mybatis-plus 的代码生成器建议也配上从数据库表直接生成 entity、mapper、service、controller再继承我上面写的 BaseController新模块的开发工作量能压缩到很小。这套方案后续要继续扩展的话最值得做的方向是引入 Mybatis-plus 的乐观锁插件和字段自动填充这样更新数据和记录 createTime、updateTime 完全自动化Swagger 文档里也不会再出现这些冗余字段。
返回列表