2026/10/7 4:15:16

MyBatis进阶实战:动态SQL、缓存机制与Spring Boot整合全解析

MyBatis进阶实战:动态SQL、缓存机制与Spring Boot整合全解析 做了这么多年JavaWeb项目我越来越觉得MyBatis是一个“入门容易精通难”的框架。新手花一上午就能跑通CRUD但真要应对复杂查询、动态SQL、缓存一致性这些问题没点底层功底的很容易翻车。这篇东西不打算从JDBC开始讲起而是直接聚焦MyBatis在真实项目里的进阶玩法把配置细节、动态SQL、缓存机制、Spring Boot整合这几块硬骨头啃透。不管你是刚学完SSM正在找实战感觉还是工作了一两年想搞懂二级缓存和源码设计这篇文章都值得你花二十分钟读完顺手收藏。1. 从JDBC到MyBatis为什么JavaWeb几乎绕不开它1.1 JavaWeb三层架构里MyBatis站在哪先捋一下JavaWeb经典的三层架构Controller层负责接收请求和参数校验Service层写业务逻辑DAO层或者叫Mapper层负责跟数据库打交道。大多数团队的分工里MyBatis就是DAO层那个最核心的“数据访问引擎”。我见过不少刚入行的朋友把MyBatis当成“一个写SQL的工具”这么理解不能算错但格局小了。MyBatis本质上解决的不只是“少写代码”的问题它解决的是Java对象和关系型数据库之间的阻抗不匹配。换句话说Java是面向对象的数据库是面向关系的两者之间需要一个映射器MapperMyBatis就是干这个的。这也是为什么MyBatis在JavaWeb项目里的地位如此稳固。相比Hibernate的全自动ORMMyBatis属于半自动框架——SQL由开发者编写框架负责参数注入和结果集映射。这个“半自动”特性恰恰是它在国内互联网公司流行的核心原因SQL可控、性能可调优、团队里任何一个后端开发都能看懂并修改。1.2 JDBC的痛点与MyBatis的解法要理解MyBatis的设计哲学最好的方式就是先回忆一下原生JDBC操作的繁琐。早期我写JDBC的时候一个最简单的查询要经历七个步骤加载驱动、获取连接、创建Statement、执行SQL、遍历ResultSet、手动封装成对象、关闭资源。这中间还有一堆checked exception要处理代码里全是try-catch-finally。最痛苦的是结果集映射。如果数据库表有二十个字段你就要手写二十行rs.getString或rs.getInt去取值再set到对象里。字段一多这段代码就变成纯粹的体力活而且极易出错——列名拼错一个字母编译期根本发现不了运行期才报SQLException。MyBatis的解法很聪明把“参数注入”和“结果映射”这两件最重复的事情交给框架开发者只需要写SQL和声明映射规则。你告诉MyBatis“这个查询结果要映射成User对象数据库的user_name对应对象的userName”剩下的遍历、赋值、类型转换统统不用管。另一个容易忽略的痛点是连接管理。JDBC时代获取连接用DriverManager每次都要建立物理连接性能很差。MyBatis引入了数据库连接池的概念虽然它自己不带池子但可以无缝集成Druid、HikariCP等池化方案彻底解决了连接频繁创建销毁的性能问题。1.3 入门到进阶的学习路径规划聊到“从入门到进阶”我得先泼一盆冷水如果你只是想在简历上写“熟悉MyBatis”那学会CRUD就够了。但如果你要在项目里真正用它解决复杂问题或者准备面试那必须在这些方面补齐第一阶段是基础使用mybatis-config.xml的全局配置、Mapper接口与XML映射文件的关系、参数绑定#{}和${}的区别、简单结果映射。这个阶段能完成单表增删改查就算过关。第二阶段是动态SQLif、where、choose、foreach这些标签的灵活组合以及如何用它们处理多条件查询、批量插入、批量更新。第三阶段是高级映射一对一、一对多、多对多的结果映射嵌套查询与嵌套结果集的选择懒加载的配置与局限。第四阶段是缓存与源码一级缓存、二级缓存的实现原理、失效场景以及从SqlSession、Executor到StatementHandler这条执行链路的核心源码。很多人的学习卡在第二阶段和第三阶段之间。原因很现实日常工作写复杂动态SQL的机会没那么多等真遇到了又不知道该怎么组合标签。我的建议是不要只做教程里的练习题去找一份真实项目的Mapper文件来读特别是那种带十几个查询条件的列表页SQL你读完再自己仿写一遍进步会非常快。2. 核心配置与Mapper映射先把地基打牢2.1 mybatis-config.xml里那些容易被忽略的配置项很多人用MyBatis就只配置一个environments和数据源然后万事大吉。实际项目里还有几个配置项看似不起眼关键时刻却能救命。mapUnderscoreToCamelCase。这个配置我强烈建议设成true。它的作用是把数据库的user_name自动映射成Java属性的userName省去一大堆resultMap的列名-属性映射。但要注意设置了它之后如果你的SQL里有计算列比如count(*)查出来就没法自动映射了得用别名AS total或者显式resultMap。typeAliasesPackage。MyBatis的XML里写resultType时默认要写全限定类名com.example.entity.User又长又烦。配置了typeAliasesPackage之后可以直接简写成User。这个在Spring Boot整合场景下尤其好用配合Alias注解可以自定义别名。lazyLoadingEnabled与aggressiveLazyLoading。这俩是懒加载的开关。开启懒加载之后像“查询订单时关联查用户”这种场景只有在真正getUser()的时候才执行第二条SQL。aggressiveLazyLoading默认在旧版本是true意味着“加载了对象就立即加载所有懒加载属性”——这会把懒加载机制直接废掉所以一般要显式设成false。defaultExecutorType。默认是SIMPLE还有REUSE复用预处理语句和BATCH批量执行。BATCH模式下批量插入的速度能提升好几倍但有个坑它会把所有SQL都缓存到批量执行器里如果中途有需要立即返回自增主键的场景会拿不到正确的主键值。这块我在后面实战章节再展开。2.2 Mapper接口与XML映射文件的绑定原理很多新手不理解为什么我定义一个Mapper接口不写实现类Spring就能直接注入一个能用的对象这个问题的答案其实就是MyBatis最核心的机制——动态代理。MyBatis启动时会扫描Mapper接口为每个接口通过JDK动态代理生成一个代理对象。当你调用userMapper.findByUsername(zhangsan)时代理对象会做三件事解析方法名找到对应的MapperStatement也就是XML里那条id为findByUsername的SQL、把方法参数绑定到SQL参数、执行SQL并封装返回结果。所以接口和XML的绑定规则就非常重要了接口的全限定名必须和XML的namespace一致接口的方法名必须和XML中的statement id一致接口方法的返回类型决定了XML中resultType的映射方式如果返回ListXML里通常写的是集合元素的类型有一个不规范但常见的写法是把SQL注解写进接口里比如Select(select * from user where id #{id})。小项目这样可以大项目我强烈不推荐。一旦SQL复杂起来注解里写动态SQL就是一场灾难可读性和可维护性都会急剧下降。XML虽然啰嗦一点但支持动态SQL的完整语法团队协作时也更容易做SQL Review。2.3 参数传递Param和POJO到底怎么选Mapper方法传参常见的有三种方式单个基本类型、多个参数用Param命名、封装成POJO或者Map。这里面的选择直接影响SQL的清晰度和扩展性。单个参数最简单SQL里直接用#{任意名字}都能拿到但我建议还是用有意义的参数名。多个参数的时候如果你不加Param注解MyBatis早期版本会强制要求用#{param1}、#{param2}这种位置参数代码可读性很差重构也容易出错。加Param注解之后SQL里就能写#{username}、#{status}这种语义化名称。参数超过三个我倾向于封装POJO。比如查询用户列表的过滤条件包括用户名、状态、时间范围、分页信息我会建一个UserQuery对象专门承载查询参数。这样做的好处是Service层调用时语义清晰、后续加查询条件不用改方法签名、还能配合MyBatis的if标签做动态条件拼装。Map传参虽然灵活但我不建议在核心业务代码里用。Map丢了类型信息SQL里取值全靠字符串key一旦key拼错运行期才发现很难维护。少量的场景下用可以大量的业务逻辑里用Map迟早要还债。3. 动态SQL与复杂映射从“能跑”到“好用”的分水岭3.1 动态SQL的核心标签与组合套路动态SQL是MyBatis最值得花时间学的部分也是从“会写CRUD”到“能在真实项目里干活”的分水岭。最常用的几个标签逐个拆开说。**if**是最基础的判断标签。典型场景是多条件查询用户可能只填了用户名也可能同时选了状态和创建时间SQL需要动态拼WHERE条件。select idlistByCondition resultTypeUser SELECT * FROM user where if testusername ! null and username ! AND username LIKE CONCAT(%, #{username}, %) /if if teststatus ! null AND status #{status} /if /where /select注意where标签的妙处如果子标签里任何一个if成立它会自动加WHERE关键字同时还会智能去除第一个条件前面多余的AND或OR。没有where的话你得写成WHERE 1 1这种老套路虽然也能跑但一眼看过去就很业余。**foreach**是处理集合参数的关键。批量插入和IN条件查询都离不开它。批量插入语法长这样insert idbatchInsert INSERT INTO user (username, email, create_time) VALUES foreach collectionlist itemuser separator, (#{user.username}, #{user.email}, NOW()) /foreach /insertcollectionlist指的是传入参数是List类型item是循环体里的别名separator,表示每条记录之间用逗号分隔。这个标签有三个坑一是collection的值如果参数加了Param(list)注解就要写注解名否则写list或collection二是一条SQL插入上千条数据会超过MySQL的max_allowed_packet限制通常建议每批500条左右三是Oracle数据库不支持这种VALUES多行写法要用INSERT ALL INTO ...替代。**choose**相当于Java里的switch。有些业务逻辑是“如果传了A就按A查否则按B查”这种互斥的条件判断用if会出问题——它可能会同时拼上两个条件而choose只会命中第一个满足的when。这套组合拳掌握之后绝大多数查询页面的动态条件都能搞定了。我自己的习惯是每个查询方法都先搭一个基础SELECT骨架然后在where里按字段逐个加if判断最后再全局看一遍SQL有没有性能隐患。千万不能写完就算完务必在Navicat里执行EXPLAIN看看索引命中情况。3.2 复杂结果映射一对一、一对多的处理方式单表查询的结果映射简单但真实业务里订单要关联用户、用户要关联角色、角色要关联权限多表关联的结果映射才是真正考验水平的地方。MyBatis提供了两种关联查询方案嵌套结果映射和嵌套查询。嵌套结果映射用一条JOIN SQL把所有数据一次性查出来然后通过association和collection标签做结果集的列映射。以订单关联用户为例resultMap idOrderWithUserMap typeOrder id propertyid columnorder_id / result propertyorderNo columnorder_no / association propertyuser javaTypeUser id propertyid columnuser_id / result propertyusername columnusername / /association /resultMap select idgetOrderWithUser resultMapOrderWithUserMap SELECT o.id AS order_id, o.order_no, u.id AS user_id, u.username FROM t_order o LEFT JOIN t_user u ON o.user_id u.id WHERE o.id #{id} /select这里有几个细节要注意。第一如果两张表都有id字段必须用别名区分比如order_id和user_id否则MyBatis分不清哪列映射到哪个对象。第二id标签不只是主键标记它在结果映射里肩负着区分不同对象的职责MyBatis判断“同一行记录是否属于同一个Order对象”就看id属性是否相同所以写错id会导致数据错乱。第三javaType或ofType不能省否则映射不知道该创建什么类型的对象。嵌套查询则是先查主表然后通过select属性再发起一条查询association propertyuser selectcom.example.mapper.UserMapper.selectById columnuser_id /这种方式写起来简单明了但有个致命问题——N1查询。如果你查了100条订单每条订单的user都要单独查一次总共会产生101条SQL。这在高并发场景下是灾难。所以除非是在懒加载配合低并发管理端页面否则我优先推荐嵌套结果映射。3.3 批量操作的性能优化与注意事项批量操作的性能问题在JavaWeb项目里非常典型。我之前做过一个数据导入功能一次性要往数据库里插入两万条记录。最开始用for循环单条插入跑完全程用了差不多三分钟操作者等得怀疑人生。后来改成MyBatis的批量插入四十多秒跑完再配合BATCH执行器压缩到二十秒以内。怎么改的核心就两步第一步SQL层面用foreach拼多条VALUES。前面已经写过示例这里不再重复。关键参数是批量大小我压测下来的经验值是300-500条一批比较合适。这个数字跟数据库的max_allowed_packet直接相关MySQL默认一般是4MB或64MB500条记录如果字段多大概几十KB完全在安全范围内。第二步配置ExecutorType为BATCH。在Spring整合MyBatis时可以通过SqlSessionTemplate指定执行器类型Bean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH); }不过这个配置是全局生效的会影响所有Mapper操作包括查询。BATCH模式下查询操作不受影响但写操作不会立即刷新到数据库而是批量攒着。如果你在循环里执行插入后又马上执行查询这条刚插入的数据很可能会查不到。这时候需要在中间手动调用sqlSession.flushStatements()强制刷新。我的最终方案是批量的场景单独创建一个ExecutorType.BATCH的SqlSessionTemplate只给批量操作使用其余的Mapper保持默认SIMPLE执行器互不干扰。这个方案实测最稳也不影响正常业务代码。4. 缓存机制深度拆解面试必问的加分项4.1 一级缓存SqlSession级别的顺手缓存MyBatis的一级缓存是默认开启的作用范围是每一个SqlSession。用生活化的方式理解每次你打开和数据库之间的会话这个会话内部有一个缓存Map同一个查询第一次执行后结果会被放进缓存第二次再执行相同SQL时直接命中缓存返回不再访问数据库。这个机制初衷是好的——避免同一个会话内重复查询浪费资源。但很多团队都踩过它的坑最典型的就是MySQL事务隔离级别与一级缓存的冲突。举个例子一个事务里先查询用户余额然后在别的地方另一个事务修改了这个用户的余额并提交接着当前事务再查询同一个用户。从数据库的角度看在可重复读隔离级别下当前事务的第二次查询应该还是旧值因为事务隔离但MyBatis的缓存机制直接把第二次查询拦截了你拿到的是第一次查询的旧结果。这俩在这里的行为是一致的可一旦业务逻辑自己执行了UPDATE、DELETE等写操作一级缓存就会被清空第二次查询就要老老实实访问数据库而这时候数据库值已经变了返回值就和第一次不同了同一个事务内两次查询结果不一样——这在可重复读隔离级别下是不应该发生的。这个问题的本质是MyBatis的一级缓存是会话级别的它和数据库事务的隔离级别没做绑定。所以现在很多人建议在需要严格事务一致性的场景下关闭一级缓存或者使用statement级别的缓存作用域。我在项目里的做法是一级缓存保持默认开启但所有涉及“读后写、写后读”的操作严格使用新SqlSession或者显式调用sqlSession.clearCache()。虽然损失了一点性能但换来的是不会出现诡异的脏读。4.2 二级缓存跨SqlSession的共享缓存怎么配二级缓存的本质是Mapper级别的共享缓存。一级缓存跟着SqlSession走SqlSession关闭缓存就没了二级缓存跟着Namespace走多个SqlSession共享同一个Mapper的缓存。配置二级缓存需要三步第一步在Mapper的XML文件里加上cache evictionLRU flushInterval600000 size512 readOnlyfalse /第二步确保对应的实体类实现了Serializable接口。因为二级缓存的readOnly默认是false走的是序列化反序列化的深拷贝实体类不实现Serializable启动会直接报错。第三步所有的查询语句要设置useCachetrue默认就是true写操作设置flushCachetrue默认也是true。这样每次INSERT/UPDATE/DELETE都会清空该Mapper的二级缓存避免脏数据。配置完成之后二级缓存的存储介质默认是内存HashMap。想玩得花的可以接Redis、Ehcache等外部缓存核心思路是实现MyBatis的Cache接口把读写方法委托给外部缓存框架。这里必须重点警告二级缓存有一个经典的大坑——多表关联查询的缓存脏数据问题。比如一个OrderMapper查了订单的同时通过关联查询带出了User信息缓存被存进了OrderMapper的Namespace下。这时候如果UserMapper更新了用户表它只会清空自己Namespace下的缓存OrderMapper缓存里的旧User信息就成了脏数据还继续被返回给上层。这个问题面试时经常被问到网上也有不少因此踩坑的血泪帖。所以在实际项目中我对二级缓存的定位一直很保守单表读多写少且一致性要求不高的场景才开比如数据字典、配置项、商品分类。涉及多表关联的业务宁可不要二级缓存也别让缓存一致性隐患变成线上事故。4.3 缓存源码级理解SqlSession、Executor和Cache的关系要真正搞懂缓存机制光看配置不够得理解内部的调用链路。MyBatis的执行过程大致是SqlSession对外提供服务是门面Executor是真正的执行器负责调度StatementHandler处理JDBC相关操作以查询为例SqlSession.selectList()会调用Executor.query()。这里的Executor是有包装链的——如果你开了二级缓存最外层是CachingExecutor它先查二级缓存查不到再交给真正的BaseExecutor去执行。BaseExecutor内部又有一级缓存逻辑也就是那个用LocalCache实现的PerpetualCache。所以一次查询的完整缓存查找顺序是二级缓存 → 一级缓存 → 数据库。写操作则相反层层刷新缓存先刷二级缓存再刷一级缓存最后真正执行SQL。理解这条链路之后很多“奇怪”现象就说得通了为什么二级缓存配置了却没生效因为你查到的是同一个SqlSession里的一级缓存结果压根没走到二级缓存那一步。想知道是否命中二级缓存可以打开MyBatis的日志看有没有出现Cache Hit Ratio。为什么两个SqlSessionA和BA更新了数据B查到的还是旧值因为A的更新只是清空了A自己的一级缓存和这个Mapper的二级缓存如果B开的是一级缓存且之前已经查过一次B的那次旧结果还是存在于B的会话里。为什么生产环境偶尔出现数据歧义、拼装错乱重启后就好了大概率是二级缓存里存的序列化对象版本和实体类版本对不上或者存的对象被并发修改了。我的建议是缓存这块不仅要会用还是要抽空把Executor这一小段源码读一遍。不为别的就为了面试的时候你能说出来“CachingExecutor”这个类名能讲清楚CacheKey由哪些元素构成MappedStatement ID、参数值、分页信息、环境ID等这比被问到时支支吾吾强得多。5. 与Spring Boot整合实战现代项目的标准姿势5.1 从SSM到Spring Boot整合步骤简化了多少谈到MyBatis就不能不提Spring Boot。Spring Boot的自动配置把不少繁琐工作都消灭了。十年前做SSM整合要自己写一堆XML配置配置数据源、配置SqlSessionFactory、配置Mapper扫描、配置事务管理器一不小心版本不兼容就折腾一整天。现在Spring Boot整合MyBatis只需要三步第一步引入依赖dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency第二步配置application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/your_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl第三步启动类或者配置类上加MapperScan注解指定Mapper接口所在的包。整个过程清爽很多。需要注意的坑有两个一是mybatis.mapper-locations的路径要和实际Mapper XML存放路径一致很多人报Invalid bound statement (not found)就是这个路径配错了二是Spring Boot 2.x的org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration里数据源的包名你可能需要看一下版本不同版本有细微差异但配置方式基本一样。5.2 配置打印SQL日志、线程安全和生产环境控制开发阶段把SQL日志打印出来几乎是刚需排查问题会肉眼可见地加速。MyBatis在Spring Boot里打印SQL有三种常见方式。第一种是前面配置里写的log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这种方式直接把SQL和参数打印到控制台最简单粗暴但打出来的日志没有级别控制生产环境开着它会疯狂输出把日志文件撑爆。第二种是配合Logback或Log4j2指定Mapper接口的日志级别为DEBUGlogging: level: com.example.mapper: debug这样配置的好处是可以用日志框架的级别管理来控制输出开发环境开DEBUG生产环境只保留INFO以上。注意这里的com.example.mapper要写你自己的Mapper接口所在的包名如果不同包下有多个Mapper可以分别配置。第三种是Spring Boot的mybatis.configuration.log-impl配合MyBatis的LogFactory用哪个Logger取决于类路径上引入了哪个日志框架。在SLF4J Logback项目里MyBatis会自动适配不需要显式指定log-impl。我这里有一个排查意见需要提醒你当你发现SQL日志打印不全或者完全不打印时先检查是不是没有配置日志框架或者类路径上同时存在多个日志框架导致MyBatis选错了Logger。另外生产环境如果想快速定位慢SQL可以结合数据库端的慢查询日志不用强制靠MyBatis的SQL日志。5.3 Spring Boot MyBatis整合的常见坑点Spring Boot MyBatis的整合虽然简单但踩坑的现场还是不少。我整理几个最常遇到的给还没踩过的朋友提前避雷。“Invalid bound statement (not found)”。这是最经典的报错90%的原因是Mapper接口和XML的绑定关系错了。排查顺序一看Mapper接口的全限定名和XML的namespace是否完全一致二看XML里的statement id是否和接口方法名一致三看mapper-locations路径是否扫描到了XML文件。“### Error updating database. Cause: java.sql.SQLException: Field xxx doesnt have a default value”。这是插入语句漏了字段。在MySQL的配置文件里如果字段设置了NOT NULL且没有默认值插入时没传该字段就会报这个错。解决方法是把SQL补齐或者改表结构给字段设置默认值。注意这里如果用的是useGeneratedKeystrue回填自增主键也要确认数据库表确实有自增主键。“Failed to obtain JDBC Connection”。数据源连接失败绝大多数是URL、用户名、密码的问题。特别容易忽略的是数据库driver类名MySQL 8.x要用com.mysql.cj.jdbc.Driver8.x以下用com.mysql.jdbc.Driver写错了直接连不上。顺带一提MySQL 8.x的URL一定要带serverTimezone参数否则默认时区跟中国差八小时查出来的时间会偏移。数据库连接池耗尽。Spring Boot整合MyBatis后默认连接池是HikariCP。在高并发下如果某个慢SQL把连接占了很久连接池很容易被打满。排查思路是看HikariCP的active连接数如果一直居高不下优先排查是不是有事务方法内的SQL执行太久或者代码里存在连接泄漏。6. 常见问题排查与MyBatis面试题速查6.1 高频问题排查实录这里我挑几个我在运维线上项目时真实排查过的案例按问题现象到解决思路的顺序给你做个速查手册。问题一查询结果和数据库不一致现象用同一条件查两次返回不同结果或者查出来的数据缺行。排查步骤先确认是否命中二级缓存看日志里有没有Cache Hit Ratio命中率高说明走缓存了考虑数据一致性再确认是否发生N1查询关联查询里触发了大量SQL最后看SQL本身有没有JOIN条件写漏LEFT JOIN忘写关联条件会产生笛卡尔积结果行数爆炸。问题二批量插入很慢现象用foreach批量插入一万条数据耗时几十秒。优化三板斧先确认是否用了BATCH执行器没用的话加上再查MySQL的rewriteBatchedStatements参数在JDBC URL后面加rewriteBatchedStatementstrue这个参数能让MySQL把多条insert语句合并成一条多值insert性能提升非常明显最后看批量大小是否合理太大反而会因为单条SQL过长而变慢。问题三XML里有中文注释导致启动失败现象启动时提示Content is not allowed in trailing section。这个其实不是MyBatis的问题是XML文件里有非法字符。最常见的元凶是注释里用了--这种SQL注释或者XML的注释!-- --没闭合。解决办法是确保XML注释格式正确不要在里面写SQL注释。问题四使用${}导致SQL注入现象某个按照sort字段排序的功能传入特殊值之后数据泄露。这是典型的字符串拼接注入。${}是直接字符串替换#{}是预编译参数占位。排序字段、表名这类没法预编译的要做白名单校验比如提前在代码里判断传入值是不是属于允许的字段集合。宁可不用${}也不要把用户输入直接拼进去。6.2 MyBatis面试题清单与要点下面这份清单是我从真实面试里总结出来的高频问题按难度递增排列问MyBatis中#{}和${}的区别是什么答#{}是预编译处理MyBatis会把它替换成?占位符通过PreparedStatement的setString等方法设置参数可以防止SQL注入${}是字符串直接替换直接把参数值拼接到SQL语句中存在SQL注入风险。能用#{}的地方绝不用${}。问MyBatis一级缓存和二级缓存的区别答一级缓存是SqlSession级别的默认开启同一个SqlSession内多次执行相同的查询会命中缓存二级缓存是Mapper级别的跨SqlSession共享需要手动配置开启。二级缓存失效的关键是执行增删改操作时会清空缓存但多表关联场景下要注意缓存一致性问题。问Mapper接口的底层层理是什么原理答JDK动态代理。MyBatis使用MapperProxy为Mapper接口生成代理对象调用接口方法时通过MapperMethod解析SQL语句和参数然后交给SqlSession执行。这也是为什么Mapper接口不需要实现类Spring容器能直接注入代理对象。问MyBatis中resultType和resultMap的区别答resultType是自动映射要求数据库字段名和JavaBean属性名一致或者开启mapUnderscoreToCamelCase之后能自动转换resultMap是手动映射适合多表关联、字段名不一致、复杂类型嵌套的场景使用resultMap可以不开启驼峰转换配置。问当实体类属性名和表字段名不一致时怎么处理答三种方式SQL里给字段起别名用AS改成和属性名一致配置mapUnderscoreToCamelCase为true让user_name自动映射到userName定义resultMap手动指定字段-属性映射。具体用哪个看团队规范和项目复杂度。这个问题我见过太多人只会背答案说不出原理。建议面试前把我的源码路径梳理一节好好看一遍能讲清楚执行链路就是加分项。6.3 进阶学习方向从会用框架到会设计框架写到这里我在想怎么给这篇“进阶”主题收个尾。很多人在MyBatis学到能应付项目和面试之后就停下来了这其实挺可惜的。因为MyBatis的设计思路在Java生态里非常典型用一个代理层隔离复杂度把易变的部分交给开发者把固定的部分交给框架。如果接下来想继续深入我推荐的路径是第一步读源码。重点看SqlSessionFactoryBuilder、XMLConfigBuilder的配置解析过程以及DefaultSqlSession到Executor的完整执行链路。读源码的目的是建立“框架行为可预期”的直觉出问题时能第一时间定位到是配置问题、缓存问题还是SQL问题。第二步做扩展。比如自己实现一个TypeHandler来处理加密字段的自动加解密或者实现一个Interceptor来做SQL日志分析、自动填充创建时间、甚至实现分页插件。MyBatis的插件机制基于动态代理和拦截器链做这些扩展比写业务更能加深对框架的理解。第三步往生态上靠。MyBatis-Plus、MyBatis-Generator、PageHelper这些周边工具在不同团队里很常见。用它们效率很高但有个前提——你最好先理解它们帮MyBatis做了什么扩展、绕过了哪些限制否则出了问题容易一头雾水。回到开头那句话MyBatis入门容易精通难。但正因为难能把这块吃透的人在团队里的价值才足够明显。这篇内容算是我这几年项目经验的一次梳理希望对你有所帮助。如果你在实操时遇到什么奇怪的问题欢迎在评论区和大家交流我看到了会尽量回复。