当前位置: 首页 > news >正文

SpringBoot整合MyBatis-Plus开箱即用工程:含分页、代码生成与CRUD示例

本文还有配套的精品资源,点击获取

简介:直接下载就能跑的SpringBoot项目,已集成MyBatis-Plus核心能力——自动配置好MySQL数据源、MyBatis-Plus分页插件(PageHelper兼容模式)、内置代码生成器模板,以及完整的Controller/Service/Mapper/Entity分层结构。项目采用清晰模块命名:主模块gemini_travel负责核心业务逻辑,gemini-thirdparty-travel作为第三方服务扩展参考。pom.xml已声明全部必要依赖,.gitignore已预设标准忽略规则,src目录结构符合SpringBoot官方推荐规范。只需在application.yml里填入你的MySQL地址、端口、用户名和密码,启动Application类即可运行,无需额外修改或调试。支持基础增删改查、多条件动态查询、分页响应(含total/pageSize/current等标准字段)、实体类自动生成(基于数据库表反向生成)。适合Java后端新手快速验证MyBatis-Plus功能,也适合作为中小规模管理后台、内部工具系统的初始脚手架。

1. 为什么这个“开箱即用”工程值得你花5分钟下载并跑起来

我带过不少刚转Java后端的新人,也帮团队搭建过十多个内部管理后台。每次聊到MyBatis-Plus集成,总绕不开一个现实问题:不是学不会原理,而是卡在“第一步就跑不起来”。你照着官网文档配完pom.xml,加了@MapperScan,写了第一个UserMapper extends BaseMapper<User>,结果一启动——Caused by: java.lang.ClassNotFoundException: com.baomidou.mybatisplus.extension.plugins.pagination.PageInterceptor;或者分页查出来total永远是0;又或者代码生成器跑完,entity里全是@TableField(value = "user_name"),但字段映射死活不对……这些不是你能力的问题,是环境配置、版本兼容、模板细节这些“看不见的坑”在拖慢节奏。

这个名为gemini_travel的工程,就是我过去三年反复打磨出来的“防踩坑快启包”。它不讲高深理论,只做一件事:把SpringBoot和MyBatis-Plus之间所有必须手动填平的缝隙,提前用最稳妥的方式焊死。关键词里的“可运行模板”,不是营销话术——它意味着你从解压到看到Started GeminiTravelApplication in 2.342 seconds这条日志,全程不需要打开IDEA的Maven面板点一次“Reload”,也不需要去Stack Overflow搜“PageHelper和MyBatis-Plus分页冲突怎么解决”。它预装了MySQL驱动(8.0.33)、MyBatis-Plus 3.5.3.1(当前3.x系列最稳定的LTS版本)、PageHelper 5.3.2(启用MyBatis-Plus原生分页模式而非PageHelper拦截器),连application.yml里数据库密码都留了占位符${MYSQL_PASSWORD:root},避免你因空密码报错而怀疑人生。

更关键的是结构设计。很多新手模板喜欢把所有东西塞进一个demo模块,结果业务一复杂,包路径乱成毛线团。而这里主模块叫gemini_travel,名字直指业务域(旅行服务),第三方扩展模块叫gemini-thirdparty-travel,命名规则清晰到能直接推导出职责边界——前者管机票酒店订单,后者接航司API或支付网关。这种命名不是为了好看,是我在给三个不同团队做技术评审时,发现模块名含糊的项目,6个月内平均要重构2.3次包结构。所以这个工程里,src/main/java/com/gemini/travel/下只有controllerservicemapperentity四个包,没有utilconfigdto这些过早引入的抽象层,所有配置类都收敛在config包里,且每个类名都带功能后缀,比如MybatisPlusConfig.java只干分页插件注册一件事,CodeGeneratorConfig.java只负责代码生成器初始化。这种克制,恰恰是让新手不迷路、老手不皱眉的关键。

如果你正面临这些场景:想快速验证MyBatis-Plus的LambdaQueryWrapper是否真能避免SQL注入;需要给实习生一个能直接改表、生成代码、调接口的最小闭环;或者你正在为新项目选型,纠结“是自己搭还是用MyBatis-Plus官方脚手架”,那这个工程就是你的答案。它不承诺替代你学习,但绝对能帮你省下至少8小时在环境配置上无意义的消耗。

2. 整体架构设计与核心组件选型逻辑

2.1 模块化分层:为什么主模块叫gemini_travel而不是springboot-demo

看到gemini-thirdparty-travel这个模块名,很多人第一反应是“这不就是个空壳子吗?”。其实不然。这个模块的存在,是整个工程架构设计中最体现实战经验的一环。在真实项目中,第三方服务集成从来不是简单的HTTP调用,它必然带来三类强耦合风险:协议差异(比如航司用SOAP,支付用REST)、异常处理策略不同(超时重试次数、降级开关粒度)、数据模型隔离(第三方返回的FlightInfo不能直接当领域实体用)。如果把这些逻辑全塞进主模块,一旦某家航司接口变更,你得改travel模块的servicedto、甚至controller,测试成本指数级上升。

所以gemini-thirdparty-travel被设计为一个独立的Maven模块,它的pom.xml里只声明了spring-boot-starter-webokhttp3(比RestTemplate更可控的HTTP客户端),绝不依赖主模块的任何业务类。它对外只暴露一个门面接口,比如ThirdPartyFlightService.queryFlights(FlightQueryRequest),内部实现可以是调用SOAP、解析XML、做字段映射,这些细节对主模块完全透明。主模块通过Spring Cloud Alibaba的@DubboReference或简单的@Autowired注入该服务,但编译期不感知其具体实现。这种设计带来的好处是:当你需要替换航司供应商时,只需新建一个gemini-thirdparty-travel-alibaba模块,实现同样的门面接口,然后在application.yml里切换spring.cloud.dubbo.reference.check=false,主模块代码一行不用动。

再看主模块gemini_travel的包结构,它严格遵循DDD的限界上下文思想,但做了轻量化适配。entity包里放的是纯粹的JPA注解实体(@TableName("t_user")),不掺杂任何DTO或VO逻辑;mapper包里每个Mapper接口都继承BaseMapper<T>,且额外定义了selectByCustomCondition这类业务定制方法;service层采用IUserService接口+UserServiceImpl实现类的经典模式,但实现类里禁止出现SQL字符串拼接,所有动态查询必须走MyBatis-Plus的QueryWrapperLambdaQueryWrapper。这种约束不是教条,而是我在处理一个千万级用户表时踩过的坑——某次上线后发现LIKE '%keyword%'查询拖垮数据库,只因一个同事在service里手写了模糊查询语句,没走索引。而LambdaQueryWrapper强制要求你用eq()like()等方法,框架会自动校验字段是否存在、类型是否匹配,从编码阶段就规避了低级错误。

2.2 依赖版本锁定:为什么选MyBatis-Plus 3.5.3.1而非最新4.x

打开pom.xml,你会注意到mybatis-plus-boot-starter的版本号是3.5.3.1,而不是官网上醒目标注的4.3.5。这不是滞后,而是经过生产环境验证后的主动选择。MyBatis-Plus 4.x系列最大的变化是全面拥抱jakarta.*命名空间(取代旧的javax.*),这看似只是包名替换,实则引发连锁反应:如果你的项目还依赖spring-boot-starter-data-jpa(它底层用hibernate-core),而Hibernate 5.6.x仍基于javax.persistence,就会出现java.lang.NoClassDefFoundError: javax/persistence/Entity。我们曾在一个金融后台项目中遇到此问题,升级4.x后,JPA的审计日志功能直接失效,排查三天才发现是命名空间冲突。

3.5.3.1版本则完美兼容Spring Boot 2.7.x(本工程采用)和3.x(需微调),且保留了所有核心生产力特性:LambdaQueryWrapper的类型安全、IService的通用CRUD、MetaObjectHandler的自动填充。更重要的是,它的分页插件PaginationInnerInterceptor已彻底取代旧版PaginationInterceptor,不再需要@Intercepts代理,性能提升约12%(基于JMH压测数据)。pom.xml里还显式声明了mysql-connector-java版本为8.0.33,这是MySQL官方推荐的、与JDK 17兼容性最好的驱动版本。很多新手用8.0.30,结果在Mac M1芯片上启动时报Unable to load authentication plugin 'caching_sha2_password',根源就是驱动版本太旧,不支持MySQL 8.0默认的认证协议。

另一个容易被忽略的细节是lombok的版本。工程里用的是1.18.30,而非常见的1.18.28。这是因为1.18.29版本存在一个致命bug:当实体类同时有@Data@Builder注解时,@Builder生成的build()方法会覆盖@Data生成的toString(),导致日志打印时输出User.UserBuilder@1a2b3c这种无意义字符串。这个问题在排查线上订单状态异常时极其致命——你根本看不到实际的订单对象内容。1.18.30修复了此问题,且与Spring Boot 2.7.x的spring-boot-devtools热部署完全兼容。

2.3 分页方案取舍:为什么弃用PageHelper而用MyBatis-Plus原生分页

application.yml里有一段关键配置:

mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler global-config: db-config: id-type: assign_id table-prefix: t_ pagination: enabled: true limit: 10

注意这里没有pagehelper:开头的配置块,也没有com.github.pagehelper.PageHelper的依赖。这是刻意为之。PageHelper作为老牌分页插件,优势在于兼容各种ORM框架,但与MyBatis-Plus深度集成时,会产生两套分页逻辑打架的问题。典型场景是:你写了一个QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.eq("status", 1);,然后调用userMapper.selectPage(new Page<>(1, 10), wrapper),此时MyBatis-Plus会用自己的PaginationInnerInterceptor拦截SQL,生成SELECT * FROM t_user WHERE status = 1 LIMIT 0, 10;但如果你同时配置了PageHelper,它也会拦截同一SQL,试图做自己的分页计算,结果可能返回空列表或total=0。

本工程采用MyBatis-Plus原生分页,其底层原理是:PaginationInnerInterceptor在SQL执行前,通过Executorquery方法拦截,解析原始SQL,提取SELECT子句,然后用COUNT(*)包裹生成统计SQL(如SELECT COUNT(*) FROM t_user WHERE status = 1),再执行分页SQL。整个过程由MyBatis-Plus统一控制,不存在逻辑冲突。application.yml中的limit: 10是全局默认每页条数,你可以在代码中动态覆盖,比如new Page<>(1, 20)表示每页20条。更关键的是,它返回的IPage<T>对象天然包含totalpagescurrentsizerecords五个字段,前端无需二次封装就能直接响应。对比PageHelper返回的PageInfo<T>,后者需要你手动调用getTotal()getList()等方法,且PageInfo本身不是泛型安全的,容易在复杂嵌套查询中出错。

还有一个隐藏优势:原生分页支持count查询优化。当你的查询条件非常复杂(比如多表JOIN+子查询),MyBatis-Plus允许你通过Page<T>.setSearchCount(false)跳过count查询,直接返回分页数据。这在报表类场景中极为实用——用户只想看第一页的20条数据,没必要为总数去扫描千万行记录。而PageHelper没有提供如此细粒度的控制开关。

3. 核心功能实现与实操要点详解

3.1 数据源自动配置:application.yml的每一行都是经验之谈

src/main/resources/application.yml是整个工程的“心脏起搏器”,它的配置远不止填数据库地址那么简单。我们逐行拆解:

spring: datasource: url: jdbc:mysql://localhost:3306/gemini_travel?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false username: root password: ${MYSQL_PASSWORD:root} driver-class-name: com.mysql.cj.jdbc.Driver hikari: connection-timeout: 30000 max-lifetime: 1800000 maximum-pool-size: 20 minimum-idle: 5 idle-timeout: 600000 leak-detection-threshold: 60000

url参数里的serverTimezone=Asia/Shanghai是必填项,否则MySQL 8.0+会报The server time zone value 'XXX' is unrecognized。这个错误在Linux服务器上尤其常见,因为系统时区和JVM时区不一致。allowPublicKeyRetrieval=true&useSSL=false则是为简化本地开发而设——生产环境必须开启SSL并配置证书,但新手第一次跑通,不该被证书配置劝退。

password使用${MYSQL_PASSWORD:root}语法,这是Spring Boot的占位符默认值机制。意思是:优先读取系统环境变量MYSQL_PASSWORD,如果不存在,则用root作为默认值。这样你既可以在本地用export MYSQL_PASSWORD=123456快速切换,也可以在Docker容器中通过-e MYSQL_PASSWORD=prod123注入,无需修改配置文件。

HikariCP连接池的参数设置,每一条都对应一个真实痛点:
-connection-timeout: 30000(30秒):避免应用启动时因数据库暂时不可用而无限等待。我们曾在线上遇到MySQL主库切换期间,应用卡在连接池初始化,导致K8s健康检查失败,Pod被反复重启。
-max-lifetime: 1800000(30分钟):强制连接最长存活时间,防止数据库端因wait_timeout(默认8小时)主动断开连接,而应用端还拿着失效连接,后续请求全部报Connection reset
-maximum-pool-size: 20:这个值不是拍脑袋定的。根据经验公式:最大连接数 ≈ (核心数 * 2) + 磁盘数,一台4核8G的开发机,磁盘通常是SSD(算1),所以4*2+1=9,取整为20留足余量。生产环境需按QPS压测结果调整。
-leak-detection-threshold: 60000(60秒):开启连接泄漏检测。如果一个连接被getConnection()获取后,60秒内未被close(),HikariCP会打印警告日志,帮你定位try-with-resources忘记写或finally块里close()被异常跳过的Bug。

3.2 代码生成器:如何用30行配置生成可直接投产的代码

gemini_travel模块下的CodeGenerator.java是真正的生产力引擎。它不是简单地生成entity,而是生成一套开箱即用的业务骨架。核心配置如下:

// 1. 全局配置 GlobalConfig gc = new GlobalConfig(); gc.setOutputDir(System.getProperty("user.dir") + "/src/main/java"); // 输出到当前项目src gc.setAuthor("gemini"); // 作者名,会写入entity类注释 gc.setOpen(false); // 生成后不自动打开文件夹 gc.setSwagger2(true); // 启用Swagger注解(@ApiModel, @ApiModelProperty) // 2. 包配置 PackageConfig pc = new PackageConfig(); pc.setModuleName("travel"); // 模块名,影响包路径:com.gemini.travel.entity pc.setParent("com.gemini"); // 父包名 // 3. 策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); // 数据库下划线转驼峰 strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setEntityLombokModel(true); // 用Lombok strategy.setRestControllerStyle(true); // 生成@RestController strategy.setControllerMappingHyphenStyle(true); // URL路径用中划线,如 /user-info/list strategy.setInclude("t_user", "t_order", "t_flight"); // 指定表名,避免生成所有表

最关键的策略是setInclude()——它强制你明确指定要生成的表,而不是用setExclude()排除。原因很简单:新表上线时,你肯定希望第一时间生成代码;而旧表废弃时,删掉对应Java文件比在exclude列表里加一行更直观。setControllerMappingHyphenStyle(true)生成的接口路径是/travel/user-info/list而非/travel/userInfo/list,这符合RESTful API设计规范,且Nginx反向代理时更易配置路由规则(location /travel/user-info/)。

生成的UserController里,你会发现所有方法都标注了@ApiOperation@ApiResponses,这是Swagger2集成的效果。比如listUsers()方法:

@ApiOperation("分页查询用户列表") @ApiResponses({ @ApiResponse(code = 200, message = "成功", response = R.class), @ApiResponse(code = 500, message = "服务器内部错误", response = R.class) }) @GetMapping("/list") public R<IPage<User>> listUsers(@RequestParam(defaultValue = "1") long current, @RequestParam(defaultValue = "10") long size, @RequestParam(required = false) String username) { Page<User> page = new Page<>(current, size); QueryWrapper<User> wrapper = new QueryWrapper<>(); if (StringUtils.isNotBlank(username)) { wrapper.like("username", username); // 注意:这里是数据库字段名,非Java属性名 } IPage<User> userPage = userService.page(page, wrapper); return R.ok(userPage); }

这里有个极易被忽略的细节:wrapper.like("username", username)中的"username"是数据库字段名t_user.username,不是Java实体的username属性。MyBatis-Plus的QueryWrapper默认使用数据库字段名进行条件构建,这是为了兼容@TableField(value = "user_name")这种自定义映射。如果你误写成wrapper.like(User::getUsername, username),会报Property 'getUsername' not found,因为QueryWrapper的lambda模式需要LambdaQueryWrapper

3.3 CRUD示例:从Controller到Mapper的完整链路实录

以用户管理为例,我们追踪一次完整的GET /travel/user-info/list?username=admin请求是如何落地的。

Controller层(UserController.java
接收参数后,构造Page<User>QueryWrapper<User>。注意@RequestParam(defaultValue = "1") long current,这里用long而非int,是因为MyBatis-Plus的Page构造函数要求long,避免类型转换异常。R.ok(userPage)返回的是统一封装的响应体,结构为:

{ "code": 200, "msg": "success", "data": { "total": 152, "pages": 16, "current": 1, "size": 10, "records": [/* User对象列表 */] } }

Service层(UserServiceImpl.java
继承ServiceImpl<UserMapper, User>,这意味着你无需手动实现page()方法,父类已提供。但业务逻辑往往不止于此,比如“查询用户时,需关联查询其最近一笔订单”。这时你在UserServiceImpl里添加:

@Override public IPage<User> pageWithLatestOrder(Page<User> page, QueryWrapper<User> wrapper) { // 1. 先查用户分页 IPage<User> userPage = this.page(page, wrapper); // 2. 提取用户ID列表,批量查订单(避免N+1) List<Long> userIds = userPage.getRecords().stream() .map(User::getId).collect(Collectors.toList()); // 3. 调用订单服务(假设在gemini-thirdparty-travel模块) Map<Long, Order> latestOrderMap = orderService.getLatestOrdersByUserIds(userIds); // 4. 关联填充 userPage.getRecords().forEach(user -> { user.setLatestOrder(latestOrderMap.get(user.getId())); }); return userPage; }

这种写法规避了经典的N+1查询陷阱。如果在User实体里写@TableField(exist = false)List<Order>,然后在XML里用<collection>,MyBatis会为每个用户执行一次SELECT * FROM t_order WHERE user_id = ? ORDER BY create_time DESC LIMIT 1,100个用户就是100次查询。

Mapper层(UserMapper.java
接口定义简洁到极致:

@Mapper public interface UserMapper extends BaseMapper<User> { // 自定义方法,MyBatis-Plus会自动扫描XML或注解 @Select("SELECT * FROM t_user WHERE status = #{status} AND create_time > #{startTime}") List<User> selectByStatusAndTime(@Param("status") int status, @Param("startTime") Date startTime); }

注意@Select里的SQL,#{status}是预编译参数,防止SQL注入;而@Param注解必不可少,否则MyBatis无法将Java方法参数名status映射到SQL里的#{status}。如果你用@Select("...WHERE status = #{arg0}"),虽然能运行,但可读性极差,且重构时参数顺序一变就崩。

Entity层(User.java
@TableName("t_user")明确指定表名,避免MyBatis-Plus按类名User自动转t_user(有些团队习惯用tb_user前缀,必须显式声明)。@TableId(type = IdType.ASSIGN_ID)表示ID由雪花算法生成,无需数据库自增。@TableField(fill = FieldFill.INSERT)标注的createTime字段,在插入时会自动填充当前时间,这依赖于MyMetaObjectHandler配置:

@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0+ this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }

这里用LocalDateTime而非Date,是因为Java 8时间API线程安全且语义清晰;strictInsertFill确保即使你手动设置了createTime,框架也不会覆盖,避免业务逻辑被意外篡改。

4. 常见问题与排查技巧实录

4.1 启动报错大全:从ClassNotFoundException到循环依赖

问题1:Caused by: java.lang.ClassNotFoundException: com.baomidou.mybatisplus.extension.plugins.pagination.PageInterceptor
这是最经典的新手报错。根源是pom.xml里引用了旧版MyBatis-Plus(如3.4.x),而application.yml却配置了新版分页插件。解决方案:
1. 检查pom.xmlmybatis-plus-boot-starter版本是否为3.5.3.1
2. 删除application.yml中所有pagehelper:开头的配置;
3. 确保MybatisPlusConfig.java里注册的是PaginationInnerInterceptor

@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }

注意DbType.MYSQL必须显式指定,不能用DbType.H2DbType.POSTGRE_SQL,否则分页SQL生成错误。

问题2:分页查询total=0,但records有数据
这通常发生在QueryWrapper条件写错时。比如数据库字段是t_user.user_name,你写了wrapper.eq("username", "admin"),而实体类里username字段映射的是user_name,但QueryWrapper默认按Java属性名找字段。正确做法是:
- 方案A(推荐):用LambdaQueryWrapper,类型安全且免拼写错误:

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUsername, "admin"); // 编译期检查,字段名错直接报红
  • 方案B:在QueryWrapper里显式指定数据库字段:
wrapper.eq("`user_name`", "admin"); // 用反引号包裹,避免关键字冲突

问题3:代码生成器运行后,entity类里@TableFieldvalue值为空
这是StrategyConfigsetNaming()setColumnNaming()没配对导致的。必须同时设置:

strategy.setNaming(NamingStrategy.underline_to_camel); // 表名转类名 strategy.setColumnNaming(NamingStrategy.underline_to_camel); // 字段名转属性名

如果只设setNaming(),表t_user会生成User类,但字段user_name仍映射为userName属性,而@TableFieldvalue为空,导致MyBatis-Plus找不到字段映射。

4.2 生产环境避坑指南:那些文档里不会写的细节

坑1:MySQL 8.0+的caching_sha2_password认证插件
本地启动报Public Key Retrieval is not allowed,本质是驱动版本太低。解决方案:
- 升级mysql-connector-java8.0.33
- 或在application.ymlurl里加allowPublicKeyRetrieval=true(仅开发环境);
-生产环境必须用sha256_password插件,并在MySQL中执行:

ALTER USER 'root'@'%' IDENTIFIED WITH sha256_password BY 'your_password'; FLUSH PRIVILEGES;

坑2:Windows下生成的文件路径含中文,导致编译失败
CodeGenerator.javagc.setOutputDir()如果指向D:\我的项目\src\main\java,生成的Java文件路径会含中文,Javac编译时报非法字符。解决方案:
- 将输出目录设为英文路径,如D:/workspace/gemini_travel/src/main/java
- 或在pom.xml里添加编译插件配置:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <encoding>UTF-8</encoding> <source>17</source> <target>17</target> <compilerArgs> <arg>-J-Dfile.encoding=UTF-8</arg> </compilerArgs> </configuration> </plugin>

坑3:多模块项目中,gemini-thirdparty-travel的Bean无法注入主模块
现象是@Autowired ThirdPartyFlightServiceNoSuchBeanDefinitionException。根源是Spring Boot的@ComponentScan默认只扫描主模块包路径。解决方案:
- 在GeminiTravelApplication.java上添加:

@SpringBootApplication(scanBasePackages = {"com.gemini.travel", "com.gemini.thirdparty"})
  • 或更优雅地,在gemini-thirdparty-travel模块的pom.xml里,将<scope>compile</scope>改为<scope>runtime</scope>,然后在主模块的pom.xml里显式声明依赖:
<dependency> <groupId>com.gemini</groupId> <artifactId>gemini-thirdparty-travel</artifactId> <version>1.0.0</version> </dependency>

这样Spring Boot会自动扫描该模块的@Component

4.3 性能调优速查表:让CRUD快10倍的小技巧

场景问题现象解决方案原理说明
大表COUNT(*)慢分页接口响应超时,total查询耗时>2sPage对象上调用setSearchCount(false)跳过COUNT查询,只返回分页数据,适用于“用户只关心第一页”的场景
多条件动态查询N+1查询用户列表时,每个用户触发一次订单查询改用IN批量查询:orderMapper.selectBatchIds(userIds)一次SQL查出所有订单,内存中关联,减少数据库往返次数
JSON字段序列化慢@TableField标注的extra_info(TEXT类型)返回JSON很慢application.yml里添加spring.jackson.serialization.write-dates-as-timestamps=false避免Jackson将LocalDateTime转时间戳再格式化,直接输出ISO格式字符串
Mapper方法过多导致启动慢应用启动时间>10sMybatisPlusConfig.java里添加@MapperScan(basePackages = "com.gemini.travel.mapper")移除所有Mapper接口上的@Mapper注解减少Spring扫描的注解数量,@MapperScan批量注册比单个@Mapper高效

最后分享一个真实案例:我们曾用这个工程搭建一个机票比价后台,初期用PageHelper分页,高峰期total查询拖垮MySQL。切换到MyBatis-Plus原生分页并启用setSearchCount(false)后,接口P99延迟从1200ms降至86ms。这背后没有黑科技,只是把每个配置项背后的“为什么”想清楚了。你现在拿到的,不是一个静态模板,而是一份浓缩了三年踩坑经验的动态说明书。

本文还有配套的精品资源,点击获取

简介:直接下载就能跑的SpringBoot项目,已集成MyBatis-Plus核心能力——自动配置好MySQL数据源、MyBatis-Plus分页插件(PageHelper兼容模式)、内置代码生成器模板,以及完整的Controller/Service/Mapper/Entity分层结构。项目采用清晰模块命名:主模块gemini_travel负责核心业务逻辑,gemini-thirdparty-travel作为第三方服务扩展参考。pom.xml已声明全部必要依赖,.gitignore已预设标准忽略规则,src目录结构符合SpringBoot官方推荐规范。只需在application.yml里填入你的MySQL地址、端口、用户名和密码,启动Application类即可运行,无需额外修改或调试。支持基础增删改查、多条件动态查询、分页响应(含total/pageSize/current等标准字段)、实体类自动生成(基于数据库表反向生成)。适合Java后端新手快速验证MyBatis-Plus功能,也适合作为中小规模管理后台、内部工具系统的初始脚手架。


本文还有配套的精品资源,点击获取

http://www.gsyq.cn/news/1464402.html

相关文章:

  • Qt状态栏别再只显示文字了!手把手教你用QLabel打造带超链接和样式的状态栏(附源码)
  • STK卫星控制句柄获取全攻略:从GetObjectFromPath到Children.Item,新手避坑指南
  • 避开这些坑!软件模拟I2C从机时,你的SCL和SDA中断处理逻辑可能错了
  • 宠物智能喂食器系统设计(设计源文件+万字报告+讲解)(支持资料、图片参考_降重降ai)_文章底部可以扫码
  • 【并购后AI系统兼容性灾难预警】:92%失败案例源于这4类数据语义断层,附诊断清单
  • 真实有效!AI率92%暴降至5%!实测10款AI智能降重工具!免费额度狂薅攻略
  • 从摄像头到麦克风:FFmpeg dshow/avfoundation/v4l2 跨平台音视频采集实战避坑指南
  • 告别时序违例:手把手教你用DC NXT TOPO模式下的compile_ultra优化大型数据路径
  • 2026年泉州管道疏通选对=省心 千里到管道疏通24年老品牌专业推荐 - 本地品牌推荐
  • 别再混淆了!一文搞懂YOLOv3里的置信度、类别概率和Sigmoid函数
  • Serverless 单兵作战:独立产品的云架构冷启动与免运维落地路线
  • Altium Designer绿色报错别头疼,这几个快捷键和叠层设置技巧帮你一键搞定
  • 从‘Hello World’到点亮LED:用Quartus 15.0新建你的第一个FPGA工程(Verilog版)
  • 地面电力巡检机器人系统设计(设计源文件+万字报告+讲解)(支持资料、图片参考_降重降ai)_文章底部可以扫码
  • 用STM32CubeMX的TIM5输入捕获功能,实现一个简易的按键消抖与长按识别(附完整代码)
  • 300Hz舰船噪声信号+MATLAB一键生成LOFAR时频图(含STFT参数预设)
  • 死锁产生条件与诊断:jps、jstack、VisualVM
  • Cartographer纯定位模式启动慢?手把手教你修改源码设置初始位姿,5分钟搞定快速重定位
  • SAP顾问转型记:手把手教你搞定Fiori Launchpad磁贴配置(以Manage Banks为例)
  • 告别漫长等待:Cartographer定位模式下自定义初始位姿的完整配置指南(附源码修改详解)
  • 华为健康数据TCX转换器:3步实现专业运动数据分析
  • 粉笔APP刷题对行测提分有帮助吗?资料分析、判断推理和言语这样练更有效
  • 2026年麻辣烫压面机免和面压面机/全自动压面机/压面机厂家综合对比分析 - 品牌宣传支持者
  • 智能筛选不再黑箱(可解释AI+决策溯源日志):从模型输出到人工复核的全链路审计方案
  • ESP32 GPIO实战:5分钟搞定按键检测与LED控制(附防抖动代码)
  • 别再手动算夹角了!用MATLAB调用STK的向量几何工具,5分钟搞定卫星姿态分析
  • 别再只盯着驻波比了!用VNA实测天线,这3个参数才是调优关键
  • 论文太单薄?资深导师力荐这几个AI论文工具
  • J-Flash设备列表配置详解:以添加华大半导体系列MCU为例,一篇搞定所有型号
  • 面向token编程,一夜百万账单,还能抗的住吗?