Spring Boot整合MyBatis实战:动态SQL安全、MyBatis-Plus高效操作与生产级配置详解

发布时间:2026/7/29 5:08:38

Spring Boot整合MyBatis实战:动态SQL安全、MyBatis-Plus高效操作与生产级配置详解
1. 项目概述为什么“常用”二字值得深挖接手一个Spring Boot项目数据访问层十有八九会用到MyBatis。网上关于“Spring Boot整合MyBatis”的入门教程铺天盖地教你配个依赖、写个Mapper注解、跑通一个UserMapper的selectById就算大功告成。但当你真正开始写业务代码面对复杂的查询条件、动态的排序字段、需要批量操作的场景或者被安全扫描工具突然报出一个SQL注入漏洞时才会发现仅仅“整合”是远远不够的。真正的战场在于那些“常用”的、高频的、却又暗藏玄机的使用场景。这篇内容我们就跳过“Hello World”式的整合直接切入实战。聚焦在Spring Boot与MyBatis日常开发中那些你一定会遇到处理不好就可能埋下隐患的“常用”操作。比如动态SQL到底该用${}还是#{}为什么奇安信扫描会报警MyBatis-Plus的“存在则更新不存在则插入”到底是怎么实现的XML里如何优雅地处理ListString参数这些问题的背后不仅仅是API的调用更是对MyBatis原理、SQL安全、以及Spring Boot生态整合深度的理解。我结合自己这些年趟过的坑把这些高频、实用的知识点梳理出来目标很明确让你在项目中用MyBatis时不仅能用更能用得稳、用得安全、用得高效。2. 核心依赖与基础配置再审视很多教程止步于引入spring-boot-starter-web和mybatis-spring-boot-starter。但在一个追求稳定和效率的项目中我们需要看得更细。2.1 依赖选型稳定版与功能增强对于生产环境我强烈建议锁定稳定版本而不是使用latest。以当前主流版本为例parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version !-- 选用一个长期支持版本 -- relativePath/ /parent dependencies dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version !-- 分页插件非常常用 -- /dependency !-- 数据库驱动根据实际情况选择 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependencies注意pagehelper是一个极其常用的分页插件它能以极简的方式实现物理分页避免内存分页的性能问题。引入它的starter可以免去大量XML配置。2.2 配置文件中的关键项application.yml里的配置远不止一个url和password。以下几个配置直接影响着开发体验和运行时行为mybatis: # 重要指定mapper.xml文件的位置。如果和Mapper接口在同一目录可省略。 # 但通常建议分离利于管理和打包。 mapper-locations: classpath:mapper/*.xml # 重要配置类型别名包这样在xml里就可以用类名代替全限定名。 type-aliases-package: com.yourdomain.project.entity configuration: # 开启驼峰命名自动映射。数据库字段是user_name实体类是userName靠这个实现自动匹配。 map-underscore-to-camel-case: true # 打印执行的SQL语句及其参数。开发环境必开生产环境务必关闭 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 全局性地启用二级缓存需要实体类实现Serializable。通常按需在mapper.xml中配置。 # cache-enabled: true # PageHelper分页插件配置 pagehelper: helper-dialect: mysql # 指定数据库方言 reasonable: true # 分页参数合理化。pageNum0时查询第一页pageNum总页数时查询最后一页。 support-methods-arguments: true # 支持通过Mapper接口参数来传递分页参数这里有个实操心得关于mapper-locations。如果你的项目结构是Maven标准的src/main/resources那么XML文件放在resources/mapper/下是安全的。但如果你把XML放在src/main/java的某个包内必须确保Maven的pom.xml中配置了resources将.xml文件包含进编译输出否则打包后会找不到XML导致Invalid bound statement异常。我个人的习惯是始终将XML放在resources目录下清晰分离。3. 动态SQL安全与灵活的博弈动态SQL是MyBatis的灵魂功能也是安全漏洞的重灾区。热搜词里提到了“mybatis 动态sql 使用${}”和“奇安信安全扫描报sql注入漏洞”这两者直接相关。3.1#{}与${}的本质区别这是MyBatis面试的必考题但很多人只知其然。#{}预编译占位符MyBatis会将其替换为JDBC的?然后使用PreparedStatement来赋值。这个过程会对传入的参数进行转义和处理从根本上防止了SQL注入。它适用于传入值的场景比如where id #{id}。${}字符串拼接MyBatis会将其内容直接替换到SQL语句中是纯粹的字符串拼接。如果替换的内容来自用户输入且未经验证就会导致SQL注入。它适用于传入SQL语句片段的场景比如动态表名、动态排序字段。3.2 动态SQL标签的安全使用MyBatis提供了if,choose,where,set,foreach等标签来构建动态SQL。安全示例多条件查询select idselectByCondition resultTypeUser SELECT * FROM user where !-- 使用 #{} 安全地传入值 -- if testusername ! null and username ! AND username LIKE CONCAT(%, #{username}, %) /if if teststatus ! null AND status #{status} /if if testroleIds ! null and roleIds.size() 0 AND role_id IN foreach collectionroleIds itemroleId open( separator, close) #{roleId} !-- 这里也必须用 #{} -- /foreach /if /where !-- 动态排序此处是${}的合理使用场景但必须确保orderField是白名单 -- if testorderField ! null and orderField ! ORDER BY ${orderField} if testorderDirection ! null and orderDirection ! ${orderDirection}/if /if /select危险示例导致SQL注入!-- 错误直接将用户输入拼接到LIKE中 -- if testkeyword ! null AND username LIKE %${keyword}% /if !-- 攻击者可以传入keyword值为 OR 11导致整个WHERE条件被绕过 --重要提示对于${}的使用必须建立严格的白名单机制。例如对于动态排序字段orderField在Java代码中先进行校验只允许“create_time”、“username”等有限的几个字段绝不能直接将前端传过来的字符串未经处理就放入${}中。奇安信等安全扫描工具核心就是检测这种直接将参数拼接到SQL中的模式。3.3 处理ListString等集合参数热搜词里有“mybatis xml传list 参数写法”这非常常见。关键在于foreach标签的collection属性。接口方法ListUser selectByUsernames(Param(nameList) ListString usernames);XML映射select idselectByUsernames resultTypeUser SELECT * FROM user WHERE username IN foreach collectionnameList itemname open( separator, close) #{name} /foreach !-- 判断集合是否为空可以在Java层做也可以在XML层做 -- if testnameList null or nameList.size() 0 AND 10 !-- 如果集合为空添加一个永假条件避免语法错误 -- /if /select注意事项collection的值“nameList”必须与Param注解指定的名称一致。如果不使用Param当参数只有一个集合时collection可以写为“list”对于List或“array”对于数组但为了清晰我强烈推荐始终使用Param。在foreach内部使用#{item}来引用当前元素同样是安全的预编译方式。对于空集合的处理上述AND 10是一种方式另一种更优雅的方式是在Java服务层进行判断如果集合为空直接返回空结果避免执行SQL。4. 与MyBatis-Plus的协同提升开发效率MyBatis-PlusMP是国内最流行的MyBatis增强工具它“为简化开发而生”。热搜词中提到了“mybatis plus 存在则跳过 不存在则新增”这正是MP提供的强大功能之一。4.1 快速集成与基础CRUD引入MP后你的Mapper接口只需继承BaseMapperT即可获得几乎所有的单表CRUD方法无需编写对应的XML。Repository public interface UserMapper extends BaseMapperUser { // 无需定义 selectById, insert, updateById, deleteById, selectList等方法 // 可以在此定义自己的复杂查询方法 }在Service层MP也提供了IService和ServiceImpl模板进一步封装。public interface IUserService extends IServiceUser { // 自定义业务方法 } Service public class UserServiceImpl extends ServiceImplUserMapper, User implements IUserService { // 可以直接使用 baseMapper 和一系列lambda查询方法 }4.2 “存在则更新不存在则插入”的实现这个需求对应MP的saveOrUpdate或saveOrUpdateBatch方法。其原理是根据实体类的主键字段默认是id进行判断。如果主键有值非null且非空取决于主键生成策略MP会先执行一次selectById查询。如果查询到记录则执行updateById如果未查询到则执行insert。使用示例User user new User(); user.setId(1L); // 如果id1的记录存在则更新否则新增 user.setUsername(newName); user.setEmail(newemail.com); // 单条操作 userService.saveOrUpdate(user); // 批量操作效率更高 ListUser userList ...; userService.saveOrUpdateBatch(userList);避坑指南主键策略确保你的实体类主键字段通常是id使用了正确的注解如TableId(type IdType.ASSIGN_ID)雪花ID或TableId(type IdType.AUTO)数据库自增。如果主键策略是AUTO你想“更新”一个id为null的记录MP会将其识别为“新增”。并发问题saveOrUpdate不是原子操作先查后改/增在高并发场景下可能存在“先查无后另一线程插入再本线程插入”导致的唯一键冲突风险。对于严格防重的场景应使用数据库的ON DUPLICATE KEY UPDATEMySQL或MERGE INTOOracle语句这可以通过MP的自定义SQL或insertOrUpdate方法需配置实现。4.3 Lambda查询与更新MP的Lambda查询链式API极大地提升了代码的可读性和类型安全性。// 查询username包含‘Tom’且状态为启用的用户列表 ListUser list userService.lambdaQuery() .like(User::getUsername, Tom) .eq(User::getStatus, 1) .list(); // 更新将指定用户的邮箱批量置为null boolean updated userService.lambdaUpdate() .in(User::getId, Arrays.asList(1L, 2L, 3L)) .set(User::getEmail, null) .update(); // 存在则更新另一种写法根据非主键字段判断 boolean saved userService.lambdaUpdate() .eq(User::getUsername, uniqueName) .set(User::getEmail, newmail.com) .update(); // 先尝试更新 if (!saved) { // 如果更新影响行数为0说明记录不存在 User newUser new User(); newUser.setUsername(uniqueName); newUser.setEmail(newmail.com); userService.save(newUser); // 执行插入 }5. 高级应用与性能优化5.1 分页查询的正确姿势使用PageHelper可以极其方便地实现物理分页。// 在查询方法前调用PageHelper.startPage之后的第一个MyBatis查询方法会被分页 GetMapping(/users) public PageInfoUser getUsers(RequestParam int pageNum, RequestParam int pageSize) { // 参数合理化已开启pageNum0会查第一页pageNum过大查最后一页 PageHelper.startPage(pageNum, pageSize); // 紧接着的查询会自动分页 ListUser userList userMapper.selectByCondition(new UserQuery()); // 用PageInfo包装结果包含总记录数、总页数等详细信息 return new PageInfo(userList); }关键点PageHelper.startPage(pageNum, pageSize)必须紧挨着执行SQL的Mapper方法调用之前中间不能有其它数据库查询操作否则分页会失效。5.2 一级缓存与二级缓存一级缓存SqlSession级别默认开启。在同一个SqlSession通常对应一次数据库会话/事务中执行相同的SQL和参数会直接从缓存返回结果。在Spring中由于通常将SqlSession作用域配置为“请求”级别且结合事务一级缓存容易引起“脏读”。例如在同一个事务中你先查询了一条数据然后另一个方法修改了这条数据但未提交你再次查询可能读到旧数据。我的建议是对于需要强一致性的查询可以在Mapper方法上添加Options(flushCache Options.FlushCachePolicy.TRUE)来禁用缓存。二级缓存Mapper级别需要显式开启。在mybatis-config.xml或application.yml中配置cache-enabled: true并在具体的Mapper.xml中添加cache/标签。二级缓存是跨SqlSession的数据以序列化形式存储因此实体类必须实现Serializable接口。二级缓存的问题更多数据一致性难保证除非整个应用只有一台服务、缓存更新策略复杂、容易引发脏数据。在分布式、微服务架构下我几乎从不使用MyBatis自带的二级缓存而是采用集中式的缓存方案如Redis。5.3 复杂结果映射association与collection处理一对一、一对多的关联查询时MyBatis的ResultMap非常强大。!-- 查询订单及其用户信息一对一 -- resultMap idOrderDetailMap typeOrder id propertyid columnorder_id/ result propertyorderNo columnorder_no/ !-- association 映射单个关联对象 -- association propertyuser javaTypeUser id propertyid columnuser_id/ result propertyusername columnusername/ /association !-- collection 映射集合关联对象 -- collection propertyitems ofTypeOrderItem id propertyid columnitem_id/ result propertyproductName columnproduct_name/ result propertyquantity columnquantity/ /collection /resultMap select idselectOrderWithDetails resultMapOrderDetailMap SELECT o.id as order_id, o.order_no, u.id as user_id, u.username, oi.id as item_id, oi.product_name, oi.quantity FROM order o LEFT JOIN user u ON o.user_id u.id LEFT JOIN order_item oi ON o.id oi.order_id WHERE o.id #{orderId} /selectN1查询问题上述是一个联表查询。另一种方式是分两次查询先在select中查订单然后在association或collection中通过select属性执行另一个Mapper查询。这种方式简单但可能导致“N1”问题查1个订单再查N个订单项。联表查询单SQL效率更高但结果映射复杂分次查询多SQL逻辑清晰但可能有性能问题。需要根据数据量和复杂度权衡。6. 常见问题排查与实战技巧6.1 典型异常与解决方案Invalid bound statement (not found)原因99%MyBatis找不到对应的Mapper XML文件。排查检查application.yml中的mybatis.mapper-locations路径是否正确。检查Maven打包后XML文件是否在jar包的对应目录下。可以在target/classes下查看。确认XML文件中的namespace是否与Mapper接口的全限定名完全一致。确认XML中的id是否与接口方法名一致。Cause: java.sql.SQLSyntaxErrorException: Unknown column ‘xxx’ in ‘field list’原因实体类字段与数据库列名映射失败。排查检查是否开启了map-underscore-to-camel-case。如果数据库列是user_name实体字段是userName这个配置会帮你自动映射。如果列名和字段名差异不遵循驼峰规则需要使用Column注解如果你用了JPA注解或在MyBatis的resultMap中显式指定result columndb_column propertyentityField/。查询结果某字段为null原因除了数据本身为null最常见的是映射失败。排查开启MyBatis的SQL日志log-impl: StdOutImpl查看实际查询返回的列名与实体类字段或resultMap中的配置逐一核对。注意大小写和空格。6.2 日志与调试将mybatis.configuration.log-impl设置为StdOutImpl可以在控制台看到完整的SQL执行过程包括预编译的SQL语句和最终传入的参数。这是调试动态SQL、排查传参问题最直接有效的方法。-- 日志输出示例 Preparing: SELECT * FROM user WHERE username LIKE CONCAT(%, ?, %) AND status ? Parameters: Tom(String), 1(Integer)6.3 在Spring Boot中运行初始化SQL脚本如果你需要在应用启动时创建表或初始化数据Spring Boot提供了很好的支持。spring: sql: init: mode: always # 或 embedded仅内存数据库时 schema-locations: classpath:schema.sql # 建表DDL >dependency groupIdorg.flowable/groupId artifactIdflowable-spring-boot-starter/artifactId exclusions exclusion groupIdorg.mybatis/groupId artifactIdmybatis/artifactId /exclusion exclusion groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId /exclusion /exclusions /dependency配置隔离确保你的MyBatis配置如SqlSessionFactory与Flowable使用的配置是分开的可以通过创建不同的Bean并指定不同的DataSource或MapperScannerConfigurer的扫描包路径来实现。整合Spring Boot与MyBatis入门容易但要用到生产级的“常用”水准就需要在这些细节上反复打磨。从安全的动态SQL编写到高效的MyBatis-Plus使用再到复杂的关联映射和问题排查每一个环节都考验着开发者对这两个框架的理解深度。记住框架是工具清晰的思路和对原理的把握才是写出稳健、高效代码的关键。多看看日志多思考SQL的执行过程很多问题都会迎刃而解。

相关新闻

六轴工业机械臂厂家怎么选 | 国产六轴协作机械臂品牌选型指南

六轴工业机械臂厂家怎么选 | 国产六轴协作机械臂品牌选型指南

2026/7/29 5:08:38

六轴工业机械臂是具备6个旋转关节的串联式工业机器人,分为传统隔离式六轴机器人与协作式六轴机械臂两大品类,是制造业上下料、装配、焊接、码垛、检测工序的核心自动化设备;六轴工业机械臂厂家选型不能只对比价格,需要匹配工艺需求…

Frida高阶内存修改:权限操控、Inline Hook与防检测实战

Frida高阶内存修改:权限操控、Inline Hook与防检测实战

2026/7/29 5:08:38

1. 项目概述:为什么内存修改是逆向工程师的“手术刀”?在逆向工程的世界里,如果说静态分析是“读病历”,那么动态调试与内存修改就是“动手术”。我们面对的软件,无论是移动应用、桌面程序还是游戏,其运行时…

灯塔客户还是抢占市场?AI企业销售策略该怎么选

灯塔客户还是抢占市场?AI企业销售策略该怎么选

2026/7/29 5:08:38

你苦苦追逐的那个知名客户,可能正在让你失去整市场。不少拥有优秀AI产品的创业公司,会花上数月时间争取第一个《财富》100强客户:融资资金被快速消耗,核心团队也被困在迟迟无法转化的交易中。创始人追逐这些客户,并不一…

KMS_VL_ALL_AIO智能激活脚本:3分钟免费激活Windows系统的终极指南

KMS_VL_ALL_AIO智能激活脚本:3分钟免费激活Windows系统的终极指南

2026/7/29 6:08:40

KMS_VL_ALL_AIO智能激活脚本:3分钟免费激活Windows系统的终极指南 【免费下载链接】KMS_VL_ALL_AIO Smart Activation Script 项目地址: https://gitcode.com/gh_mirrors/km/KMS_VL_ALL_AIO 还在为Windows系统激活而烦恼吗?每次开机看到激活提醒&…

CCF CSP认证真题解析:矩形交集面积计算与C++实现避坑指南

CCF CSP认证真题解析:矩形交集面积计算与C++实现避坑指南

2026/7/29 6:08:40

1. 项目概述:从一道认证题看算法实践最近在整理CCF CSP认证的历年真题,2023年3月份的那场认证里,第一道题“田地丈量”给我留下了挺深的印象。这道题本身不算难,但非常典型,它完美地融合了基础的几何计算、边界条件处理…

2026年口碑前五吸顶轨道灯生产商,你选对了吗?

2026年口碑前五吸顶轨道灯生产商,你选对了吗?

2026/7/29 6:08:40

老李做外贸灯具三年,换了三个供应商,清关税、认证费、售后费加起来亏了十几万。最近他找我喝酒,红着眼说:“兄弟,我算是搞明白了,吸顶轨道灯这行,挑供应商比挑对象还难!”我问他怎么…

企业用了多家云服务,并不等于自动拥有高可用

企业用了多家云服务,并不等于自动拥有高可用

2026/7/29 6:08:40

为了降低单一云平台故障带来的影响,一些企业开始同时使用两家甚至多家云服务商。看起来,只要资源分散在不同平台,即使其中一家出现问题,业务也可以切换到另一家继续运行。但在实际环境中,“使用多家云”与“具备多云高…

利用企业微信 JS-SDK 实现手机端原生能力调用

利用企业微信 JS-SDK 实现手机端原生能力调用

2026/7/29 6:08:40

当企业开发 H5 混合应用时,往往需要调用手机相册、扫一扫、地理位置定位等硬件或原生系统能力。此时必须在前端页面引入并配置企业微信的 JS-SDK。配置步骤后端计算安全签名(signature)。签名算法涉及 jsapi_ticket、noncestr、timestamp 和当…

Linux C/C++开发环境搭建:从GCC到VSCode的完整指南

Linux C/C++开发环境搭建:从GCC到VSCode的完整指南

2026/7/29 5:58:40

1. 项目概述:为什么需要一个“趁手”的Linux C/C环境?每次看到新手朋友在Windows上装个IDE,然后对着黑框框敲下gcc main.c,结果蹦出一堆“找不到命令”或者“头文件缺失”的错误时,我就特别想跟他们聊聊环境搭建这件事…

[具身智能-649]:个人电脑搭建 RTSP 服务完整方案(Windows / Ubuntu 双平台,适配 RDK X5 rtsp2display 调试)

[具身智能-649]:个人电脑搭建 RTSP 服务完整方案(Windows / Ubuntu 双平台,适配 RDK X5 rtsp2display 调试)

2026/7/28 13:30:18

目标:电脑作为RTSP 服务端,循环推送 H264/H265 视频流; RDK X5 通过 rtsp2display 拉流预览,完全不需要在开发板编译 live555。 提供两套成熟方案: ✅ 方案 A:FFmpeg(最简单,优先推…

PDF合并与动态水印的工程化方案:2026国内免费工具实测对比

PDF合并与动态水印的工程化方案:2026国内免费工具实测对比

2026/7/28 16:04:36

一、背景与测试方案 在实际项目交付中,PDF文件合并与版权保护水印的叠加是一个高频但容易被低估的技术需求。典型的处理链路涉及:多源PDF的文件流合并、页面级水印渲染(含透明度混合与图层叠加)、输出文件体积控制。看似简单的操作…

PDF拆分压完图糊了?2026国内免费实测,档案员都在用的组合方案

PDF拆分压完图糊了?2026国内免费实测,档案员都在用的组合方案

2026/7/28 16:04:35

说实话,提到PDF拆分再压缩,我真是被折腾得够呛。 上个月公司年度合同归档,一份300多页的PDF总合同,需要按年份拆分成三个独立文件,再分别压缩到10MB以内方便邮件发送各部门确认。我心想这还不简单?先找个海…

AI会议纪要怎么做?会议录音转文字加自动整理,三个月实测流程

AI会议纪要怎么做?会议录音转文字加自动整理,三个月实测流程

2026/7/29 0:08:23

打工人总是跑不掉要写会议纪要。 我在一家互联网公司,一周至少八场会:产品评审、数据复盘、项目同步、客户沟通,每场一小时起步。 以前的标准流程是开会拼命记→会后凭记忆补→整理成文档发群,结果经常记不全、记错、记串。 大概年…

重庆化龙桥老旧小区改造,怎么搞定夜景照明“不扰居”又能省成本?

重庆化龙桥老旧小区改造,怎么搞定夜景照明“不扰居”又能省成本?

2026/7/29 0:08:23

重庆化龙桥靠着嘉陵江,老小区多,最近几年城市更新做的勤,不少住户都反映过小区夜景亮了是好事,可有的灯太晃眼,半夜拉着窗帘都透光,睡不好觉。还有物业算账,这灯开一整晚,公摊电费蹭…

目标模糊、资源泛滥、进度失控,AI学习计划制定失败的3大隐形陷阱及救急方案

目标模糊、资源泛滥、进度失控,AI学习计划制定失败的3大隐形陷阱及救急方案

2026/7/29 0:08:23

更多请点击: https://codechina.net 第一章:目标模糊、资源泛滥、进度失控,AI学习计划制定失败的3大隐形陷阱及救急方案 目标模糊:学得越勤,离真实能力越远 当学习目标停留在“学会AI”或“搞懂大模型”这类宽泛表述…