尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

深入MyBatis核心源码:八大模块解析与实战应用

深入MyBatis核心源码:八大模块解析与实战应用 1. 从“会用”到“懂它”为什么我们要啃MyBatis源码这块硬骨头用了这么多年MyBatis增删改查写得飞起动态SQL也玩得挺溜但每次面试官问起“MyBatis的一级缓存和二级缓存有什么区别”、“#{}和${}的底层处理机制”、“插件是怎么拦截并修改SQL的”心里是不是还是会咯噔一下或者当线上出现一个诡异的SQL执行问题日志打了参数看了配置查了还是找不到头绪最后只能靠重启大法或者玄学调试这些场景恰恰暴露了我们停留在“会用”层面的局限性。MyBatis作为Java持久层的事实标准之一其设计之精妙远不止于一个简单的ORM框架。深入其核心源码不是为了炫技而是为了在关键时刻能像外科医生一样精准定位问题能像架构师一样理解其设计取舍从而写出更健壮、更高性能的代码。今天我们不谈那些边边角角就聚焦于构成MyBatis骨架与灵魂的八大核心源码模块带你从“黑盒使用者”转变为“白盒掌控者”。2. 基石构建Configuration与SqlSessionFactory的初始化迷宫很多人以为配置MyBatis就是写个mybatis-config.xml和一堆mapper.xml然后程序就能跑了。但你知道从那一堆XML文件到内存中一个可用的、线程安全的SqlSessionFactory中间经历了多少道“工序”吗这个过程就封装在Configuration类和SqlSessionFactoryBuilder的构建过程中。2.1 ConfigurationMyBatis的“中央大脑”你可以把Configuration对象想象成MyBatis运行时的“中央配置库”和“元数据中心”。它不是一个简单的Map容器而是一个高度结构化、承载了框架所有核心信息的对象。核心属性解析environments: 存储数据源和事务管理器配置。这里有个关键点虽然配置中可以定义多个环境如dev、test但一个SqlSessionFactory只能对应一个Environment。这决定了你的应用运行时连接的是哪个数据库。mappedStatements: 这是重中之重。它是一个Map键是namespace.id如com.xxx.UserMapper.selectById值是一个MappedStatement对象。每一个在mapper.xml中定义的select、insert等标签都会被解析并封装成一个MappedStatement。这个对象包含了这条SQL语句的所有信息SQL源码、参数映射、结果映射、缓存配置、语句类型等。caches: 二级缓存的空间。它以namespace为键存储着各个Mapper命名空间对应的Cache对象。resultMaps和parameterMaps: 存储着复杂的结果集映射和参数映射定义。parameterMaps现在基本被parameterType和Param注解替代但架构上仍保留。interceptorChain: 插件拦截器链。所有配置的插件Interceptor都会在这里被组装成一个链这是插件机制能够工作的基础。初始化流程的“暗坑”初始化并不是简单地读取XML。XMLConfigBuilder和XMLMapperBuilder这两个解析器承担了繁重的工作。它们使用了XPath解析XML但更关键的是它们调用了一系列的*Handler如XMLStatementBuilder来构建最终的MappedStatement。注意解析mapper.xml时MyBatis会检查是否允许出现重复的id。默认情况下不同XML文件中的id可以相同只要namespace不同。但如果你在同一个namespace下比如通过mapper resource和mapper class两种方式引入了同一个Mapper接口出现了重复的id框架在初始化时就会抛出异常。这个检查就发生在XMLMapperBuilder的解析阶段。2.2 SqlSessionFactoryBuilder工厂的缔造者这个类通常只用一个方法build(InputStream inputStream)。它的工作看似简单但内部流程严谨创建XMLConfigBuilder解析主配置文件。调用XMLConfigBuilder.parse()返回一个Configuration对象。此时所有配置已加载完毕。用这个Configuration对象实例化一个DefaultSqlSessionFactory。为什么是DefaultSqlSessionFactoryMyBatis提供了SqlSessionFactory接口而DefaultSqlSessionFactory是其默认且唯一的实现。它持有Configuration实例所有创建SqlSession的请求都由此发出。这里的设计体现了工厂模式将复杂的SqlSession创建过程封装起来对外提供统一接口。一个实战中的初始化问题有时你会遇到“Invalid bound statement (not found)”这个经典错误。90%的情况是mapper.xml没有被正确加载到Configuration.mappedStatements中。排查时除了检查文件路径更应该去思考初始化流程你的mapper.xml文件是否在mybatis-config.xml的mappers标签中正确声明如果是Spring BootMapperScan的路径是否覆盖了你的Mapper接口理解Configuration的构建过程能让你快速定位到问题出在“资源加载”这一环而不是盲目地去改SQL。3. 会话管理SqlSession与Executor的协作共舞拿到了SqlSessionFactory下一步就是获取SqlSession来执行操作。SqlSession是MyBatis的核心接口你可以把它看作一次数据库会话的顶层抽象。但真正干活的是它背后的“执行引擎”——Executor。3.1 SqlSession门面Facade模式的典范DefaultSqlSession是默认实现。它本身不直接执行SQL而是一个“调度员”或“门面”。它持有Configuration和Executor的引用。当你调用sqlSession.selectOne(“statementId”, param)时它主要做三件事根据statementId从Configuration里获取对应的MappedStatement。将参数传递给Executor去执行。返回Executor执行的结果。这种设计的好处是职责清晰。SqlSession负责管理会话生命周期如提交、回滚、关闭和提供用户友好的API而将具体的执行逻辑委托给Executor。3.2 Executor执行策略的指挥家Executor才是执行层的核心。MyBatis采用了策略模式提供了几种不同的执行器SimpleExecutor: 最简单的执行器。每次执行都会创建一个新的Statement对象用完后立即关闭除非是ReuseExecutor或批处理场景下的特殊处理。它不重用Statement。ReuseExecutor: 重用执行器。它会在一个SqlSession生命周期内缓存PreparedStatement对象以SQL语句为key。当执行相同的SQL时会尝试重用缓存的PreparedStatement。这能减少数据库驱动创建PreparedStatement的开销。BatchExecutor: 批处理执行器。它将所有更新操作insert, update, delete缓存起来等到调用flushStatements()或提交事务时一次性发送给数据库执行可以大幅提升批量操作的性能。如何选择执行器在创建SqlSession时可以通过参数指定。在Spring集成中通常使用默认的SimpleExecutor。对于需要批量操作的场景可以在代码中手动获取BatchExecutor。理解它们的区别有助于你在特定场景下做出性能优化。更重要的是Executor是插件Interceptor拦截的主要目标之一。插件可以拦截Executor的query和update方法从而在SQL执行前后插入自定义逻辑如分页、数据权限过滤。Executor的接口设计清晰的query,update,commit,rollback等方法为插件扩展提供了完美的切入点。3.3 二级缓存与CachingExecutor装饰器模式的应用如果你在配置中开启了二级缓存cache/MyBatis并不会直接使用上述的基础执行器而是会使用CachingExecutor。CachingExecutor是一个装饰器Decorator。它内部持有一个Executordelegate可能是SimpleExecutor等。它的工作流程是当执行查询时先根据CacheKey由MappedStatement Id、参数、分页信息等计算得出去二级缓存中查找。如果命中直接返回缓存结果。如果未命中则调用delegate.query()方法去数据库查询然后将结果存入二级缓存再返回。CachingExecutor的存在使得缓存逻辑与基础执行逻辑解耦。你可以轻松地开启或关闭缓存而不影响核心的执行流程。这也是MyBatis设计中“开闭原则”的体现。4. 语句执行StatementHandler与ParameterHandler的精密配合当Executor决定要执行一条SQL时它会将任务进一步下发给StatementHandler。这是真正与JDBCStatement打交道的层面。4.1 StatementHandlerJDBC操作的封装者StatementHandler负责创建Statement对象、参数化、执行SQL、处理结果集。它也有不同的实现对应不同的Statement类型PreparedStatementHandler: 处理PreparedStatement这是最常用的支持预编译和参数替换。SimpleStatementHandler: 处理普通的Statement。CallableStatementHandler: 处理CallableStatement用于调用存储过程。Executor的doQuery()方法中关键几步是获取Configuration。根据MappedStatement创建对应的StatementHandler。调用StatementHandler.prepare()创建Statement。调用StatementHandler.parameterize()设置参数。调用StatementHandler.query()执行并返回结果。4.2 ParameterHandler参数映射的魔术师StatementHandler.parameterize()方法内部其实是调用了ParameterHandler.setParameters()。ParameterHandler默认实现是DefaultParameterHandler的任务就是根据你在Mapper方法中传入的参数以及MappedStatement中定义的参数映射关系将Java对象中的属性值正确地设置到JDBCPreparedStatement的占位符?上。这里就涉及到#{}和${}的本质区别#{}: MyBatis会将其解析为一个JDBC的预编译占位符?。ParameterHandler会使用PreparedStatement.setXXX()方法来安全地设置参数值能有效防止SQL注入。${}: MyBatis会将其视为字符串直接替换String Substitution。在SqlSource构建SQL时${}内的内容会被直接替换成对应的参数值字符串。这里没有使用PreparedStatement的参数化设置因此存在SQL注入风险通常只用于动态指定表名、列名等非值参数。理解ParameterHandler的工作你就明白了为什么#{}是安全的而直接拼接字符串或滥用${}是危险的。5. 结果处理ResultSetHandler的映射艺术SQL执行完毕拿到ResultSet后如何将其转换成我们定义的Java对象或List、Map这就是ResultSetHandler的职责。DefaultResultSetHandler是这个过程的灵魂。它的handleResultSets()方法逻辑非常复杂但核心步骤清晰遍历ResultSet可能有多结果集存储过程返回。获取ResultMap从MappedStatement中取得描述结果映射规则的ResultMap。创建结果对象根据ResultMap的type属性通过反射或TypeHandler实例化目标对象。自动映射如果开启了autoMapping会尝试根据数据库返回的列名下划线转驼峰后去匹配对象属性名并调用TypeHandler进行类型转换和赋值。根据ResultMap映射处理result标签定义的显式映射包括复杂属性association, collection的嵌套查询或嵌套结果处理。处理嵌套查询N1问题之源如果ResultMap中定义了association或collection且其select属性指向另一个查询这里会触发额外的查询。这就是著名的“N1查询问题”的源头。优化方式通常是使用“连接查询嵌套结果映射”。TypeHandler的桥梁作用 在整个映射过程中TypeHandler类型处理器扮演了Java类型与JDBC类型之间转换的桥梁。无论是ParameterHandler设置参数还是ResultSetHandler读取结果凡是涉及到类型转换都离不开它。MyBatis内置了常用类型的处理器你也可以为自定义类型如枚举实现自己的TypeHandler。理解ResultSetHandler你就能明白为什么MyBatis的resultMap功能如此强大也能在遇到映射失败、属性为null、或者性能问题时知道该从何处着手排查。6. 脚本解析SqlSource与BoundSql的动态SQL引擎MyBatis最强大的特性之一就是动态SQL。而将那些带有if,where,foreach的XML标签最终变成一条可执行的JDBC SQL字符串就是SqlSource和BoundSql的功劳。6.1 SqlSourceSQL的源头工厂SqlSource是一个接口代表一条SQL语句的源头。根据Mapper中SQL的编写方式MyBatis会创建不同类型的SqlSourceDynamicSqlSource: 对应包含动态SQL标签OGNL表达式的SQL。它需要经过解析才能确定最终的SQL字符串。RawSqlSource: 对应静态的、不含动态标签的SQL。它在初始化时就被解析成静态SQL性能更高。ProviderSqlSource: 对应通过SelectProvider等注解提供的SQL。StaticSqlSource: 最终所有SqlSource都会被解析成StaticSqlSource它持有最终可执行的SQL字符串和参数映射信息。6.2 动态SQL的解析过程以DynamicSqlSource为例其getBoundSql方法会创建DynamicContext上下文它持有参数对象。调用SqlNode如IfSqlNode,ForEachSqlNode的apply方法。这些SqlNode构成了一个树形结构它们会根据OGNL表达式从参数对象中求值动态地决定是否将自身包含的SQL片段拼接到上下文中。最终DynamicContext中拼接出了完整的SQL字符串。使用SqlSourceBuilder对这个字符串进行第二次解析将所有的#{}占位符解析出来生成ParameterMapping列表并最终创建一个StaticSqlSource。6.3 BoundSql可执行SQL的最终形态无论是哪种SqlSource调用其getBoundSql(parameterObject)方法后都会返回一个BoundSql对象。这个对象是一次SQL执行的最终描述它包含sql: 已经完成动态解析、可以直接交给JDBC执行的SQL字符串其中#{}已被替换成?。parameterMappings:ParameterMapping列表描述了每个?对应的参数属性名、JavaType、JdbcType等信息。parameterObject: 用户传入的原始参数对象。Executor和StatementHandler拿到的就是BoundSql对象。理解这个过程你就明白了动态SQL是如何“动”起来的以及为什么我们在调试时打印的SQL和XML中写的不完全一样。7. 缓存体系一级缓存与二级缓存的深度剖析缓存是提升性能的利器但用不好就是“坑”器。MyBatis的两级缓存设计需要透彻理解。7.1 一级缓存SqlSession级别的“工作备忘录”范围默认开启且无法关闭。其作用域是一个SqlSession一次数据库会话。实现在BaseExecutor中有一个localCache属性一个PerpetualCache对象。它的Key是CacheKey由MappedStatement Id、参数、分页等计算得出。生命周期与SqlSession共存亡。当执行insert、update、delete、commit、rollback或手动调用clearCache()时该SqlSession的一级缓存会被清空。工作模式在同一个SqlSession中连续两次执行相同的查询相同的statementId和参数第二次会直接返回缓存的结果不会访问数据库。坑点提示分布式环境无效一级缓存是会话级别的在Web应用中通常一个请求对应一个SqlSessionSpring中常与事务绑定请求结束就关闭了所以无法跨请求共享。脏读风险如果两个操作共享同一个SqlSession比如在同一个事务中第一个操作查询了数据第二个操作修改了同一条数据但未提交此时第一个操作再次查询拿到的还是缓存中的旧数据。因此在涉及更新的操作后MyBatis会自动清空本SqlSession的缓存。7.2 二级缓存Mapper命名空间级别的“共享黑板”范围需要手动在mapper.xml中配置cache/标签来开启。作用域是一个namespace通常是一个Mapper接口。实现底层也是PerpetualCache但被CachingExecutor装饰。它存储在Configuration对象的caches这个Map中。生命周期与应用生命周期一致除非配置了刷新策略。当执行了来自同一个namespace的任意insert、update、delete语句后该namespace下的所有缓存会被清空。序列化要求因为缓存可能被序列化到磁盘或跨会话共享所以存入二级缓存的实体类必须实现Serializable接口。核心工作机制事务提交后生效二级缓存的数据是在SqlSession执行commit()或close()时才被真正刷入缓存池。这意味着如果你在事务中查询了数据但在事务提交前其他会话是看不到这份缓存的。跨会话共享不同的SqlSession只要操作同一个Mapper就可以共享二级缓存。严重注意事项多表关联查询的陷阱如果UserMapper中有一个查询关联了Order表结果被缓存。当OrderMapper执行了一个更新操作它只会清空OrderMapper命名空间下的缓存而UserMapper的缓存依然存在导致读到脏数据。因此涉及多表关联的查询要谨慎使用二级缓存或者使用cache-ref来建立缓存引用关系但这会带来管理复杂度。数据一致性二级缓存适用于读远多于写、且数据实时性要求不高的场景如配置数据。对于金融、交易等强一致性要求的场景通常不建议开启。8. 插件机制Interceptor与责任链的扩展魔法MyBatis的插件Plugin机制是其框架扩展性的体现允许用户在不修改框架源码的情况下介入核心组件的执行过程。其核心是责任链模式和动态代理。8.1 拦截器签名与Intercepts注解定义一个插件需要实现Interceptor接口并使用Intercepts和Signature注解来声明你要拦截哪个对象的哪个方法。Intercepts({ Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}) }) public class MyPlugin implements Interceptor { // ... }Signature指明了拦截点type目标类如Executor,StatementHandler,ParameterHandler,ResultSetHandler、method方法名、args方法参数类型列表。8.2 Plugin.wrap()动态代理的包装器在MyBatis初始化时会遍历所有配置的插件并调用Plugin.wrap(target, this)方法。这个方法会获取目标对象target的所有接口。检查当前插件的Intercepts签名判断是否需要拦截该目标对象。如果需要则使用JDK动态代理创建一个代理对象。这个代理对象的InvocationHandler就是Plugin类本身。Plugin的invoke方法会判断当前调用的方法是否在拦截范围内。如果是则调用插件的intercept方法否则直接调用原方法。这就形成了一条代理链假设配置了插件A和B。MyBatis创建Executor实例后先用A去wrap得到一个代理对象proxyA再用B去wrapproxyA得到最终的代理对象proxyB。当调用Executor.query()时请求先到达B的interceptB可以决定是否调用invocation.proceed()将请求传递给链中的下一个即AA再决定是否传递给真正的Executor对象。这就是责任链模式。8.3 实战实现一个简单的SQL执行时间监控插件理解了原理实现一个插件就很简单了。下面是一个记录慢SQL的插件示例Intercepts({ Signature(type Executor.class, method update, args {MappedStatement.class, Object.class}), Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}) }) public class SlowSqlInterceptor implements Interceptor { private long threshold 1000; // 慢查询阈值单位毫秒 Override public Object intercept(Invocation invocation) throws Throwable { long start System.currentTimeMillis(); try { // 继续执行调用链 return invocation.proceed(); } finally { long end System.currentTimeMillis(); long time end - start; if (time threshold) { // 获取MappedStatement和BoundSql以记录详细信息 MappedStatement ms (MappedStatement) invocation.getArgs()[0]; BoundSql boundSql; if (invocation.getArgs().length 5) { boundSql (BoundSql) invocation.getArgs()[5]; } else { // 对于update方法参数结构不同需要从MappedStatement获取 Object parameter invocation.getArgs()[1]; boundSql ms.getBoundSql(parameter); } System.err.println(String.format(慢SQL警告执行耗时[%dms]语句ID[%s]SQL[%s], time, ms.getId(), boundSql.getSql())); } } } Override public Object plugin(Object target) { // 使用Plugin工具类创建代理 return Plugin.wrap(target, this); } Override public void setProperties(Properties properties) { // 可以从配置中读取阈值 String thresholdStr properties.getProperty(threshold); if (thresholdStr ! null) { this.threshold Long.parseLong(thresholdStr); } } }在mybatis-config.xml中配置plugins plugin interceptorcom.example.SlowSqlInterceptor property namethreshold value500/ /plugin /plugins这个插件拦截了Executor的query和update方法在执行前后计算耗时并打印出慢SQL的详细信息。通过这个例子你可以看到插件机制的强大之处无需修改MyBatis源码就能无侵入地增强其功能。常见的分页插件如PageHelper、数据权限过滤插件都是基于此机制实现的。理解它你就能为自己的项目定制各种强大的扩展功能。
返回列表