做Java开发这几年我时不时就会被MyBatis的“核心属性”这四个字绊一脚。比如一个setting标签想不起来该放mybatis-config.xml还是application.ymlresultType映射不上结果是没开mapUnderscoreToCamelCase项目经理让排查一条Update执行慢的问题我先把SQL日志打开都不知道在哪配。这些问题说到底是没有把MyBatis核心属性的体系串起来。这篇笔记我打算用由浅入深的方式从全局配置文件讲到Mapper映射再深入到缓存、拦截器、分页插件和源码加载顺序把日常开发高频使用、面试又常问的核心属性完整盘一遍。适合正在用MyBatis但没系统性梳理过配置的同学也适合准备MyBatis面试题、想进一步理解配置背后原理的朋友。1. MyBatis核心属性从哪里入手先把全局配置文件脉络理清1.1 environment与dataSource数据库连接属性是怎么被消费的MyBatis的全局配置文件有自己固定的骨架DTD规定了标签顺序顺序错了直接启动报错。基本结构是这样?xml version1.0 encodingUTF-8 ? !DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//EN http://mybatis.org/dtd/mybatis-3-config.dtd configuration properties resourcedb.properties/ environments defaultdevelopment environment iddevelopment transactionManager typeJDBC/ dataSource typePOOLED property namedriver value${driver}/ property nameurl value${url}/ property nameusername value${username}/ property namepassword value${password}/ /dataSource /environment /environments mappers mapper resourcemapper/UserMapper.xml/ /mappers /configurationenvironments代表环境集合通过default指定当前激活哪一套。为什么要设计多套环境因为本地、测试、生产数据库往往不一样同一份配置可以包含多套environment部署时只需要修改default指向。这在实际项目里其实没那么常用因为Spring Boot整合后连接池都交给外部管理但理解它能帮你搞清楚MyBatis属性加载的最初逻辑。dataSource的type三选一UNPOOLED是每次请求都新建连接基本只适合DemoPOOLED是MyBatis自带的连接池内部用PooledConnection实现复用JNDI是交给Tomcat等容器管理。日常开发中用POOLED居多它能解决频繁创建连接的开销问题但别指望它和HikariCP性能一样好如果项目已经用Spring Boot推荐把数据源交给HikariCPMyBatis只负责SQL执行层。1.2 settings里值得优先掌握的六个开关属性settings是MyBatis核心属性里信息量最大的一个区域几乎每个开关都对应一个开发场景。我按踩坑频率排序给你推荐几个必须关注的mapUnderscoreToCamelCase默认false数据表字段是user_nameJava属性是userName不开这个开关时resultType自动映射会匹配不上字段全是null。开了之后自动映射会做下划线转驼峰这是最值得优先打开的属性。cacheEnabled二级缓存总开关默认true。注意即使全局开了某个Mapper没配cache/二级缓存依然不生效。lazyLoadingEnabled延迟加载总开关默认false。开启后关联对象在被调用时才查询配合aggressiveLazyLoading使用。这个属性会改变SQL触发时机新手很容易被“明明只查了主表为什么后来多了条关联SQL”搞懵。jdbcTypeForNull默认OTHER。向Oracle插入null值时容易报“无效的列类型”因为Oracle不认OTHER设成NULL能解决一大批插入空值异常。logImpl指定日志实现可选SLF4J、LOG4J2、STDOUT_LOGGING等。设成SLF4J后统一走项目日志框架排查SQL问题方便很多。defaultExecutorType默认SIMPLE。如果一段业务逻辑里要连续执行大量update/insert可以改成BATCH能显著减少网络往返和SQL预编译开销。要注意BATCH模式下selectKey和自增主键的获取行为会不同。我见过一个典型坑项目没有配mapUnderscoreToCamelCase然后所有实体字段又恰好和数据库列名一致开发环境没问题上线换了个表结构后一堆字段映射不上。这个开关的成本几乎为零但收益很大。1.3 properties、typeAliases与typeHandlers全局层面容易被忽略的属性properties的作用是定义变量后续所有配置里都可以用${占位符}引用。比如数据库连接信息可以抽到db.properties也可以通过property子标签写在XML里properties resourcedb.properties property nameusername valueroot/ /properties${username}最终取哪个值涉及加载优先级这个我放到后面源码部分详细讲因为真的有人在这里被绕晕。typeAliases用来给Java类型起短名。不给别名的时候resultType要写全限定名com.example.entity.UserDO配了别名后写UserDO就行了。推荐用包扫描注解的方式typeAliases package namecom.example.entity/ /typeAliases实体上标Alias(UserDO)即可。这里有个隐患如果不用Alias注解MyBatis会默认把类名首字母小写作为别名不同包下同名类会导致别名冲突报错时不好排查。typeHandlers用来处理Java类型和数据库类型之间的转换。比如数据库存的是JSON字符串Java对象希望直接映射成一个Map或List靠默认TypeHandler是做不到的需要自定义。实现BaseTypeHandlerT重写setNonNullParameter、getNullableResult三个方法然后注册typeHandlers typeHandler handlercom.example.handler.JsonTypeHandler javaTypejava.util.Map/ /typeHandlers这个知识点在面试里出现频率也不低问“MyBatis如何自定义类型转换器”时答出TypeHandler和注册方式基本就过关了。2. Mapper映射文件核心属性拆解每天都要写的东西更要吃透2.1 namespace、statementId与statementType这三个基础属性Mapper XML是MyBatis真正干活的地方。一个Mapper映射文件的根节点mapper最重要的属性是namespace。如果用Mapper接口开发namespace必须等于接口的全限定名这点没得商量。namespace加select等标签的id组成了statementId。SqlSession执行SQL时本质上就是根据statementId找到对应的MappedStatement。所以Invalid bound statement (not found)这个报错一出现优先检查两件事namespace是否和接口全限定名一致id是否和接口方法名一致。statementType控制Statement类型可选PREPARED默认、STATEMENT、CALLABLE。STATEMENT不做预编译直接拼SQL有SQL注入风险基本不推荐。CALLABLE用于调用存储过程配合{call procedure_name(?,?)}写法。日常开发保持默认PREPARED即可面试时知道三者的区别就够用。parameterType和resultType是另外两个高频属性。parameterType在MyBatis 3.x里其实不是必须的框架可以通过ParameterHandler自动推断参数类型但写上能让代码更明确。resultType用于自动映射resultMap用于复杂映射下面单独展开。2.2 resultType与resultMap的选择逻辑和字段映射细节resultType是最简单的映射方式MyBatis会把查询结果的列名按规则映射到实体属性。配合mapUnderscoreToCamelCase使用看起来非常省事select idgetById resultTypecom.example.entity.UserDO select id, user_name, create_time from user where id #{id} /select但resultType解决不了三类问题字段名和列名差异太大时不好看关联查询需要嵌套对象一对一、一对多关系需要association和collection。这时候就得用resultMapresultMap iduserDetailMap typecom.example.entity.UserDO id propertyid columnid/ result propertyuserName columnuser_name/ association propertydept javaTypecom.example.entity.DeptDO id propertyid columndept_id/ result propertyname columndept_name/ /association collection propertyroles ofTypecom.example.entity.RoleDO id propertyid columnrole_id/ result propertyname columnrole_name/ /collection /resultMap这里最容易踩的坑是id和result用错。id在MyBatis缓存判断里承担核心作用两个对象的id相同会被判定为同一行数据用于collection去重。如果误把业务字段标成id会导致结果集行数变少。碰到“查询结果只有几条但SQL直接跑却有几十条”的诡异问题时先检查resultMap的id定义。autoMappingBehavior这个全局属性也会影响结果映射。默认是PARTIAL会忽略没有在resultMap里定义的列自动映射打标。如果设成NONE连自动映射都不做了每列都得显式写result这个一般用不到但面试题里偶尔会拿它考映射机制。2.3 #{}与${}参数占位符核心参数属性里的安全分水岭这是MyBatis面试题里出现频率最高的考点之一也是实践中最需要警惕的属性写法。#{}是预编译占位符MyBatis会把它解析成?再通过PreparedStatement设置参数。它能有效避免SQL注入是绝大多数场景下的首选。${}是字符串直接替换MyBatis会把变量值原样拼接到SQL中不会预编译。我见过有人在where user_id ${id}这么写一旦id被外部传入攻击者拼上1 or 11就能把整张表拖出来这是绝对的大忌。${}真正合理的使用场景是表名、ORDER BY字段这类数据库结构元素因为预编译占位符不允许替换标识符。即使要用也建议加白名单校验String tableName Whitelist.check(tblName); // 只允许 list、order、log 等固定值除了#{}和${}参数映射里还有jdbcType这个子属性。比如插入一条记录某个字段值为null经常报Oracle类型错误。解决办法是在参数占位符里显式声明类型insert idinsertUser insert into user(id, user_name, nickname) values(#{id}, #{userName}, #{nickname, jdbcTypeVARCHAR}) /insert虽然全局配置jdbcTypeForNullNULL也能兜底大部分场景但显式声明能让SQL意图更清楚排查问题时一眼就能看出哪列允许空值、哪列不允许。2.4 动态SQL常用标签的配合实战动态SQL是MyBatis最实用的能力之一核心属性集中在test表达式里。if标签的test写的是OGNL表达式判断条件是否成立where会自动去掉开头的AND或ORset会自动去掉结尾的逗号foreach用于IN查询和批量插入choose/when/otherwise对应Java的switch。我贴一个常用的组合select idlistByCondition resultTypeUserDO select * from user where if testuserName ! null and userName ! and user_name #{userName} /if if teststatus ! null and status #{status} /if /where /select这里有个容易踩的坑test里判断字符串是否为空用单引号判断数字直接写! null。在Java代码里习惯写双引号换成OGNL表达式后很多人会栽在这个细节上报错还不好看懂。foreach批量插入也是一个高频场景insert idbatchInsert insert into user(user_name, status) values foreach collectionlist itemitem separator, (#{item.userName}, #{item.status}) /foreach /insertcollection属性如果参数是List填list如果是数组填array如果是Param(list) ListUser填注解指定的名字。这个对应关系不记住运行时很容易报“There is no getter for property named xxx”。动态SQL会掩盖很多执行细节所以在测试环境一定要开启SQL日志看清楚动态SQL最终拼出来的结果是不是符合预期。这能帮你找到很多“明明if条件应该成立却少了一段SQL”的问题。3. 缓存属性深入一级缓存、二级缓存的机制、配置与失效场景3.1 一级缓存会话级缓存为什么在Spring中经常“失灵”MyBatis一级缓存默认开启作用域是SqlSession。同一个SqlSession中两次完全相同的查询第二次会直接命中缓存不再访问数据库。我把一级缓存理解为“会话级私聊窗口”窗口关了缓存就没了窗口里做了增删改缓存也会被清空。一级缓存的命中条件也比较严格具体来说必须是同一个SqlSession实例statementId必须一样也就是同一个Mapper方法查询参数必须完全相同包括参数类型、值、顺序两次查询之间没有执行过增删改操作也没有调用过SqlSession.clearCache()。既然一级缓存这么好用为什么在Spring项目里经常发现每次查询都走数据库因为Spring整合MyBatis时使用的是SqlSessionTemplate它默认会代理每次Mapper调用每次访问都获取一个全新的SqlSession除非包在事务里。也就是说脱离事务的普通查询天然享受不到一级缓存带来的效果。这不算Bug是Spring事务环境下SqlSession生命周期决定的。一级缓存底层是PerpetualCache内部就是一个HashMap。因为它的作用域小、生命周期短实际项目里我基本不拿它做性能优化主要靠它避免在一次事务内反复执行重复SQL比如同一个事务里先查用户、再查用户、又查用户这种无意义的重复请求。3.2 二级缓存mapper级缓存的所有配置属性逐个说清楚二级缓存作用域是namespace也就是一个Mapper对应一个缓存实例。开启二级缓存需要同时满足条件全局cacheEnabled为true默认就是true、在Mapper XML里配置cache/、查询结果对象实现Serializable接口默认情况下需要因为二级缓存可能做序列化存储。cache/标签的几个核心属性经常被问到cache evictionLRU flushInterval60000 size512 readOnlytrue blockingfalse/eviction缓存淘汰策略。LRU按最少使用淘汰FIFO先进先出SOFT软引用WEAK弱引用。默认LRU。flushInterval缓存刷新间隔单位毫秒默认不刷新直到发生增删改才清空。size缓存最多存放的对象个数默认1024。设置太大会内存溢出太小命中率低。readOnlytrue时缓存返回同一对象引用性能好但调用方修改对象会影响缓存false时返回对象的拷贝安全性好但需要序列化。blocking缓存未命中时是否用锁控制并发能防止缓存击穿默认false。把它类比成“namespace级别的公共白板”多个SqlSession之间可以共享同一个Mapper的缓存。但公共白板的维护成本就比私聊窗口高多了一旦并发更新脏数据问题随之而来。3.3 多表查询与二级缓存结合时的经典坑位二级缓存最容易出问题的场景是多表关联查询。举个例子用户表和部门表分别有UserMapper和DeptMapperUserMapper里的查询join了dept表结果被UserMapper的二级缓存缓存。此时部门名称被改了DeptMapper执行了update它只会清空自己namespace的缓存而UserMapper缓存里还保留着旧部门名。于是用户刷新页面看到的部门名还是旧的。解决手段有三种。一是共享缓存用cache-ref namespaceDeptMapper/让UserMapper复用DeptMapper的缓存这样任何一方更新都会清空同一份缓存但这样缓存粒度变大命中率下降。二是避免跨表查询结果走二级缓存把这些SQL的useCache设为false。三是设计上就是不让业务查询依赖二级缓存缓存统一交给Redis。我的个人建议是互联网项目里二级缓存能不用就不用。数据库一旦成为瓶颈真正能扛量的是Redis、本地Caffeine缓存这类可控的缓存方案而不是藏在ORM层里的namespace缓存。把cacheEnabled全局关掉也是一种选择至少不会出现莫名其妙的脏读。4. 拦截器、分页插件与日志打印MyBatis核心属性之上的扩展实战4.1 自定义拦截器的四个切入点与注册方式MyBatis拦截器机制允许你在四大核心对象上做切面Executor负责SQL执行能拦截update、query、flushStatements、commit、rollback等方法StatementHandler负责JDBC Statement处理能拦截prepare、batch、update、query等ParameterHandler负责参数处理主要拦截setParametersResultSetHandler负责结果集处理主要拦截handleResultSets。自定义拦截器需要两步用Intercepts注解声明拦截的目标对象和方法实现Interceptor接口在intercept方法里写增强逻辑。Intercepts({ Signature( type Executor.class, method update, args {MappedStatement.class, Object.class} ) }) public class AutoFillInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms (MappedStatement) invocation.getArgs()[0]; Object parameter invocation.getArgs()[1]; // 在这里给创建时间、更新时间赋值 return invocation.proceed(); } }Signature里的method和args必须和真实方法签名完全一致否则运行时不生效。这是拦截器“好像配了但没反应”最常见的坑。Spring Boot里注册拦截器时可以通过Bean方式加入插件链Bean public Interceptor myAutoFillInterceptor() { return new AutoFillInterceptor(); }MyBatis启动时会自动装配所有Interceptor类型的Bean到插件链中。4.2 PageHelper分页插件核心属性与使用姿势分页插件是拦截Executor的经典案例。PageHelper拦截了Executor.query方法自动改写SQL生成count查询和带LIMIT的分页查询省去手写方言的麻烦。基本用法很固定有一段“铁律”必须记牢PageHelper.startPage之后必须紧跟第一条Select查询中间不能穿插其他查询或业务逻辑。PageHelper.startPage(pageNum, pageSize); ListUser list userMapper.selectList(condition); PageInfoUser pageInfo new PageInfo(list);PageHelper的属性通常配置在分页插件上PageInterceptor pageInterceptor new PageInterceptor(); Properties properties new Properties(); properties.setProperty(helperDialect, mysql); properties.setProperty(reasonable, true); properties.setProperty(supportMethodsArguments, true); pageInterceptor.setProperties(properties);helperDialect数据库方言。配了之后分页SQL会按对应方言生成不配时插件自动识别。reasonabletrue时页码越界自动修正比如查询第100页但总共只有5页会强制修正到最后一页false时返回空数据。这个属性要看业务需求有的前端就是拿越界页码来检测“还有没有下一页”的。supportMethodsArguments把分页参数从Mapper方法的参数里自动提取不用显式调用startPage。建议谨慎开启隐式行为会让维护代码的人看不懂。pageSizeZeropageSize为0时查询全部数据默认false。一个容易出错的地方是PageHelper的线程安全问题。分页参数是绑定在线程上下文里的异步多线程场景下子线程拿不到分页信息父线程又可能在一次查询后残留上下文影响下一次不分页的查询。所以在startPage之后如果查询抛异常或者走了其他分支分页上下文不会自动清理下一跳SQL可能被错误分页。解决办法之一是直接用PageHelper.clearPage()手动清理。4.3 SQL日志打印配置与Update执行慢的排查链路排查MyBatis问题第一件事就是把SQL日志打开。Spring Boot项目里最常用的几种方式在application.yml里配置MyBatis的日志实现mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl或者在日志框架里指定Mapper接口的包名logging: level: com.example.mapper: debug打开之后每次SQL执行会打印Preparing、Parameters、Total三行信息。Preparing是预编译后的SQL模板Parameters是实际传入的参数值Total是本次查询耗时。排查Update执行慢这种问题时第一件事就是看这三行把“SQL本身慢”和“MyBatis框架开销慢”区分开。如果Total很大去数据库里跑一遍同一条SQL看是命中索引问题还是数据库负载问题。如果单条SQL在数据库客户端执行很快但MyBatis执行很慢重点看是不是连接获取阻塞、驱动参数配置、缓存失效导致的重复查询。如果是大批量update还要考虑MyBatis默认SIMPLE执行器会一条一条推进事务网络往返次数多整体时间长。这时可以全局把defaultExecutorType设为BATCH或者在配置application.yml时指定executor-type: batch或者把多条update合并成一条CASE WHEN批量更新SQL。不过BATCH模式也有代价它会把SQL攒到flushStatements时才真正提交此时selectKey拿自增主键、update返回的影响行数都会和预期不同优化前要在测试环境把行为验证清楚。5. 从源码看核心属性的加载顺序与配置优先级5.1 Configuration对象所有核心属性的最终仓库MyBatis启动时SqlSessionFactoryBuilder会读取配置文件最终把全局配置、Mapper映射、别名、TypeHandler、插件等全部封装进一个Configuration对象。这个对象是MyBatis整个运行期的核心SqlSessionFactory内部持有的就是它。XMLConfigBuilder按DTD顺序逐个解析配置文件里的标签常见的关键顺序是properties→settings→typeAliases→typeHandlers→objectFactory→objectWrapperFactory→reflectorFactory→plugins→environments→databaseIdProvider→mappers这个顺序不是随便定的。比如settings里配置了mapUnderscoreToCamelCase后续在解析resultMap时就要参考它来决定映射策略typeAliases先注册好后面parameterType、resultType里写别名才能解析成功。所以DTD不允许乱序其实是在帮我们降低配置之间的耦合风险。Configuration里的核心属性比如mapperRegistry存储Mapper代理工厂、keyGenerator、cacheEnabled、localCacheScope都对应配置文件里的某块内容。如果面试官问“MyBatis启动时做了什么”答出SqlSessionFactoryBuilder→XMLConfigBuilder→Configuration这条链路就是标准答案。5.2 properties属性加载与占位符替换的先后顺序properties的处理逻辑在源码里也遵循解析顺序先加载resource或url指定的外部文件再把XML里property子标签逐个放入属性集合。因为子标签在后面加载所以同名时property子标签会覆盖外部文件里的值。有了属性集合之后后续解析配置文件时一旦遇到${...}占位符就会从这个集合里取值替换。比如dataSource typePOOLED property namedriver value${driver}/ property nameurl value${url}/ /dataSource这里的${driver}是在propertiesElement已经解析完成后才被替换的。Spring Boot整合时还可以通过mybatis.configuration-properties传入额外的属性集合并且在读取外部配置时保持和Spring环境变量的优先级一致。源码层面有一个细节变量替换用的正是Spring Boot在构建SqlSessionFactory时传入的Properties所以application.yml里的配置也会参与占位符解析。从实践中得到的结论是项目里不要到处用${}全局属性一旦被外部优先级覆盖排查起来非常花时间。能用Spring的ConfigurationProperties管理数据源就不要把连接信息塞在MyBatis XML里。5.3 围绕核心属性最常见的几个面试考点复盘面试问到MyBatis很多问题本质都在考核心属性。整理几个高频考点供复习参考resultType和resultMap的区别一个自动映射、一个显式映射复杂关联关系必须用resultMap缓存去重还依赖id的定义。#{}和${}的区别预编译vs拼接${}存在SQL注入风险表名、排序字段只能用${}时要做白名单。一级缓存和二级缓存的区别作用域不同一级缓存是SqlSession级二级缓存是namespace级二级缓存需要显式开启、对象要实现序列化、多表操作要小心脏读。Mapper接口没有实现类为什么能调用MyBatis通过MapperProxy动态代理生成接口实例SqlSession.getMapper拿到代理对象方法调用会解析为statementId并执行。如何获取自增主键useGeneratedKeystrue配合keyPropertyidOracle则用selectKey或序列。MyBatis插件的原理基于JDK动态代理可以对Executor、StatementHandler、ParameterHandler、ResultSetHandler做拦截分页插件就是拦截Executor.query的典型案例。这些考点如果都能结合配置细节讲出来面试官一般会认为你不仅会用MyBatis而且理解它的运行机制。6. 常见问题与排查技巧实录6.1 高频问题速查表我在实际工作和带人过程中整理过一份MyBatis高频问题速查表先用表格快速定位问题现象大概率原因排查方向查询结果字段全是null没开mapUnderscoreToCamelCase或resultMap列名对不上检查自动映射开关、检查SQL别名Invalid bound statementnamespace或id和Mapper接口不一致核对namespace和接口全限定名、id和方法名XML里写报错XML特殊字符需要转义用lt;或![CDATA[]]包住插入null报无效列类型Oracle不认jdbcTypeForNullOTHER设置全局jdbcTypeForNullNULL或参数显式指定jdbcType分页不生效返回全量数据startPage后没紧接查询或插件没注册成功检查调用顺序、检查插件注册、检查版本兼容打印不出SQL日志logImpl没配好或日志级别太高设置log-impl查看Mapper包logging.level开启二级缓存后数据不更新多表操作导致namespace缓存没有联动刷新用cache-ref、设置useCachefalse或者关掉二级缓存Update执行慢SQL本身慢、逐条提交、驱动参数不优打开日志观察Total必要时批量执行Oracle日期/时间映射异常驱动版本、时区、jdbcType映射问题用TO_CHAR转换或调整字段映射6.2 三个值得展开的实战排查案例**案例一Oracle查询时间映射问题。**有次写Oracle查询数据库字段是TIMESTAMP类型查出来映射到Java的Date时间总是比数据库时间少了8小时。排查后确认是JDBC驱动和JVM时区没有对齐。处理方式有两种在SQL里用TO_CHAR(create_time, YYYY-MM-DD HH24:MI:SS)转成字符串避免日期类型在驱动层发生时区转换或者在Java实体里使用LocalDateTime并在JDBC连接串里配置正确的时区参数。最省事的办法其实是第一种让数据库把格式问题解决掉代码层只做展示。**案例二Update执行慢最后发现是批量更新在“一把梭”。**业务人员反馈某个接口超时打开日志看到一条Update语句Total耗时3000ms。但在数据库客户端手动执行同样SQL只要50ms。差别在于MyBatis里这条update每次都是独立预编译、独立提交网络往返开销和事务提交开销全部叠加。改成批量SQL之后一次更新多行数据接口耗时从3秒降到了200ms。这个案例说明看到Update执行慢不要只盯着SQL本身还要看执行器类型、批量策略和事务边界。**案例三PageHelper分页插件失效。**有一次在Spring Boot项目里配了PageHelper查询时返回了全量数据分页完全没生效。检查发现PageInterceptor没有注册到MyBatis的插件链只引入了Maven依赖没有创建Bean。后来用Bean把PageInterceptor声明出来版本也和MyBatis对应上分页才恢复正常。还有一次是startPage之后中间先查了一次别的表导致分页作用到了错误的查询上。这类问题代码上不报错逻辑上却很隐蔽排查时要靠日志确认分页SQL有没有被改写。6.3 开发工具侧的两个小技巧XML高亮和日志插件MyBatis XML在IDEA里默认就是一个普通的XML文件写动态SQL时标签闭合、test表达式错误都不提示体验很差。我一般会让团队统一安装MyBatisX插件它有几个明显好处XML里能高亮select、if等标签Mapper接口和XML可以互相跳转还能提示resultMap的字段映射。对排查Invalid bound statement之类的问题很有帮助。另一个实用工具是MyBatis Log Plugin它可以把MyBatis输出的Preparing和Parameters两行日志拼成一条可直接执行的完整SQL复制到数据库客户端就能跑。排查“SQL为什么查不到数据”“参数是不是传错了”这类问题时这个插件能节省大量时间。最后分享一个我在实际项目里的习惯新项目用MyBatis Plus或Spring Data JDBC时能力边界要提前说清楚老项目手写MyBatis时全局settings固定开mapUnderscoreToCamelCaselogImpl设成SLF4J分页用PageHelper就把startPage紧贴查询语句能不用二级缓存就不用。这些规则看起来很基础但每条背后都对应过一次线上问题。配置属性这种东西光背文档没用踩过坑才知道哪条要小心。希望这篇由浅入深的笔记能帮你把MyBatis核心属性真正串成一张网。