MyBatis从入门到源码:动态SQL、缓存机制与MyBatis-Plus选型全解析 我之前带过一个刚转行的新人他把 JDBC 的代码写了一百多行就为了查一张表里的三个字段。我问他为什么不用 MyBatis他反问我用框架查数据和自己写 SQL 有什么区别这个问题其实问到了根子上。javaWeb 开发走到今天MyBatis 几乎成了操作数据库的事实标准它不是一个可有可无的封装而是把Java 对象和数据库记录之间的映射这件事从体力活变成了配置活。这篇内容就是写给正在学 javaWeb、想系统掌握 MyBatis 的人从 2023 版本 Idea 怎么建项目开始一路走到源码层面最后聊聊 MyBatis 和 MyBatis-Plus 的选型问题。整篇文章会包含完整的项目搭建步骤、核心配置讲解、动态 SQL 和缓存机制的分析以及面试里最高频的那几个问题。我的目标很直接看完之后你能自己搭一个干净的 MyBatis 项目能说清楚一条 SQL 从接口调用到数据库返回的完整链路也能在面试官问MyBatis 和 MyBatis-Plus 有什么区别的时候给出有深度的回答而不是背概念。1. 为什么 JavaWeb 项目里绕不开 MyBatis先搞清楚它到底解决了什么问题很多人学 MyBatis 的时候最困惑的不是 API 怎么用而是我为什么要用。这很正常如果你只写过个位数的 JDBC 代码感觉不到痛点自然理解不了框架的价值。所以我先不急着上代码而是把 JDBC 的实际开发困境摊开来看。1.1 JDBC 时代的体力活代码里全是重复模板JDBC 操作数据库的流程说白了就六步加载驱动、获取连接、创建 Statement、执行 SQL、遍历 ResultSet、关闭资源。这六步里真正有价值的只有两行——SQL 语句本身和把 ResultSet 里的数据取出来塞进对象的那几行赋值代码。其余的全是模板。更麻烦的是你用 JDBC 写一个新增方法先要PreparedStatement里一个参数一个参数地setString、setInt写一个查询方法又要一个字段一个字段地getString、getInt。只要表结构一变Java 代码跟着大改。如果项目里有几十张表光这些翻译代码就能写到怀疑人生。这里还有一个隐性成本连接管理。每次操作都手动获取连接、手动关闭一旦某条路径忘了关连接数据库连接池就被慢慢耗光系统就卡死了。这种问题在开发环境还不明显一上生产就立刻爆炸。1.2 MyBatis 的核心价值把 SQL 还给你把体力活接过去MyBatis 最聪明的设计是它没有像 Hibernate 那样试图帮你生成 SQL而是把 SQL 映射这件事做到了极致。你用 XML 或者注解写 SQLMyBatis 负责把?参数自动绑定到 Java 方法的参数上把查询结果自动映射成 Java 对象。SQL 写得好不好、优不优化完全由你控制而参数绑定、结果映射、连接管理这些重复劳动全部交给框架。这个设计理念我觉得特别符合工程实际。JavaWeb 项目里业务再复杂底子就是增删改查而增删改查里最灵活、最需要优化的就是查询。MyBatis 让你既能像写 JDBC 一样精细控制 SQL又不需要忍受 JDBC 的海量模板代码。1.3 和 Hibernate、Spring Data JPA 相比MyBatis 到底赢在哪说到这一定有人会提 Hibernate。这两者的路线差异非常大Hibernate/JPA 是全自动路线你定义好实体类和表的关系映射框架自动生成 SQL开发效率极高。但代价是 SQL 是框架生成的一旦遇到复杂的多表关联查询你得学会 HQL 或 Criteria API而且生成的 SQL 未必走索引排错还麻烦。MyBatis 是半自动路线SQL 你自己写框架只做参数映射和结果映射。这意味着你永远知道数据库在跑什么优化起来心里有底。我见过不少团队从 Hibernate 切到 MyBatis原因出奇一致项目大了之后Hibernate 的自动 SQL 变成了一团没法优化的黑盒DBA 拿着慢查询日志找过来开发都不知道是哪段代码产生的。MyBatis 不会有这种问题SQL 就明明白白写在那慢查询一排查一个准。1.4 学 MyBatis 之前你最好具备的基础扎实的 SQL 功底至少能熟练写多表联查、分组查询、子查询Java 反射的基础概念不要求精通但要知道怎么回事对 JDBC 操作流程有一个感性认识不用熟练理解即可了解 Maven因为项目依赖和构建全靠它如果 SQL 还不熟建议先花两周狠狠补一补再回来学 MyBatis。框架帮不了你的 SQL 水平这是真话。2. 从创建项目到跑通第一个 MyBatis 查询完整的环境搭建与最小可用程序纸上谈兵没有意思这一章直接上手。我用 2023 版本的 IntelliJ IDEA 演示完整流程Maven 作为构建工具数据库用 MySQL。你可以一边看一边操作整个流程走完你的第一个 MyBatis 程序就跑起来了。2.1 用 2023 版本 IDEA 创建一个干净的 JavaWeb 项目打开 IDEA选择New Project。这里注意一点不要再选老式的Java Enterprise模板了直接选最普通的Java项目然后把构建工具选成Maven。在 2023 版本里IDEA 已经内置了较新的 Maven 版本不需要像老教程那样另外下载。项目创建之后你会在 pom.xml 里添加依赖。我建议第一步就引入这些坐标dependencies !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- MyBatis 核心 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.16/version /dependency !-- JUnit 单元测试 -- dependency groupIdjunit/groupId artifactIdjunit/artifactId version4.13.2/version scopetest/scope /dependency /dependencies如果你的项目需要 Web 能力Servlet、JSP再额外加javax.servlet-api和 JSP 相关依赖。但 MyBatis 本身不依赖 Web 容器它只是一个持久层框架在纯 Java 环境里就能运行。2.2 数据库准备以最典型的用户表举例我习惯用一张最贴近业务的用户表来讲避免用student、teacher这些老套示例。假设我们要做的是一个简单的后台管理系统第一张表是sys_userCREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, email VARCHAR(100) DEFAULT NULL COMMENT 邮箱, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表;这里插一句很多教程里的表设计非常随意主键用int字符集不指定字段不加注释。实际 javaWeb 项目里这些细节直接影响后续开发。主键必须是大整数类型或雪花 ID字符串必须用utf8mb4不然遇到 emoji 表情直接存不进去。2.3 五步走从 POJO 到 Mapper 的完整实现第一步创建实体类SysUser对应表结构public class SysUser { private Long id; private String username; private String password; private String nickname; private String email; private LocalDateTime createTime; // 生成对应的 getter 和 setter这里省略 }第二步创建 Mapper 接口。注意MyBatis 的 Mapper 是一个接口没有实现类这是很多新手最容易懵的地方。框架通过动态代理自动帮你生成了实现public interface SysUserMapper { SysUser selectById(Long id); }第三步创建映射文件SysUserMapper.xml放在resources/mapper/目录下?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN https://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.mapper.SysUserMapper select idselectById resultTypecom.example.entity.SysUser SELECT id, username, password, nickname, email, create_time FROM sys_user WHERE id #{id} /select /mapper第四步创建 MyBatis 的全局配置文件mybatis-config.xml?xml version1.0 encodingUTF-8 ? !DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//EN https://mybatis.org/dtd/mybatis-3-config.dtd configuration environments defaultdevelopment environment iddevelopment transactionManager typeJDBC/ dataSource typePOOLED property namedriver valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/your_db?serverTimezoneAsia/Shanghaiamp;useSSLfalseamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword valueyour_password/ /dataSource /environment /environments mappers mapper resourcemapper/SysUserMapper.xml/ /mappers /configuration第五步写测试代码跑通整个流程public class SysUserMapperTest { Test public void testSelectById() throws IOException { String resource mybatis-config.xml; InputStream inputStream Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory new SqlSessionFactoryBuilder().build(inputStream); try (SqlSession session sqlSessionFactory.openSession()) { SysUserMapper mapper session.getMapper(SysUserMapper.class); SysUser user mapper.selectById(1L); System.out.println(user.getUsername()); } } }2.4 新手必踩的三个坑配置文件路径、URL 编码和 ResultType第一个坑是配置文件路径。Resources.getResourceAsStream(mybatis-config.xml)里的路径是相对于classpath的所以mybatis-config.xml必须放在resources目录下而不是随手放在项目根目录。第二个坑是JDBC URL 里的参数。符号在 XML 里必须写成amp;否则 XML 解析直接报错。同时serverTimezoneAsia/Shanghai必须指定MySQL 8 对时区敏感不指定会报错或时间差八小时。第三个坑是resultType的属性映射。MyBatis 默认开启mapUnderscoreToCamelCase是 false也就是说数据库的create_time和下划线风格的列名默认不会自动映射到 Java 的createTime属性。要么你在 SQL 里给列起别名SELECT create_time AS createTime要么在全局配置里开启驼峰映射settings setting namemapUnderscoreToCamelCase valuetrue/ /settings强烈建议直接开启驼峰映射这是所有真实项目的标配不然每个查询都写别名写完想哭。3. MyBatis 配置与 Mapper 映射详解从全局配置到一行日志的完整链路跑通第一个程序之后就该认真理解配置体系了。MyBatis 的配置分两层一层是全局配置文件mybatis-config.xml管的是框架运行时的全局行为另一层是 Mapper 映射文件管的是每条 SQL 和参数的对应关系。很多人学完就忘是因为只记了标签名字不理解配置加载的顺序和优先级。3.1 mybatis-config.xml 的标签加载顺序以及每个标签到底能干什么mybatis-config.xml里的标签顺序是死的书写顺序颠倒就会报错。完整的顺序是properties→settings→typeAliases→typeHandlers→objectFactory→objectWrapperFactory→reflectorFactory→plugins→environments→databaseIdProvider→mappers为什么顺序这么严格因为 MyBatis 的 XML 解析器是按顺序逐标签解析的后面的标签可能依赖前面标签注册的信息。比如mappers必须放在最后因为映射文件的注册需要前面所有配置已经就绪。重点说几个高价值的properties 标签用来引入外部属性文件典型场景是把数据库连接信息抽到一个db.properties里properties resourcedb.properties/然后在 environments 里用${driver}、${url}引用。好处是环境切换时只改一个文件不用动 XML。settings 标签全局行为开关。除了刚才说的驼峰映射还有几个常用的cacheEnabled全局二级缓存开关默认 truelazyLoadingEnabled延迟加载开关联查多表时很有用logImpl指定日志实现排错必备typeAliases 标签给实体类起别名。不然每个 Mapper XML 里写resultTypecom.example.entity.SysUser写多了手指头都疼。配置方式typeAliases package namecom.example.entity/ /typeAliases配置后Mapper XML 里可以直接写resultTypeSysUser。MyBatis 还会自动把驼峰式类名转成小写开头的别名SysUser对应sysUser不区分大小写。3.2 Mapper 映射文件的 namespace 和参数传递逻辑Mapper XML 的根元素是mapper namespace...这个 namespace 必须和 Mapper 接口的全限定名一致。MyBatis 就是通过这个 namespace 把 XML 里的 SQL 和接口方法绑在一起的。参数传递是新手重灾区。我给出几个最常见场景的对比单个参数传递直接使用#{任意名字}SysUser selectByUsername(String username);select idselectByUsername resultTypeSysUser SELECT * FROM sys_user WHERE username #{username} /select多个参数必须用Param注解命名否则 MyBatis 只能用arg0、param1这种丑到爆的引用方式SysUser selectByUsernameAndEmail(Param(username) String username, Param(email) String email);select idselectByUsernameAndEmail resultTypeSysUser SELECT * FROM sys_user WHERE username #{username} AND email #{email} /select传对象时直接引用对象属性名int insertUser(SysUser user);insert idinsertUser parameterTypeSysUser useGeneratedKeystrue keyPropertyid INSERT INTO sys_user(username, password, nickname, email) VALUES (#{username}, #{password}, #{nickname}, #{email}) /insert这里有个超级实用的细节useGeneratedKeystrue配合keyPropertyid插入数据后 MyBatis 会自动把自增主键回填到传入对象的id属性里。很多新人不知道这个功能插入完数据还要再查一次数据库拿主键白白多一次 IO。3.3 resultMap 完全解读什么时候必须用什么时候可以偷懒resultMap是 Mapper 映射里最强大的功能之一。它的核心作用是解决两个问题列名和属性名不一致时的映射关系以及复杂的嵌套查询结果映射。最简单的场景如果全局配置里已经开了驼峰映射那普通查询根本不需要 resultMapresultType就够了。但遇到多表联查查询结果包含多个实体类的字段时就必须用 resultMap 了resultMap idUserOrderMap typecom.example.vo.UserOrderVO id propertyid columnid/ result propertyusername columnusername/ result propertyorderId columnorder_id/ result propertyorderAmount columnorder_amount/ /resultMap select idselectUserOrders resultMapUserOrderMap SELECT u.id, u.username, o.id AS order_id, o.amount AS order_amount FROM sys_user u LEFT JOIN sys_order o ON o.user_id u.id WHERE u.id #{id} /select注意id标签特别重要它告诉 MyBatis 哪一列是唯一标识。尤其在嵌套映射的时候MyBatis 依赖这个 id 来判断对象是否已经存在写错了会造出重复的对象。3.4 配置打印 SQL 日志日志打得好排错没烦恼热词里有mybatis配置打印这个能力太重要了。MyBatis 的日志实现需要在 settings 里指定。我常用的是STDOUT_LOGGING这是 MyBatis 内置的简单日志输出配置一下就能直接在控制台看到完整 SQLsettings setting namelogImpl valueSTDOUT_LOGGING/ /settings配置之后每次执行 SQL控制台会输出这样的信息 Preparing: SELECT id, username, password, nickname, email, create_time FROM sys_user WHERE id ? Parameters: 1(Long) Columns: id, username, password, nickname, email, create_time Row: 1, admin, 123456, 管理员, adminexample.com, 2024-01-01 12:00:00 Total: 1这几行信息价值巨大。Preparing告诉你 MyBatis 最终生成的 SQL 长什么样Parameters告诉你参数类型和值Total告诉你返回了几条记录。遇到查询结果和预期不一样的问题第一步永远是看日志确认 SQL 是不是你脑子里想的那样。实际项目里生产环境我不会用STDOUT_LOGGING而是集成 Logback 或者 Log4j2配置org.mybatis包的日志级别为 DEBUG效果一样但更便于集中管理。不过在学习和单测阶段STDOUT_LOGGING 真的是最简单粗暴的工具。4. 动态 SQL 与缓存机制从能查到到高效查的必经之路跑通基本的增删改查之后MyBatis 最精华的部分才开始浮现。动态 SQL 解决的是同一个方法根据条件拼出不同的 SQL的问题缓存解决的是同样的查询能不能不每次都打数据库的问题。这两块都是 javaWeb 面试的高频区也是实际项目里真正拉开水平的地方。4.1 if、where、set、foreach、choose动态 SQL 五件套先说一个困扰很多人的典型场景列表查询页面用户可能传用户名、可能传邮箱、可能两个都不传SQL 怎么处理用纯 JDBC 的话你得手动拼字符串还要小心多余的AND和WHERE。MyBatis 的动态 SQL 直接用where标签解决select idselectByCondition resultTypeSysUser SELECT * FROM sys_user where if testusername ! null and username ! AND username LIKE CONCAT(%, #{username}, %) /if if testemail ! null and email ! AND email #{email} /if /where /selectwhere标签会自动去掉第一个AND如果没有条件它连WHERE都不会生成。这是我最常用的写法。同理更新操作里用set标签解决动态 SET 的问题update idupdateUser UPDATE sys_user set if testnickname ! nullnickname #{nickname},/if if testemail ! nullemail #{email},/if /set WHERE id #{id} /updateset会自动去掉末尾多余的逗号不用自己处理。批量插入或批量删除用foreach这是另一个高频考点insert idbatchInsert INSERT INTO sys_user(username, password, nickname) VALUES foreach collectionlist itemuser separator, (#{user.username}, #{user.password}, #{user.nickname}) /foreach /insertdelete iddeleteByIds DELETE FROM sys_user WHERE id IN foreach collectionids itemid open( close) separator, #{id} /foreach /deletechoose则等同于 Java 里的 switch命中一个分支就不再进入其他分支。比如按条件查询时有用户名就按用户名查没有就用邮箱查都没有就按 ID 降序返回前十条这类逻辑用choose写起来非常干净。4.2 动态 SQL 原理简述OGNL 表达式的威力动态 SQL 里的test属性底层用的是一门叫 OGNL 的表达式语言。这个表达式语言支持大部分 Java 运算符所以你在test里写的username ! null and username ! 本质上是在对传入的参数对象做属性访问和逻辑判断。理解这一点对你排错帮助很大。比如传进来的参数是Maptest里就直接写 key 名传的是对象就写属性名。如果写错MyBatis 会报There is no getter for property named xxx看到这个错误第一反应不是查 XML而是检查参数名是否匹配。4.3 一级缓存同一个 SqlSession 内的查询复用MyBatis 的缓存机制分两级。一级缓存是 SqlSession 级别的缓存默认开启无需任何配置。也就是说同一个 SqlSession 里如果执行两条完全相同的 SQL第二次查询会直接从缓存拿结果不再访问数据库。这个机制有它的坑。最典型的是你在同一个 SqlSession 里先查了用户接着执行了一条 UPDATE 修改了这个用户再查同一个用户MyBatis 只会执行一次查询第二次查的是缓存里的旧数据。因为任何增删改操作都会清空一级缓存这是 MyBatis 保证数据一致性的最基本手段。我见过生产环境出问题的场景就是有人把 SqlSession 的生命周期拉得很长在长事务里反复查询同一条数据以为查的是最新值结果拿到的是一级缓存的旧值。SqlSession 的生命周期应该和一次请求保持一致用完就关这在官方文档里有明确建议。4.4 二级缓存跨 SqlSession 的缓存用好是神器用不好是灾难二级缓存是 Mapper 级别的缓存多个 SqlSession 可以共享。默认不开启需要显式配置。配置方式分两步。第一步在全局配置的 settings 里确认cacheEnabled为 true默认就是 true第二步在 Mapper XML 里加一行cache/这样就启用了这个 Mapper 的二级缓存。运行机制是一个 SqlSession 查询完数据后结果会被放入二级缓存另一个 SqlSession 发起相同查询时先查二级缓存没有再查数据库。但二级缓存有一个非常需要注意的前提缓存的实体类必须实现序列化接口否则报NotSerializableException更重要的是涉及多表关联查询时更新操作只会清空当前 Mapper 的缓存其他关联 Mapper 的缓存不会被清空。什么意思呢比如订单表 JOIN 用户表查出订单信息缓存放在OrderMapper里。此时用户表被修改UserMapper的缓存会被清空但OrderMapper的缓存里还残留着旧用户信息数据就脏了。所以在真实项目中我一般建议没有百分之百把握就不要开二级缓存。单表场景可以试多表关联复杂的地方关掉更安全。这也是很多资深开发者的共识。4.5 #{} 和 ${} 的本质区别这是面试送命题关于#{}和${}的区别几乎是 MyBatis 面试必问题。我直接给结论#{}是预编译参数占位符MyBatis 会把它翻译成?然后用PreparedStatement的setXxx方法安全地传入参数值。它能有效防止 SQL 注入。${}是字符串直接替换MyBatis 会把参数值原样拼接到 SQL 语句里。如果参数是用户输入且未经过校验SQL 注入风险极高。我见过一个真实案例。有人在做一个排序功能前端传来排序字段后端用${orderBy}拼在 ORDER BY 后面。结果有人把参数改成id; DROP TABLE sys_user;--整个表差点没了。排序字段这种场景一定要用白名单校验或者用映射表把前端传值转换为黑名单外的固定字段名。正确的使用姿势是能用#{}的地方一律用#{}。只有表名、列名、排序字段这类 SQL 结构部分无法用预编译占位符时才考虑${}并且必须做严格的合法性校验。5. 深入源码看执行链路MyBatis 的一行查询背后发生了什么学 MyBatis 到进阶阶段最重要的一个分水岭就是你能不能脱离 API 层面从源码的角度理解一条 SQL 的执行过程。这一章我不会大段贴源码而是把核心的执行链路拆出来讲清楚同时解答面试里最常考的那几个为什么。5.1 从配置文件到 SqlSessionFactoryXML 解析的关键路径回到最初的代码String resource mybatis-config.xml; InputStream inputStream Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory new SqlSessionFactoryBuilder().build(inputStream);它的本质是mybatis-config.xml 里的每一个配置项都会被解析并封装到 Configuration 对象里。settings里的每个 setting会被设置到 Configuration 的属性上typeAliases注册的所有别名会被存进 TypeAliasRegistrymappers里声明的 Mapper 文件会被解析成 MappedStatement 对象并注册到 Configuration 中。MappedStatement是理解 MyBatis 的一个关键概念。它封装了一条 SQL 的所有信息SQL 语句本身、参数类型、返回类型、是否动态 SQL、缓存配置等。每个 Mapper XML 里的一个select、insert节点在 MyBatis 内部就是一个 MappedStatement以namespace.id为 key 存放在 Configuration 的 Map 里。这也是为什么 namespace 不能重复、id 不能重复一旦重复后面的 MappedStatement 会覆盖前面的运行阶段就会拿到错误的 SQL。5.2 Mapper 接口和动态代理接口没有实现类但 MyBatis 偷偷造了一个新手最大的疑问是Mapper 接口没有实现类怎么就能直接调用答案是MyBatis 使用了 JDK 动态代理。当你调用session.getMapper(SysUserMapper.class)时MyBatis 会为这个接口创建一个代理对象。这个代理对象实现了InvocationHandler接口的invoke方法。当你调用mapper.selectById(1L)时实际上是调用了invoke在这个方法内部根据方法名找到 Configuration 里对应的 MappedStatement解析参数如果有Param就按注解名组装参数 Map交给 Executor 去执行这条 SQL这一整套流程对开发者的感官来说就是调了个普通接口方法但底层全是对 MappedStatement 的查找和 SQL 执行。这就是 MyBatis 最巧妙的设计之一你不写实现类框架在运行时帮你实现。5.3 Executor 执行器缓存、SQL 解析和 JDBC 操作的真正执行者Executor 是 MyBatis 的执行核心。MyBatis 有几种 Executor 类型SimpleExecutor、ReuseExecutor、BatchExecutor默认是SimpleExecutor。Executor 的执行过程大致是检查一级缓存命中则直接返回检查二级缓存命中则直接返回没命中则创建StatementHandlerStatementHandler负责创建 JDBC 的Statement、设置参数ParameterHandler负责把 Java 参数绑定到 SQL 参数的?上执行查询拿到ResultSetResultSetHandler把结果集映射为 Java 对象对象存入一级缓存然后返回这里有个很经典的面试考点如果你的方法开启了二级缓存执行顺序是先查二级缓存再查一级缓存最后才查数据库。很多人答反了把一级缓存放在了最前面这会在面试时立刻露馅。5.4 TypeHandlerJava 类型和数据库类型之间的翻译官从 JDBC 层面看ResultSet.getString()返回字符串ResultSet.getLong()返回长整型。但你的 Java 实体类属性可能是LocalDateTime、Boolean甚至自定义枚举类型。谁负责转换答案是TypeHandler。MyBatis 内置了一批常用的 TypeHandler比如LocalDateTimeTypeHandler、BooleanTypeHandler、EnumTypeHandler等。当你写#{nickname}时MyBatis 根据 Java 参数的类型自动选择合适的 TypeHandler 来设置 JDBC 参数当结果集映射到对象时反过来用 TypeHandler 从 ResultSet 取值。如果遇到内置 TypeHandler 搞不定的自定义类型可以实现BaseTypeHandler注册成一个自定义 TypeHandler在mybatis-config.xml里配置到typeHandlers中。这个功能在项目中处理 JSON 字段、加密字段时特别好用。5.5 Plugin 插件机制拦截器模式的经典应用MyBatis 的插件机制是基于 JDK 动态代理实现的拦截器。它可以拦截四类核心对象的方法Executor、ParameterHandler、ResultSetHandler、StatementHandler。像 PageHelper 分页插件就是拦截了 Executor 的 query 方法在执行 SQL 之前自动拼上LIMIT条件。理解插件机制对进阶很重要因为你以后写公共组件时很可能用得上。比如统一给所有查询 SQL 追加租户条件或者给敏感字段做自动加解密都可以通过插件实现。但注意插件拦截的是核心流程乱用会影响性能和稳定性生产环境要极度谨慎。5.6 高频面试题串讲如果你理解了上面这些下面这些题都能答MyBatis 的一级缓存和二级缓存有什么区别一级缓存是 SqlSession 级别的默认开启二级缓存是 Mapper 级别的需要配置。任何增删改都会清空一级缓存二级缓存在多表关联时容易产生脏数据。#{}和${}的区别#{}走预编译防 SQL 注入${}直接拼接有注入风险仅可用于表名、列名等结构位置且需校验。MyBatis 是如何做到接口没有实现类也能执行的通过 JDK 动态代理InvocationHandler 里根据方法名找到 MappedStatement然后交给 Executor 执行。MyBatis 的分页原理是什么如果是内存分页用 RowBounds数据全查出来再内存截取效率低生产上一般用 PageHelper它基于插件拦截 Executor 的查询自动改写 SQL 追加 LIMIT。MyBatis 延迟加载的实现原理默认关闭通过动态代理创建代理对象真正访问关联属性时才触发查询。注意开启延迟加载需要lazyLoadingEnabledtrue和 CGLIB 依赖。MyBatis 的一级缓存有失效场景吗有SqlSession 关闭、执行增删改、手动清空缓存、查询条件不同、不同 SqlSession 等都会导致失效。6. MyBatis 与 MyBatis-Plus 对比进阶路上的选型分析学到这你已经掌握了 MyBatis 的核心。但现实项目里你大概率还会遇到 MyBatis-Plus。很多招聘 JD 上写熟悉 MyBatis 或 MyBatis-Plus面试也常问两者区别。这一章我把两者的关系、优势、坑和选型建议一次说清楚。6.1 MyBatis-Plus 到底是啥它不是替代品而是增强工具包MyBatis-Plus简称 MP是 MyBatis 的一个增强工具它没有改变 MyBatis 的核心机制而是在 MyBatis 之上做了大量封装提供了很多开箱即用的功能。它的定位是只做增强不做改变所以你在 MyBatis 里写的所有配置、SQL、Mapper XML在 MyBatis-Plus 里依然完全适用。这一点是理解两者关系的关键。用一句通俗的话总结MyBatis 是手动挡MyBatis-Plus 是自动挡。自动挡的车底子里仍然是发动机、变速箱、离合器只不过帮你省掉了繁琐的换挡操作。6.2 核心区别对比单表 CRUD、分页、代码生成器最大的区别在单表 CRUD。MyBatis 里你想查一张表的所有字段需要手写SELECT * FROM table WHERE id ?每个表都要写一遍。MyBatis-Plus 提供了一个BaseMapper基类你只要让你的 Mapper 接口继承它就自动拥有了selectById、selectList、insert、updateById、deleteById等一堆方法连 SQL 都不用写。分页方面MyBatis 需要引入 PageHelper 插件才能物理分页MyBatis-Plus 内置了分页插件只需配置一个MybatisPlusInterceptorBean然后用它提供的Page对象即可PageSysUser page new Page(1, 10); PageSysUser result sysUserMapper.selectPage(page, new LambdaQueryWrapperSysUser().like(SysUser::getUsername, admin));条件构造器QueryWrapper和LambdaQueryWrapper是 MyBatis-Plus 的另一大杀器。它用 Java 代码的方式拼接查询条件替代了 XML 里一部分动态 SQL。比如ListSysUser users sysUserMapper.selectList( new LambdaQueryWrapperSysUser() .eq(SysUser::getStatus, 1) .lt(SysUser::getCreateTime, LocalDateTime.now()) .orderByDesc(SysUser::getId) );这段代码等于SELECT * FROM sys_user WHERE status 1 AND create_time ? ORDER BY id DESC。代码可读性好而且因为有 Lambda 表达式的类型引用字段名安全不怕数据库列改名后编译期发现不了。6.3 选型建议什么场景用 MyBatis什么场景用 MyBatis-Plus如果你的项目是全新项目、以单表 CRUD 为主、需要快速交付我建议直接上 MyBatis-Plus。它能显著减少样板代码而且团队里哪怕是个刚毕业的实习生半天就能上手。如果你的项目是老项目、复杂查询非常多、多表联查眼花缭乱MyBatis-Plus 也依然是合理的选择因为 MyBatis 的能力它全都有。你会写 MyBatis 的 XML在 MyBatis-Plus 里照样能写。换句话说先精通 MyBatis再使用 MyBatis-Plus几乎没有学习成本。这里我必须强调一个可能挨骂但很真实的观点不要因为用了 MyBatis-Plus 就不学 MyBatis。面试的时候MP 的自动 CRUD 很容易解释但MyBatis 的缓存机制是什么一条 SQL 是怎么执行出来的这类问题只有真正学懂 MyBatis 才能答得出来。MP 只是提高效率的工具MyBatis 才是理解持久层的根本。而且如果遇到特别复杂的查询优化你最终还是要回到手写 SQL 和 XML 的层面。MyBatis 的功底就是你在架构设计、性能优化这些更高阶问题上的底气。7. 关于学习路径和常见误区我的个人体会写到这里我想花一点篇幅聊聊学习 MyBatis 过程中最常见的学习误区和路径建议。这些东西不是文档里写的是我踩过坑、也带过不少人之后总结出来的。7.1 最常见的三个误区照抄配置、忽略日志、不敢看源码第一个误区是能跑就行配置全靠抄。网上大量教程给的 mybatis-config.xml 都只包含最小配置很多新手抄完就跑通了然后就不再深究每个标签的含义。结果项目一换环境、一换数据库就完全不知道怎么调试。我对自己的要求是每一个配置项都要能解释清楚它存在的意义。不知道为什么存在的配置就不要写。第二个误区是不看日志直接猜问题。我见过太多人 SQL 查不出数据第一反应是改代码、加断点而不是先看一眼控制台日志。MyBatis 的日志已经把 SQL、参数、返回条数全打出来了这几乎是排错的第一手信息。每次遇到查询问题我的习惯是先看 SQL 是否正确再看参数是否正确最后才怀疑映射配置。用了这个顺序90% 的问题在一分钟之内就能定位。第三个误区是觉得源码高不可攀。说到读 MyBatis 源码很多人的第一反应是我还没到那个水平。其实阅读源码不一定要从头读到尾可以从一个入口方法比如SqlSessionFactoryBuilder.build开始跟着调用链往下走。MyBatis 的代码在开源框架里算是非常清晰易读的类和方法的命名都很有语义化。花一个周末时间跟着断点走一遍selectById的完整流程你对这个框架的理解会超过 90% 的开发者。7.2 我给新人的 MyBatis 学习路径建议如果你完全是零基础我建议按这个顺序走第一步用一周时间补齐 SQL 基础。重点是多表联查、分组、聚合函数。不要用图形化工具点点点直接在命令行或数据终端里敲 SQL。第二步用一周时间照着本章第二节的内容把 MyBatis 第一个程序跑通。不要复制粘贴每一个字母都亲手敲一遍。这个过程中遇到的所有报错都值得你花时间搞懂原因。第三步用一周时间把第三章、第四章的内容吃透。重点是掌握 Mapper XML 的各种标签、动态 SQL、缓存机制。每学一个知识点就写一个小的 demo 验证它。第四步如果你还有精力就进入第五章的源码阅读阶段。不需要理解全部代码关键是能讲清楚执行链路。第五步学习 Spring Boot 整合 MyBatis。现在的 javaWeb 项目基本都是 Spring Boot MyBatis 的组合把两者整合起来通过mybatis-spring-boot-starter你会发现在纯 Java 环境里学的东西全部派上了用场。7.3 最后一个建议一定要手写一遍完整的项目学习框架最忌讳的是只看不写、只背不练。我见过太多人刷了一堆 MyBatis 面试题让他现场写一个selectById的 Mapper XML结果连namespace都写不对。知识只有经过手写、报错、修复这个过程才会真正变成你自己的。我个人的建议是无论你的目标是找工作还是做项目都抽出几天时间手写一个基于 JavaWeb MyBatis 的完整案例比如一个最简版的后台用户管理模块功能包括用户列表分页查询、按条件搜索、新增用户、修改用户信息、批量删除。这个项目做完你的 MyBatis 基础就相当扎实了后面学 Spring Boot、学 MyBatis-Plus都只是在这个地基上添砖加瓦而已。