1. 这不是“背八股”而是吃透MyBatis的底层呼吸节奏你有没有过这种体验面试前狂刷“MyBatis面试题”把“#{}和${}区别”“一级二级缓存机制”“Mapper代理原理”背得滚瓜烂熟结果被问一句“如果SQL执行慢你从MyBatis层能怎么定位”就卡壳或者在Spring Boot项目里改个分页逻辑发现PageHelper不生效查半天才发现是Select注解和XML混用导致的Executor类型冲突——这些都不是“八股文没背熟”而是对MyBatis的运行肌理缺乏真实触感。我带过十几支Java后端团队也做过近百场技术面试。最常看到的问题不是候选人答不出标准答案而是当问题稍微偏移题库边界比如“MyBatis怎么让LocalDateTime存进MySQL不报错”“为什么foreach里用collectionlist有时报空指针”人就立刻失去分析路径。这说明所谓“八股文”本质是对框架设计意图、数据流转链路、配置作用域边界的系统性理解不是名词解释合集。这篇文章不列100道题、不搞填空默写。我会带你从MyBatis最原始的JDBC封装出发一层层剥开它的骨架它如何把一个接口方法调用变成一条可执行的SQLXML里的resultMap到底在内存里生成了什么对象Spring Boot自动装配时SqlSessionFactory和SqlSessionTemplate这两个Bean谁管生命周期、谁管线程安全为什么SelectProvider比XML更难调试这些才是你在真实项目里每天打交道的“呼吸节奏”。关键词里反复出现的“mybatis”“八股文”“springboot整合mybatis”“mybatis原理”指向的从来不是知识点罗列而是工程现场的决策依据——比如你该选MyBatis Plus还是原生MyBatis不是看谁功能多而是看你的团队是否需要动态SQL的绝对可控性或者能否接受Wrapper类带来的编译期类型丢失。后面所有章节都围绕这个原则展开每一个技术点都绑定一个真实场景、一个踩坑瞬间、一个可验证的调试手段。2. 从JDBC裸写到SqlSessionMyBatis不是魔法是精密的胶水很多初学者以为MyBatis是“高级JDBC”这说法不准确。JDBC是规范MyBatis是对JDBC使用模式的标准化封装。要真正理解它必须回到最原始的JDBC代码看清MyBatis究竟替你做了什么、又隐藏了什么。2.1 手写JDBC的5个重复劳动就是MyBatis的诞生理由假设你要查用户列表裸写JDBC是这样的Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(SELECT id, name, email FROM user WHERE status ?); ps.setInt(1, 1); // 手动设参数类型和位置强耦合 ResultSet rs ps.executeQuery(); ListUser users new ArrayList(); while (rs.next()) { User u new User(); u.setId(rs.getLong(id)); // 字段名硬编码改表结构就得改这里 u.setName(rs.getString(name)); u.setEmail(rs.getString(email)); users.add(u); } // 必须手动关闭资源漏一个就内存泄漏 rs.close(); ps.close(); conn.close();这短短十几行藏着5个高频痛点参数绑定脆弱ps.setInt(1, 1)的1是序号SQL一改顺序就崩结果映射冗余每个字段都要rs.getXxx(xxx)表字段多时代码爆炸资源管理易错close()必须在finally块里写三遍新手90%会漏SQL与Java强耦合SQL写死在Java里无法复用、无法审计、无法做静态检查异常处理模板化SQLException处理逻辑千篇一律却要每处重写。MyBatis做的不是发明新东西而是把这5个重复劳动做成可配置、可复用、可拦截的标准化流程。它的核心价值是把开发者从JDBC的语法细节中解放出来专注业务逻辑本身。2.2 SqlSessionMyBatis的“呼吸单元”不是连接池代理很多人误以为SqlSession就是Connection的包装。这是致命误解。SqlSession是MyBatis的工作单元Unit of Work它内部持有一个Executor执行器决定是简单执行、批处理还是缓存查询一个Configuration全局配置快照含所有Mapper注册信息一个Transaction事务管理器控制commit/rollback但不一定持有Connection只有在执行SQL时才从数据源获取用完即还。你可以把它理解成“一次数据库会话的上下文容器”。它的生命周期必须严格管控绝对不能跨线程共享SqlSession不是线程安全的Spring中由SqlSessionTemplate代理实现线程隔离不能长期持有它内部有缓存一级缓存长时间不关闭会导致内存泄漏不能手动创建在Spring环境中必须通过Autowired SqlSessionTemplate注入由Spring管理其创建和销毁。提示如果你在Service里直接new SqlSessionFactory().openSession()恭喜你已经埋下高并发下的连接泄漏炸弹。Spring Boot的MapperScan自动注入的Mapper接口背后正是SqlSessionTemplate在为你做线程安全的SqlSession分发。2.3 Mapper接口的“零实现”之谜动态代理的真实成本当你写UserMapper userMapper sqlSession.getMapper(UserMapper.class);MyBatis做了什么它用JDK动态代理为UserMapper接口生成一个代理类。这个代理类的invoke()方法里核心逻辑是解析方法签名如selectById(Long id)→ 获取对应MappedStatement封装了SQL、参数类型、结果映射等元数据从Configuration中查找该MappedStatement调用Executor.query(ms, parameter, rowBounds, resultHandler)执行查询。关键点在于Mapper接口本身没有实现类所有方法调用都被代理拦截并路由到统一的执行引擎。这意味着你无法在Mapper接口里写业务逻辑比如加个if判断因为根本没地方写所有SQL执行都经过Executor所以插件Interceptor可以在这里做日志、分页、加密等横切操作方法名必须和XML中的id或注解中的SQL ID严格一致否则代理找不到MappedStatement。这就是为什么IDEA有时“勾选不了MyBatis Framework”——它依赖于IDE解析XML或注解生成的MappedStatement元数据。如果XML路径没被Maven资源插件正确拷贝或者注解SQL写错了格式IDE就无法建立方法与SQL的映射关系自然无法提供跳转、补全等智能支持。3. XML与注解双轨制何时该用哪条路取决于你的团队成熟度MyBatis支持XML和注解两种SQL定义方式。网上争论很多但真相是没有优劣只有适用场景。选择依据不是“哪个更酷”而是“你的团队能否承担对应的技术债”。3.1 XML企业级项目的“宪法文件”优势在可治理性XML文件如UserMapper.xml的核心价值在于它把SQL从Java代码中彻底剥离成为独立的、可版本控制、可审计、可静态分析的资产。我们团队曾接手一个金融系统其核心交易SQL全部写在XML里。迁移时发现三个关键收益SQL审查流程化DBA可以在Git PR中直接评论某条SQL的索引使用是否合理而不用翻Java代码动态SQL可测试用MyBatis的SqlSessionFactoryBuilder可以单独加载XML生成BoundSql对象验证ifchoose条件分支是否生成预期SQL历史兼容性强当需要将Oracle迁移到MySQL时只需替换XML中的方言SQL片段Java层零修改。XML的语法糖foreach、set、trim看似复杂实则是对SQL结构化表达的精准建模。比如set标签生成的SQL会自动去掉末尾多余的逗号这比手拼字符串安全得多。但XML的代价也很明显学习曲线陡峭且IDE支持弱于Java代码。比如bind标签绑定变量在IntelliJ中无法跳转到定义处resultMap的嵌套属性映射错误提示往往模糊“Could not find result map”。实操心得在大型项目中我们强制要求所有涉及多表关联、复杂条件、分页逻辑的SQL必须用XML。而单表CRUD、简单查询允许用注解。这样既保证核心SQL的可维护性又不牺牲开发效率。3.2 注解小团队的“敏捷利器”但请警惕它的隐性陷阱Select(SELECT * FROM user WHERE id #{id})看起来清爽但它把SQL和Java代码耦合在一起带来三个隐形风险SQL长度失控当需要写10个字段的INSERT语句注解里堆满字符串可读性暴跌动态SQL无解SelectProvider虽然能动态生成SQL但调试极其困难——你无法在IDE里直接看到最终生成的SQL只能靠日志类型安全缺失#{id}的id是字符串字面量拼写错误如#{ids}编译期不报错运行时才抛BindingException。我们曾有个项目因UpdateProvider方法返回的SQL少了一个空格导致UPDATE user SET name#{name}WHERE id#{id}合并成SET name#{name}WHERE语法错误。排查花了3小时只因日志里打印的是“Provider method returned null”而非真实SQL。因此我的建议很明确注解只用于极简场景单表、无条件、无关联一旦SQL超过5行或包含任何条件分支立刻切回XML。这不是教条而是用血换来的经验。3.3 混用陷阱为什么Select和XML不能共存于同一Mapper接口这是高频踩坑点。当你在一个接口里同时写了public interface UserMapper { Select(SELECT * FROM user WHERE id #{id}) User selectById(Param(id) Long id); User selectByName(Param(name) String name); // 期望走XML }而XML里也有select idselectByName ...结果是selectByName方法永远走注解SQLXML被忽略。原因在于MyBatis的加载优先级注解 XML。MapperRegistry在注册Mapper时会先扫描接口上的注解如果有就直接用注解生成MappedStatement不再去XML里找同名ID。更隐蔽的坑是如果注解SQL写错了比如表名拼错MyBatis不会报错而是静默地用一个空的MappedStatement导致后续执行时报Invalid bound statement。此时你查XML查到崩溃却忘了注解的存在。避坑指南在团队规范中必须二选一——要么全注解要么全XML。混合使用是自找麻烦。Spring Boot的mybatis.mapper-locations配置只对XML生效对注解完全无感。4. 配置文件的权力地图mybatis-config.xml与application.yml的职责边界MyBatis的配置分散在多个地方全局配置文件mybatis-config.xml、Spring Boot的application.yml、Mapper XML、甚至Java代码里。新手常混淆它们的作用域导致“改了这里没效果改了那里又崩了”。4.1mybatis-config.xmlMyBatis的“宪法”定义不可变规则这个文件是MyBatis原生的配置入口它定义的是框架级、全局性、不可覆盖的规则。典型配置项configuration settings setting namelogImpl valueSLF4J/ !-- 日志实现影响整个MyBatis -- setting namemapUnderscoreToCamelCase valuetrue/ !-- 下划线转驼峰全局生效 -- /settings typeAliases typeAlias aliasUser typecom.example.entity.User/ !-- 类型别名XML中可用User代替全限定名 -- /typeAliases plugins plugin interceptorcom.example.plugin.PagePlugin/ !-- 插件影响所有SQL执行 -- /plugins /configuration关键特性不可被Spring Boot覆盖application.yml里的mybatis.configuration.*是对它的补充不是替代影响所有Mapper比如mapUnderscoreToCamelCasetrue所有XML和注解的ResultMap都默认启用插件Plugin的唯一入口分页、日志、加密等横切逻辑必须在这里声明。注意Spring Boot 2.0 默认不加载mybatis-config.xml需显式配置mybatis.config-locationclasspath:mybatis-config.xml。否则你写的插件永远不会生效。4.2application.ymlSpring Boot的“调度室”管理集成细节Spring Boot的配置文件负责的是与Spring生态的集成策略不涉及MyBatis内核逻辑mybatis: mapper-locations: classpath:mapper/*.xml # 告诉Spring去哪里扫描XML config-location: classpath:mybatis-config.xml # 加载MyBatis原生配置 configuration: log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl # 这里配置等价于mybatis-config.xml里的setting type-handlers-package: com.example.typehandler # 类型处理器包路径它和mybatis-config.xml的关系是application.yml是启动器mybatis-config.xml是执行器。前者告诉Spring“怎么集成MyBatis”后者告诉MyBatis“怎么运行SQL”。常见误区把mapUnderscoreToCamelCase写在application.yml里mybatis.configuration.map-underscore-to-camel-casetrue以为和XML里一样——其实效果相同但违背了“宪法文件”的设计哲学在application.yml里配插件mybatis.pluginsxxx这是无效的插件必须在mybatis-config.xml里声明。4.3 Mapper XMLSQL的“宪法实施细则”定义具体执行逻辑每个XML文件是针对一个Mapper接口的具体实施方案。它包含namespace必须和Mapper接口全限定名一致是MyBatis定位Mapper的唯一IDresultMap定义Java对象与数据库字段的映射关系支持嵌套、继承、鉴别器discriminatorsql可复用的SQL片段用include refidxxx/引用cache开启二级缓存配置eviction淘汰策略、flushInterval刷新间隔等。这里有个关键细节resultMap的id属性是它在整个MyBatis上下文中的唯一标识。你可以在其他XML里用resultMapcom.example.mapper.UserMapper.UserResultMap引用它实现跨XML复用。这比在Java里写一堆Results注解清晰得多。实操技巧我们团队约定所有resultMap的id必须是namespace . 逻辑名如UserMapper.UserResultMap。这样在IDE里CtrlClick就能跳转避免命名冲突。5. 缓存机制的双刃剑一级缓存为何总“失效”二级缓存怎么不踩OOM雷缓存是MyBatis性能优化的核心但也是面试和线上事故的重灾区。“一级缓存失效”“二级缓存脏数据”“缓存穿透”等问题根源不在概念而在对缓存生命周期的误判。5.1 一级缓存SqlSession级别的“临时记事本”不是永久存储一级缓存Local Cache默认开启作用域是单个SqlSession实例。它的行为像一个HashMapKeyCacheKey由MappedStatement ID SQL 参数 环境ID等组成Value查询结果List或Object。关键事实它只在同一个SqlSession内有效。Spring中每次调用Mapper方法SqlSessionTemplate都会获取一个新的SqlSession除非你手动开启事务并传播所以一级缓存几乎“看不见”增删改操作会清空缓存执行insert/update/delete时MyBatis会清空当前SqlSession中所有相关MappedStatement的缓存防止脏读它不跨线程、不跨事务即使在同一个事务里不同Service方法拿到的SqlSession也是不同的。所以当你看到“一级缓存失效”大概率是因为没有开启事务Transactional每次方法调用都是新SqlSession在Service里手动sqlSession.clearCache()执行了任何修改SQL触发了缓存清空。验证方法在Mapper方法上加Select(SELECT * FROM user WHERE id #{id})然后在Service里连续调用两次userMapper.selectById(1L)开启事务后第二条SQL不会打印被缓存命中关闭事务两条SQL都会执行。5.2 二级缓存跨SqlSession的“共享记忆”但必须亲手喂养二级缓存Second Level Cache是Mapper级别的多个SqlSession可共享。但它默认关闭需显式开启全局开启mybatis-config.xml中setting namecacheEnabled valuetrue/Spring Boot默认已开Mapper级别开启在XML中添加cache/或在接口上加CacheNamespace实体类实现Serializable因为缓存数据要序列化存储。二级缓存的存储介质是PerpetualCache内存HashMap但生产环境必须替换为Redis等分布式缓存。这时你需要引入mybatis-redis-cache等适配器配置Redis连接池在cache标签里指定typeorg.mybatis.caches.redis.RedisCache。但二级缓存有严重副作用它无法感知数据库的直接变更。比如运维直接DELETE FROM user缓存不会自动失效导致应用读到脏数据。解决方案只有两个主动刷新在关键更新操作后调用Cache.clear()清空对应Mapper的缓存设置超时用cache evictionLRU flushInterval60000/60秒后自动失效。血泪教训我们曾有个订单系统用二级缓存存商品库存结果促销时DBA手动清库存表缓存未失效导致超卖。从此规定所有涉及金额、库存、状态的敏感数据禁用二级缓存必须走实时查询。5.3 缓存插件实战用RedisCache实现分布式缓存MyBatis的二级缓存SPI设计非常优雅。要接入Redis只需实现org.apache.ibatis.cache.Cache接口public class RedisCache implements Cache { private final String id; // Mapper namespace private final RedisTemplateString, Object redisTemplate; Override public void putObject(Object key, Object value) { redisTemplate.opsForValue().set(buildKey(key), value, 30, TimeUnit.MINUTES); } Override public Object getObject(Object key) { return redisTemplate.opsForValue().get(buildKey(key)); } private String buildKey(Object key) { return mybatis: id : key.toString(); } }然后在XML中引用cache typecom.example.cache.RedisCache/注意buildKey()必须包含id即Mapper namespace否则不同Mapper的缓存会互相覆盖。这是新手最容易忽略的点。6. 动态SQL的精密手术刀foreach、set、trim的真实战场动态SQL是MyBatis的灵魂但也是最易出错的部分。foreach循环拼接IN条件、set自动生成UPDATE语句表面是语法糖背后是MyBatis对SQL结构的深度理解。6.1foreach不只是循环是SQL语法的结构化生成foreach的核心属性collection要遍历的集合list、array、map或Param指定的名称item集合中每个元素的别名separator元素间的分隔符open/close整个循环块的包裹符号index下标对List有用。常见错误collectionlist但实际传入的是ArrayListMyBatis无法识别报BindingExceptionseparator,但open(close)导致IN (,1,2,3)多一个逗号。正确写法安全版select idselectByIds resultTypeUser SELECT * FROM user WHERE id IN foreach collectionids itemid open( separator, close) #{id} /foreach /select但更健壮的做法是在Java层预判空集合。因为如果ids为空foreach会生成IN ()MySQL直接报错。所以Service里应加判断if (CollectionUtils.isEmpty(ids)) { return Collections.emptyList(); } return userMapper.selectByIds(ids);6.2set与trimUPDATE语句的“智能组装器”set是trim的特例专为UPDATE设计。它等价于trim prefixSET suffixOverrides, !-- 内容 -- /trimsuffixOverrides,的意思是自动删除结尾的逗号。所以update idupdateUser UPDATE user set if testname ! nullname #{name},/if if testemail ! nullemail #{email},/if if teststatus ! nullstatus #{status},/if /set WHERE id #{id} /update如果只传了name和status生成的SQL是UPDATE user SET name ?, status ? WHERE id ?trim更通用可用于WHERE条件where if testname ! nullAND name LIKE CONCAT(%, #{name}, %)/if if teststatus ! nullAND status #{status}/if /wherewhere会自动处理开头的AND并删除末尾的AND。它内部就是用trim prefixWHERE prefixOverridesAND |OR 实现的。关键提醒if标签里的test表达式用的是OGNL不是EL。! null是安全的但!name.empty会报错必须写name ! null and name ! 。6.3bind在SQL执行前“预计算”解决复杂条件bind标签允许你在SQL执行前用OGNL表达式计算一个新变量并绑定到当前上下文。典型场景模糊查询时自动加%select idsearchUsers resultTypeUser bind namepattern value% _parameter.name % / SELECT * FROM user WHERE name LIKE #{pattern} /select这里_parameter是MyBatis内置对象代表传入的参数单个参数时是参数本身多个参数时是Map。bind让你避免在Java层拼接字符串保持SQL纯净。另一个高阶用法动态表名。虽然MyBatis官方不推荐有SQL注入风险但某些分表场景必须用select idselectFromTable resultTypeUser bind nametableName valueuser_ _parameter.shardId / SELECT * FROM ${tableName} WHERE id #{id} /select注意表名必须用${}不能用#{}因为#{}会加引号变成user_1而${}是字符串替换。务必确保shardId是可信值如枚举、白名单校验否则就是SQL注入漏洞。7. Spring Boot整合的暗礁自动装配、事务、多数据源的生死线Spring Boot的mybatis-spring-boot-starter极大简化了集成但也隐藏了大量“默认约定”。一旦偏离约定就会陷入“配置了却不生效”的泥潭。7.1 自动装配的3个核心BeanSqlSessionFactory、SqlSessionTemplate、MapperScannerConfigurerStarter自动配置了三个关键BeanSqlSessionFactoryMyBatis的工厂持有Configuration是所有SqlSession的源头SqlSessionTemplate线程安全的SqlSession代理Spring中所有Mapper注入的都是它MapperScannerConfigurer扫描MapperScan指定包下的接口为每个接口生成代理Bean。它们的关系是MapperScannerConfigurer→ 创建Mapper Bean → 依赖注入SqlSessionTemplate→SqlSessionTemplate内部调用SqlSessionFactory创建SqlSession。所以当你遇到“Mapper注入失败”排查顺序是检查MapperScan(com.example.mapper)是否覆盖了你的Mapper包检查SqlSessionFactory是否创建成功看启动日志是否有Creating shared SqlSessionFactory检查DataSource是否配置正确spring.datasource.url等。7.2 事务失效的5个经典场景90%源于MyBatis与Spring的协作误解Spring事务基于AOP代理而MyBatis的SQL执行依赖SqlSession。两者协作失败事务就失效。高频场景场景原因解决方案Service方法没加Transactional最基础错误事务根本没开启加Transactional检查是否在public方法上Transactional在private方法上Spring AOP无法代理private方法改为public或用TransactionTemplate自己new SqlSessionFactory().openSession()绕过Spring管理事务不生效必须用Autowired SqlSessionTemplate同一个Service里调用另一个Service的非事务方法事务传播失效用this调用会绕过代理改用ApplicationContext.getBean()或注入自身异常被try-catch吞掉Spring默认只对RuntimeException回滚配置Transactional(rollbackFor Exception.class)最隐蔽的是第4条。比如Service public class UserService { public void updateUser(User user) { // 这里调用的是this.updateProfile()不是代理对象事务不生效 this.updateProfile(user); } Transactional public void updateProfile(User user) { // 事务代码 } }正确做法是注入自身Service public class UserService { Autowired private UserService self; // 自注入 public void updateUser(User user) { self.updateProfile(user); // 通过代理调用 } }7.3 多数据源AbstractRoutingDataSource的路由逻辑与MyBatis适配当项目需要读写分离或多租户时必须配置多数据源。Spring的AbstractRoutingDataSource是标准解法但它和MyBatis的整合有坑。核心步骤定义多个DataSourceBeanmasterDataSource,slaveDataSource创建RoutingDataSource重写determineCurrentLookupKey()根据ThreadLocal或注解决定路由最关键的一步为每个数据源创建独立的SqlSessionFactory并指定dataSource。错误做法共用一个SqlSessionFactoryBean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean factoryBean new SqlSessionFactoryBean(); factoryBean.setDataSource(routingDataSource()); // 错routingDataSource是抽象的 return factoryBean.getObject(); }正确做法为每个物理数据源配一个FactoryBean Primary public SqlSessionFactory masterSqlSessionFactory(Qualifier(masterDataSource) DataSource ds) throws Exception { SqlSessionFactoryBean factoryBean new SqlSessionFactoryBean(); factoryBean.setDataSource(ds); return factoryBean.getObject(); } Bean public SqlSessionFactory slaveSqlSessionFactory(Qualifier(slaveDataSource) DataSource ds) throws Exception { SqlSessionFactoryBean factoryBean new SqlSessionFactoryBean(); factoryBean.setDataSource(ds); return factoryBean.getObject(); }然后在Mapper接口上用MapperScan指定不同的sqlSessionFactoryRefMapperScan(basePackages com.example.mapper.master, sqlSessionFactoryRef masterSqlSessionFactory) public class MasterConfig { } MapperScan(basePackages com.example.mapper.slave, sqlSessionFactoryRef slaveSqlSessionFactory) public class SlaveConfig { }这样不同包下的Mapper就天然绑定了不同的数据源和事务管理器。8. MyBatis Plus vs 原生MyBatis不是升级是换赛道“怎么在Spring Boot中将MyBatis升级为MyBatis Plus”——这个热搜词暴露了最大误解MyBatis Plus不是MyBatis的“升级版”而是一个完全不同的ORM范式。它用“约定优于配置”换取开发速度但牺牲了SQL的绝对控制权。8.1 MyBatis Plus的3个核心承诺以及对应的3个妥协MyBatis Plus承诺原生MyBatis实现方式妥协点零XML CRUDuserMapper.selectById(1L)自动生成SQL必须写XML或注解无法定制SQL比如想用SELECT id,name FROM user而不是SELECT *条件构造器QueryWrapperUser w new QueryWrapper(); w.eq(status, 1).like(name, a)手写动态SQL或用Example类条件复杂时Wrapper代码比XML还长且编译期无类型检查eq(statu, 1)拼错字段名不报错分页插件PageUser page new Page(1, 10); userMapper.selectPage(page, null)集成PageHelper或手写LIMIT #{offset}, #{size}分页SQL由MP自动生成无法干预比如想用游标分页所以选择MP不是“更先进”而是团队是否愿意为开发速度放弃对SQL的精细控制。8.2 什么时候必须退回原生MyBatis我们团队的红线清单需要复杂关联查询MP的TableField(exist false)无法优雅处理N张表JOINXML的resultMap是唯一解对SQL性能极致要求MP生成的SQL可能有冗余字段、多余JOINDBA要求每条SQL必须人工审核遗留系统改造已有大量XML强行切MP成本远高于收益需要自定义TypeHandler或PluginMP的插件体系InnerInterceptor和原生MyBatis不兼容很多老插件无法复用。真实体验我们一个报表系统原用MP的LambdaQueryWrapper当条件超过10个时Java代码长达50行且无法复用。改成XML后用sql片段提取公共条件代码量减半DBA还能直接优化SQL。8.3 MyBatis Flex中间路线的探索者MyBatis Flex是新兴框架试图在MP和原生之间找平衡。它保留了MP的便捷APIQueryWrapper但允许在Wrapper中嵌入原生SQL片段wrapper.and(status 1 AND create_time #{createTime})支持Kotlin DSL类型安全更强插件体系兼容原生MyBatis。但它尚未经过大规模生产验证。我们的建议是新项目可评估Flex老项目升级优先考虑原生MyBatis的渐进式优化如引入MyBatis Dynamic SQL。9. 面试高频题的底层拆解从“标准答案”到“现场推演”最后回归标题里的“八股文”。真正的面试官不关心你能否复述定义而是想看你如何用MyBatis的原理解决一个从未见过的问题。我们拆解3个高频题展示“推演式回答”9.1 “#{} 和 ${} 的区别”——别背答案画执行链路图错误回答“#{}是预编译${}是字符串替换”。正确推演#{}MyBatis解析时生成?占位符交给JDBCPreparedStatement设置参数由数据库驱动完成类型转换和防注入${}MyBatis解析时直接字符串替换生成最终SQL完全不经过PreparedStatement所以${table_name}可以动态表名但必须确保table_name是白名单值而#{id}永远安全