多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MyBatis核心机制与缓存实战:从JDBC到Spring Boot的完整解析

MyBatis核心机制与缓存实战:从JDBC到Spring Boot的完整解析 身边搞Java的朋友十有八九都跟MyBatis打过交道。不管你是刚入行还在纠结JDBC模板代码还是已经在Spring Boot项目里把MyBatis用得飞起这个框架几乎成了国内Java后端绕不开的标配。但要真说“懂”MyBatis很多人其实是处于一种“会用但说不清”的状态知道写Mapper接口、写XML知道有缓存但一级缓存和二级缓存到底怎么工作、什么时候失效、Spring Boot里面为什么加个注解就能扫描到Mapper这些问题一问就卡壳。这篇内容我打算把MyBatis从核心机制到缓存实现、从SQL日志配置到源码阅读路线完整梳理一遍。不是教科书式的概念罗列更多是我自己这些年实际使用、排查问题、面试别人和被面试时积累下来的理解。无论你是准备面试还是想在项目里把缓存和日志配置用得更明白都能从里面找到能直接拿去用的东西。1. 先说清楚MyBatis到底解决了什么问题1.1 从JDBC的痛说起Java后端最早操作数据库用的是JDBC原生API。第一眼看上去还挺正规注册驱动、获取连接、创建PreparedStatement、执行查询、遍历ResultSet、手动关闭连接。但写多了就发现全是重复劳动。尤其是查询逻辑复杂一点ResultSet里字段要一个一个手动取出来往实体类里塞写一百行代码可能九十五行都在做机械赋值。// 典型的JDBC查询代码我早期项目里到处都是 Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn dataSource.getConnection(); ps conn.prepareStatement(select id, name, age from user where id ?); ps.setLong(1, userId); rs ps.executeQuery(); User user new User(); while (rs.next()) { user.setId(rs.getLong(id)); user.setName(rs.getString(name)); user.setAge(rs.getInt(age)); } return user; } finally { if (rs ! null) rs.close(); if (ps ! null) ps.close(); if (conn ! null) conn.close(); }这段代码看着不长但一个项目里可能有几十上百个这样的方法。每次加一张表、加一个查询就得把这个流程重新写一遍。更麻烦的是如果表结构字段改了所有对应的实体类赋值代码都得跟着改漏一个就是线上问题。MyBatis做的事情本质上就是把“SQL执行”和“结果映射”这两件事做了一个框架级的封装。你只需要定义好Mapper接口、SQL语句以及实体类的映射关系框架来负责创建连接、执行语句、把结果集自动映射成对象。表现在代码上就是我们非常熟悉的写法public interface UserMapper { User selectById(Param(id) Long id); }XML里写一条SQL接口方法直接调用。程序员从重复的模板代码里解脱出来关注点可以全部放在SQL本身和业务逻辑上。1.2 MyBatis和Hibernate的路线之争聊MyBatis难免提到Hibernate。很多刚接触的人会问既然Hibernate能完全自动映射为什么还要用MyBatis这种半自动的框架我的理解是MyBatis把SQL的掌控权明明白白交回到开发者手里。我们团队做过好几个报表类项目SQL要关联四五张表还涉及各种条件分支、函数嵌套。Hibernate的HQL或者Criteria写起来非常别扭生成的SQL有时候连DBA都看不懂性能一有问题根本没法快速定位。换成MyBatis就直白多了SQL是我们自己写的执行计划有问题直接在SQL层面调优。代价就是你要自己维护SQL不能像Hibernate那样完全小于等于“对象操作数据库”的舒适区。但对追求可控性和查询性能的团队来说这个取舍完全值得。MyBatis在国内普及度这么高恰恰是因为很多业务系统的查询复杂度远超CRUD手写SQL反而是一种优势。2. MyBatis核心机制拆解SqlSession与Mapper动态代理2.1 SqlSession的生命周期很多人从一开始就搞错了MyBatis最核心的入口就是SqlSession。你可以把它理解成一次数据库会话的抽象类似于JDBC里一个Connection的“升级版”。它负责执行SQL、获取Mapper代理、管理事务、控制一级缓存。我在代码里看到过不少直接把SqlSession当成全局对象用的写法在工具类里定义一个静态SqlSession所有Mapper都从它身上拿。这种写法通常能跑但隐患非常大。SqlSession不是线程安全的内部还绑定了一级缓存和事务状态如果多个线程共享同一个SqlSession轻则缓存数据错乱重则连接池被污染导致连接迟迟不归还。正确做法是短生命周期使用。一次请求需要操作数据库就创建一次SqlSession用完立刻关闭try (SqlSession session sqlSessionFactory.openSession()) { UserMapper mapper session.getMapper(UserMapper.class); User user mapper.selectById(1L); // 业务处理 }在Spring集成环境中这个生命周期由框架来管理一般不手动操作。但面试的时候我经常会问候选人一个问题Spring整合MyBatis之后SqlSession是每次都新建还是一次请求共用这个问题能看出你是否理解MapperProxy的动态代理机制以及SqlSessionTemplate的存在意义。2.2 Mapper接口为什么不需要写实现类这是MyBatis里最让人好奇的设计之一。你定义了一个接口没写任何实现类却能在Service里直接注入进来用。关键就在于动态代理。MyBatis启动时会扫描Mapper接口针对每个接口生成一个代理对象放进Spring容器。当你调用接口里的方法时实际上调用的是MapperProxy的invoke方法。它会根据你调用的方法名在配置里找到对应的MappedStatement——也就是你写的SQL语句的封装对象然后交给SqlSession执行。// 从SQLSession getMapper的调用链来看 public T T getMapper(ClassT type, SqlSession sqlSession) { MapperProxyFactoryT mapperProxyFactory (MapperProxyFactoryT) knownMappers.get(type); return mapperProxyFactory.newInstance(sqlSession); }这也就解释了为什么Mapper接口的方法名必须和XML里SQL的id保持一致。id不是随便写的它是MappedStatement的唯一标识。方法名对应id参数对应SQL里的#{}占位符返回值对应resultType或resultMap。理解了这条链路你就能明白为什么Mapper接口不能重载——因为方法名直接映射到SQL id重载会导致无法区分到底该绑定哪条SQL。2.3 参数映射与结果映射的细节MyBatis参数映射里最有用也最容易被忽视的就是#{}和${}的区别。很多人知道#{}是预编译占位符${}是字符串拼接但再往深问一句何时必须用${}反而答不上来。-- 表名、排序字段这类结构信息只能用 ${} select * from ${tableName} order by ${orderColumn} -- 传值参数应该老老实实用 #{} select * from user where name #{name}核心原因在于JDBC的预编译占位符只能绑定值不能绑定表名、列名、排序方向这类SQL结构。所以凡是涉及动态表名、动态排序字段的SQL只能用${}。这也是SQL注入的高发点。使用${}时所有输入必须自己做白名单校验绝不直接信任前端传来的字符串。我见过不止一次因为排序字段用了${}且未校验被人在请求参数里塞了恶意SQL导致数据库数据被删的案例。结果映射方面MyBatis默认按列名和属性名做映射开启mapUnderscoreToCamelCase之后数据库的user_name列就能自动映射到userName属性。但遇到复杂嵌套结果比如一对多、多对多就要靠resultMap里的association和collection了。这里的性能陷阱很明显N1查询通常是不合理嵌套查询导致的。3. 缓存机制一级缓存与二级缓存实现3.1 一级缓存默认开启但容易踩坑MyBatis的一级缓存默认是开启的作用域是SqlSession。同一个SqlSession里执行两次完全相同的查询第二次不会真正访问数据库而是直接从本地缓存里拿结果。这听起来很美好但坑也在这。先看一个经典问题在同一个SqlSession里第一次查询用户返回对象A然后执行了一个update操作修改了同一条记录接着再去查询同一个用户拿到的还是第一次查询的缓存对象。因为MyBatis的规则是执行了增删改操作后会清空一级缓存。所以如果你在两次查询之间做过update第三次查询确实会重新查库。但如果期间没有增删改缓存就一直存在。很多会出问题的地方在于Spring整合MyBatis之后不同方法通常是不同SqlSession的。如果你在一个事务里通过同一个SqlSession查询两次第二次走了缓存但你手动修改了第一次查询返回的对象接着第二次查询拿到了同一个对象引用数据已经被你污染了。这种隐性问题排查起来非常费劲。我一般建议明确一级缓存只作为会话级别的优化手段不要依赖它来实现业务上的数据一致性。需要强一致性的场景直接忽略缓存每次查询都走数据库。// 同一SqlSession下的两次查询结果完全一致 SqlSession sqlSession sqlSessionFactory.openSession(); UserMapper mapper sqlSession.getMapper(UserMapper.class); User user1 mapper.selectById(1L); User user2 mapper.selectById(1L); // user1 user2 是true第二次走的是缓存看到这里有人会问那怎么强制不走缓存呢可以用sqlSession.clearCache()手动清空或者把查询设置到不同SqlSession里执行。实际项目中正常使用Spring管理的SqlSessionTemplate时每个Mapper方法默认拿到的SqlSession可能不同所以一级缓存的问题频率并不高。但如果手写代码时没注意就很容易出现。3.2 二级缓存实现配置细节、序列化问题与脏读风险二级缓存的作用域是Mapper级别的也就是同一个namespace下的所有查询可以共享缓存。它跨SqlSession生效默认是关闭的需要手动开启。配置步骤很简单第一步在主配置里设置cacheEnabled为true这个默认就是true。第二步在Mapper的XML里加一行mapper namespacecom.example.mapper.UserMapper cache evictionLRU flushInterval60000 size512 readOnlytrue/ /mapper几个关键参数我解释一下。eviction是缓存回收策略默认是LRU即最近最少使用淘汰。flushInterval是刷新间隔单位毫秒到了指定时间自动清空缓存这里设置60000就是60秒刷新一次。size是缓存最多缓存多少个对象引用。readOnly设为true时缓存返回的是同一个对象实例性能好但存在数据安全隐患设为false时MyBatis会序列化再反序列化出副本返回安全性高。这里必须提醒两件事。第一件是序列化问题。如果readOnly设为false那么缓存对象对应实体类必须实现Serializable接口否则反序列化报错。很多人在配置二级缓存后一执行查询就抛异常查了半天发现是实体类没实现序列化接口。第二件是脏读风险这是更严重的坑。MyBatis的二级缓存是namespace级别的如果你用多表联查比如UserMapper里查了订单表的数据同时订单表又被OrderMapper做了更新操作那UserMapper的二级缓存并不会因为OrderMapper的更新而失效。结果就是UserMapper里缓存了旧的数据拿到客户端去展示用户看到的就是脏数据。解决这个问题的方案有几种。我常用的做法是对涉及多表联查的Mapper干脆不开启二级缓存或者把所有关联查询都放到同一个namespace下统一管理。还有一个更彻底的选择是引入外部缓存中间件比如Redis直接放弃MyBatis自带的二级缓存。实际上我接触的几个线上高并发项目基本都是这个思路。本地二级缓存维护成本高、节点间数据不一致问题多远不如Redis缓存可控。!-- 一个多表联查的例子容易产生脏读 -- select idselectOrderWithUser resultTypemap select o.id, o.total, u.name from t_order o left join t_user u on o.user_id u.id /select这种SQL写在UserMapper里二级缓存会缓存结果。但如果用户修改了昵称UserMapper的缓存并不会自动刷新除非显式调用了clearCache。3.3 缓存失效场景实战记录有一回线上反馈后台改了用户手机号但前端页面显示的还是旧号码。第一反应是Redis缓存问题排查了一圈发现Redis缓存时长设的10分钟但用户等了半小时依然显示旧号码。后来把焦点放到MyBatis二级缓存上。这个项目没引入外部缓存中间件完全靠MyBatis自带二级缓存撑着。定位后发现更新用户手机号的接口调用的是CustomerMapper而查询用户详情的接口走UserMapper。两个Mapper的namespace不同虽然操作的是同一张表但缓存之间完全没有关联。CustomerMapper执行update之后清空的只是CustomerMapper的缓存UserMapper里的缓存依然顽固地存在。那次修完之后我对MyBatis二级缓存的结论就一句话只在纯单表操作、且对数据实时性要求不高的场景下使用否则宁可不用。4. SQL日志配置打印让每条SQL都清清楚楚4.1 最基础的配置方式MyBatis配置打印SQL本质上是开启日志框架对MyBatis包的DEBUG级别输出。用的日志实现不同配置方法也不同。采用Logback的项目在logback.xml里加logger namecom.example.mapper levelDEBUG/这里的包名最好定义成存放Mapper接口的包。这样日志里能直观看到每个Mapper的执行情况。如果直接配置成org.mybatis的DEBUG级别会把框架内部启动信息也刷出来日志量大且没必要。如果你用的是Spring Boot这个配置通常就够了。如果项目里同时存在多个数据源或者多个Mapper包可以分别指定。4.2 打印带参数的完整SQL很多人配置完上面那步发现SQL是打印出来了但参数值却看不到。MyBatis默认打印的SQL模板是带占位符的 Preparing: select id, name, age from user where id ? and name ? Parameters: 1(Long), 张三(String)能到这一步其实已经够用了。但调Bug的时候特别是复杂条件查询我更希望能看到可以直接复制执行的完整SQL。实现方式有几种我常用的方案是引入p6spy。这是个SQL拦截打印组件它能输出完整的、带实际参数的SQL语句还会打印执行耗时。p6spy的使用很简单引入依赖然后修改数据源驱动配置。把原来的URL和Driver都替换成p6spy的包装版本spring.datasource.driver-class-namecom.p6spy.engine.spy.P6SpyDriver spring.datasource.urljdbc:p6spy:mysql://localhost:3306/shop再在classpath下放一个spy.properties文件配置日志输出格式、过滤掉一些无用的语句。做完这一步你就能在控制台看到类似select id, name, age from user where id 1 and name 张三这种可直接执行的SQL。排查问题效率会提升不止一个档次。4.3 参数占位符的特殊情况配置打印SQL的过程中我还遇到过#{}和${}打印差异的问题。如果你的SQL里用了${}拼接动态表名那打印出来的SQL就是完整拼好的不需要额外还原。但如果全是#{}参数即使p6spy帮你还原了参数也要注意时间类型参数的格式。比如LocalDateTime类型的参数p6spy默认打印出来可能是带T的ISO格式直接复制到Navicat里执行有时会报错。这时候要注意看日志配置里有没有针对时间格式的处理没有的话手动调整一下即可。还有一个小技巧很多时候我们需要打印SQL执行耗时。MyBatis本身的日志不输出耗时但如果用了druid连接池它的DruidFilter里自带慢SQL统计。配置一下慢SQL阈值spring.datasource.druid.filter.stat.slow-sql-millis500超过500毫秒的SQL就会单独记录下来。这是排查慢查询最有效的途径比你自己看日志肉眼找快得多。我每次接手一个老项目的第一件事就是把慢SQL统计打开先看一遍TOP N慢查询心里就有底了。5. Spring Boot MyBatis集成实战从配置到避坑5.1 依赖引入与基础配置Spring Boot集成MyBatis现在走的是mybatis-spring-boot-starter这条线。引入方式很固定dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency注意版本号要跟Spring Boot版本匹配。Spring Boot 3.x时代用3.0.3Spring Boot 2.x时代用2.3.x这样的版本。版本不匹配会导致自动配置失效最典型的现象是Mapper注入报错或者SqlSessionFactory初始化失败。配置方面最常用的是这几项mybatis.mapper-locationsclasspath:mapper/*.xml mybatis.type-aliases-packagecom.example.entity mybatis.configuration.map-underscore-to-camel-casetruemapper-locations指定XML文件的位置。type-aliases-package能让你在XML里写resultType时只写类名不用写完整包路径。map-underscore-to-camel-case这个配置我强烈建议打开否则每个字段都要手动写resultMap非常痛苦。5.2 Mapper扫描的两种方式Spring Boot里让容器感知Mapper接口有两种常用方式。一种是在启动类或者配置类上使用MapperScanMapperScan(com.example.mapper) SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }另一种是直接在Mapper接口上加Mapper注解然后在启动类上没有MapperScan也能生效。实际项目中我更推荐MapperScan因为它是批量扫描不用每个接口都加注解代码看起来干净。而且MapperScan支持指定多个包扫描路径可以灵活控制。这里有个容易踩的坑如果多个Mapper目录里有重复的接口名或者XML文件的id冲突启动时可能不报错但运行时查出来的数据就会牛头不对马嘴。遇到过几次之后我给自己定了条规矩XML的namespace必须完整写成接口的全限类名不要偷懒这样至少能保证启动阶段就发现重复绑定问题。5.3 分页插件与多数据源配置几乎所有实战项目都会用到分页。MyBatis里最常用的分页方案就是PageHelper。它使用ThreadLocal原理把你的分页参数绑定到当前线程然后拦截后续SQL自动拼接limit。用起来很简单PageHelper.startPage(1, 10); ListUser users userMapper.selectAll(); PageInfoUser pageInfo new PageInfo(users);但PageHelper有几个使用细节必须注意。第一startPage之后必须紧跟第一个查询中间不能穿插其他耗时逻辑否则分页参数会被下一个查询消费掉。第二多数据源环境下PageHelper的分页只会对拦截到的数据源生效。第三如果SQL里已经有limitPageHelper再拼接一次limit就会出现双重分页的Bug。多数据源的配置在集成MyBatis时稍微复杂一些。我需要分别配置两个DataSource、两个SqlSessionFactory、两个MapperScan配置文件里分别指定不同的包路径和XML路径。这个场景下我建议把每个数据源对应的Mapper包分开放千万别混在一起不然事务管理会乱成一锅粥。6. 高频面试题速查从缓存问到源码6.1 面试必问的几个问题MyBatis相关面试题里出现频率最高的是下面这么几个。我整理时候也顺便把答题要点标了出来。第一个是关于#{}和${}的区别。答题时抓住预编译和字符串拼接这个核心区别然后补充说明${}的SQL注入风险以及动态表名排序字段只能用${}的场景基本就能覆盖绝大部分考点。第二个是MyBatis执行流程。这个问题考察的是整体理解。可以从SqlSessionFactory读取配置开始说到SqlSession的创建Mapper代理的获取MappedStatement的查找Executor执行器处理SQL再到StatementHandler、ParameterHandler、ResultSetHandler四大组件的协作。能把这条链路讲清楚基本功就过关了。第三个是一级缓存和二级缓存。答题关键点在于讲清楚一级缓存是SqlSession级别的、无法跨SqlSession共享、增删改会清空缓存二级缓存是Mapper namespace级别的、需要手动开启、存在脏读风险。如果面试官继续追问生产环境中缓存方案怎么选可以顺势说出外部缓存中间件的优势会加分不少。第四个是映射器Mapper接口为什么不能重载。这题的答案在于方法名绑定SQL id的机制。方法重载会导致方法名相同MyBatis无法区分到底该关联哪一条SQL所以MyBatis的Mapper接口不允许重载。第五个是MyBatis中Executor的作用。它处在核心位置所有SQL执行最终都经过Executor。Executor有几种类型简单执行器SimpleExecutor、复用语句的ReuseExecutor、批量操作的BatchExecutor。Spring Boot默认使用SimpleExecutor可以配置defaultExecutorType切换。缓存相关的装饰器模式也体现在Executor上比如CachingExecutor就是给Executor套上了一层缓存能力。6.2 源码阅读路线建议想深入MyBatis源码的我建议按这个顺序来读。先看SqlSessionFactoryBuilder理解配置是怎么被解析的。再看XMLConfigBuilder重点看它如何把mybatis-config.xml里的元素解析成Configuration对象。接着看Configuration类它是全局配置的“大字典”里面包含了MappedStatement、ResultMap、Cache等所有注册信息。看完这两个你会对MyBatis的初始化逻辑有一个完整认知。然后看MapperProxy和MapperProxyFactory理解动态代理。再看MappedStatement它用来封装一条SQL的所有元信息包括SqlSource、StatementType、ResultMaps。紧接着可以进入四大组件阶段Executor、StatementHandler、ParameterHandler、ResultSetHandler。你会发现整个SQL执行流程就是一条责任链每个环节各司其职。最后可以单独看缓存相关源码重点看PerpetualCache、LruCache、SerializedCache这些类。它们是二级缓存的实现基础。理解了LruCache的回写机制你就能回答出淘汰策略到底是怎么触发的。6.3 面试答题思路参考单纯背答案效果不好我给你一个答题结构的参考。面试官问你MyBatis的问题回答时可以套用“定义—场景—原理—细节”这个框架。拿“MyBatis是什么”举例。定义部分它是一个半自动的ORM框架专注于SQL的灵活控制与结果映射。场景部分适合SQL复杂、需要对执行性能精细控制的场景。原理部分底层使用JDBC通过动态代理生成Mapper实现通过Xml或注解维护SQL与方法的映射。细节部分讲一个你在实际项目中踩过的坑比如二级缓存脏读或者分页插件误用。这么一段下来深度和实战感都体现出来了。7. 一个更实用的配置Mybatis配置打印与多环境切换7.1 不同环境的日志级别控制项目部署到生产环境不可能像开发环境那样把SQL全部打到日志里去日志量太大会影响性能。所以MyBatis的SQL打印应当只在开发环境和测试环境开启。我习惯的做法是配置logback的springProfile来区分环境springProfile namedev logger namecom.example.mapper levelDEBUG/ /springProfile springProfile nameprod logger namecom.example.mapper levelINFO/ /springProfile这样同样一套配置部署到dev环境自动打印SQL生产环境只打印INFO级别的日志干净又安全。还有个小细节生产环境即使用DEBUG级别打印SQL也一定不要开着p6spy它的性能损耗在低并发时看不出来高并发下直接放大接口响应时间。7.2 一个自定义拦截器的思路如果想在SQL执行前后做统一处理比如记录操作日志或者修改某些字段可以写一个MyBatis的Interceptor插件。实现Interceptor接口加上Intercepts注解指定拦截目标然后在intercept方法里通过Invocation对象拿到当前执行的SQL语句做统一处理。我写过最常用的一个拦截器是自动填充创建时间和修改时间的。数据库表里这两个字段几乎是每张表必有的但每次插入或更新时手动set非常容易漏。用拦截器统一处理在SQL执行之前判断当前操作是insert还是update然后自动把当前时间设置到对应的参数对象里。从那之后新表直接加字段代码里一行都不用改。这也是大家常说的公共字段自动填充的底层逻辑。如果你用的是MyBatis-Plus这个功能一个注解就搞定了。但如果你坚持用原生MyBatis那拦截器就是绕不开的路径。通过读源码理解Interceptor机制再落地成一个公共字段填充插件这个过程特别能锻炼人对MyBatis内部运行机制的把控能力。7.3 从配置到代码的细节习惯用MyBatis这几年我养成了几个习惯。第一个是XML里尽量不写复杂的动态SQL嵌套如果某个查询的条件组合超过三种我会拆成多个查询方法而不是在一个SQL里堆一堆if判断。可读性和维护效率远比减少那几行代码重要。第二个是SQL的别名规范多表查询时每张表都给固定缩写别名关联字段一律带上表别名这样执行计划分析的时候一眼就能看清字段来源。第三个是尽量把常用查询和更新语句写简单、写精准能用一条SQL完成的就不要拆成两条在代码里做二次计算。数据库的压力控制很多层面是靠这些平时的细节积累出来的。8. 个人实际使用中的几个心得体会写到这里再分享几个我在项目实践中总结出来的零散心得。第一刚接手一个老项目时建议先把Mapper XML从头到尾扫一遍。重点看三件事有没有select *有没有懒加载关联有没有不合理的大表全量查询。这三个问题基本能覆盖掉大部分SQL性能隐患。你会发现很多老项目的SQL写得非常随意几张几十万行的表直接join没有任何索引提示就是靠数据库硬扛。这根本不是MyBatis的问题是写SQL的人没把运维当回事。第二关于Mapper接口和XML文件的管理上我一直保持一个原则每个Mapper接口对应一个XML文件文件的命名、存放路径全部一致绝不出现多个XML文件塞进同一个目录却没按Mapper区分的写法。如果你的项目里出现了两个接口共用一个XML的情况趁早拆开不然后面做代码搜索和重构时会疯掉。第三二级缓存以及外部缓存的选择一定要在项目设计阶段就定好不要等数据一致性出问题以后再来补。前文提到的脏读案例如果当时我们在设计阶段就明确所有Mapper不开启二级缓存、统一走Redis缓存就不会有后续的半夜排查事故了。MyBatis自带的二级缓存本身是个很好的机制但它有自己的适用边界不适合就果断放弃不要舍不得。第四框架本身的知识和学习任何工具一样永远是围绕实际问题去学才记得牢。如果你正在准备MyBatis的技术面试不要死记硬背答案把面试题当成一个个待验证的小项目启动一个Spring Boot工程把目标场景复现出来通过调试一步一步去看框架在干什么。比如你可以在断点里看MapperProxy的invoke方法执行时返回的对象类型观察一级缓存生效时查出来的对象是不是同一个引用验证二级缓存反序列化后独占的对象实例这些从实践里得到的感觉比看十篇源码分析文章都管用。最后一件事团队协作项目里MyBatis XML里SQL的统一风格很重要。如果团队里有的同事喜欢把动态SQL条件都写在WHERE后面用11拼接有的喜欢用 标签自动处理多余条件尽管两种写法都能跑但代码风格的不统一会让后续维护的人非常痛苦。我倾向用 片段来抽离公共查询条件配合 、 标签做条件拼接结构清楚减少重复新同事接手也容易上手。这些度量都是代码之外的管理经验但同样值得在技术文章里占一席之地。关于MyBatis从入门到进阶的核心内容基本就是这些了。用一句话来总结我的整体感受MyBatis的定位从来不是深奥的框架它更像是一把经过了市场检验的工具简单、直接、可控。但正因为简单很多人反而忽略了它内部的精巧和边界。真正把它用明白的人会在SQL执行链路、缓存边界、日志可观测性这些层面都有清晰的认识而这些认识恰恰是区分“会用”和“懂”的分水岭。
返回列表