乐于分享
好东西不私藏

MyBatis 源码解读(一):整体架构与启动初始化

MyBatis 源码解读(一):整体架构与启动初始化

写在前面

看 MyBatis 源码的人一般分两种:一种是面试被问得怀疑人生来补课的,另一种是真要在项目里搞深度定制(写分页插件、魔改缓存策略之类)。不管你是哪一种,入口都一样——你得先搞明白 MyBatis 启动的时候到底干了些什么勾当,那些 XML 配置是怎么一步步变成运行时能用的对象的。

这篇文章就把这条链路从头跟到尾。读完之后你应该能回答一个面试高频题:SqlSessionFactoryBuilder.build() 这一行代码背后,到底发生了多少事?

剧透:比你以为的多得多。

一、整体架构:三层楼,各干各的

先不急着看代码。MyBatis 的模块划分搞不清楚,后面读源码就像逛商场不带导览图——走到哪算哪,永远不知道自己在几楼。

MyBatis 的源码包结构大致分三层:

层次
核心包
职责
通俗理解
接口层
session
对外暴露 API
前台,你打交道的窗口
核心处理层
executor
scriptingmappingbuilder
SQL 解析、执行、结果映射
后厨,真正干活的地方
基础支撑层
type
iologgingreflectiontransaction
类型转换、资源加载、日志、反射、事务
水电气,离了谁都转不了

一个 SQL 从发出到返回结果,要经过的完整路径:

SqlSession → Executor → StatementHandler → ParameterHandler → JDBC                                                           ↓SqlSession ← Executor ← ResultSetHandler ← ResultSet ← JDBC

打个比方:SqlSession 是服务员接单,Executor 是后厨调度,StatementHandler 是厨师,ParameterHandler 是配菜工,ResultSetHandler 是装盘出菜的。JDBC 就是灶台——真正跟数据库"火拼"的地方。

这个流程后面几篇会逐层展开。这篇聚焦最前面的问题:这条链路上用到的所有对象,是谁创建的、什么时候创建的?

答案就藏在启动初始化流程里。

二、启动入口:SqlSessionFactoryBuilder——一次性工具人

不管是纯 MyBatis 还是 Spring 集成,最终都会走到同一个入口:

SqlSessionFactory factory = new SqlSessionFactoryBuilder()    .build(inputStream);

看 SqlSessionFactoryBuilder 的源码(org.apache.ibatis.session.SqlSessionFactoryBuilder),这个类薄得离谱——本质上就干一件事:把配置文件的输入流甩给 XMLConfigBuilder 去解析,然后用解析结果创建 DefaultSqlSessionFactory

核心方法:

public SqlSessionFactory build(InputStream inputStream,        String environment, Properties properties){try {        XMLConfigBuilder parser = new XMLConfigBuilder(            inputStream, environment, properties);return build(parser.parse());    } catch (Exception e) {throw ExceptionFactory.wrapException("Error building SqlSession.", e);    } finally {        ErrorContext.instance().reset();try { inputStream.close(); } catch (IOException e) { }    }}public SqlSessionFactory build(Configuration config){returnnew DefaultSqlSessionFactory(config);}

两行关键代码,其余都是打杂的异常处理和资源关闭:

  1. parser.parse() —— 解析 XML,构建 Configuration 对象
  2. build(config) —— 用 Configuration 创建 DefaultSqlSessionFactory

SqlSessionFactoryBuilder 没有任何状态,用完即弃。就像搬家公司——帮你把家具从旧家搬到新家,完事走人,你不会留着搬家公司的电话天天打。这也解释了为什么官方文档说它最好的使用方式就是方法局部变量——用完就被 GC 回收,不需要留引用。

三、XMLConfigBuilder:配置解析的主厨

3.1 继承体系

XMLConfigBuilder 继承自 BaseBuilder,整个 Builder 家族长这样:

BaseBuilder  ├── XMLConfigBuilder        — 解析 mybatis-config.xml(大盘总控)  ├── XMLMapperBuilder        — 解析 Mapper XML(每个 Mapper 文件一个实例)  ├── XMLStatementBuilder     — 解析 <select>/<insert> 等标签(最小粒度)  └── MapperAnnotationBuilder — 解析 Mapper 接口上的注解(注解派的入口)

BaseBuilder 持有一个 Configuration 引用和几个注册器(TypeAliasRegistryTypeHandlerRegistry),子类共享这些注册器来完成类型转换和别名查找。这就好比后厨共享一个调料架——大家都能用,但调料架只有一份。

3.2 构造方法:Configuration 的诞生

privateXMLConfigBuilder(XPathParser parser,        String environment, Properties props){super(new Configuration());  // ← Configuration 在这里出生    ErrorContext.instance().resource("SQL Mapper Configuration");this.configuration.setVariables(props);this.parsed = false;this.environment = environment;this.parser = parser;}

注意 super(new Configuration())Configuration 的无参构造方法可不是空壳,它在出生那一刻就干了不少活——注册内置的类型别名(int → Integerstring → String)和类型处理器。这些是 MyBatis 开箱即用的基础能力,就像新手机预装的系统应用,你没得选,但还真离不开。

XPathParser 在构造 XMLConfigBuilder 之前就已经创建好了,它负责把 XML 文件解析成 DOM 树,并提供 XPath 查询能力。验证 DTD 的工作由 XMLMapperEntityResolver 完成——它很聪明,会从类路径下加载本地的 DTD 文件,而不是傻乎乎地去远程拉取。毕竟谁也不想因为网络抖动导致框架启动失败。

3.3 parse() 方法:解析入口,只准进一次

public Configuration parse(){if (parsed) {thrownew BuilderException("Each XMLConfigBuilder can only be used once.");    }    parsed = true;    parseConfiguration(parser.evalNode("/configuration"));return configuration;}

parsed 标志位保证一个 XMLConfigBuilder 实例只被调用一次。这不是防御性编程的过度设计——Configuration 对象在解析过程中是被逐步填充的,就像往碗里加料,调完味了你再往里倒一遍酱汁,那不叫加料,那叫灾难。

3.4 parseConfiguration:按顺序解析各节点——顺序很重要

这是最核心的方法,决定了配置的加载顺序:

privatevoidparseConfiguration(XNode root){try {        propertiesElement(root.evalNode("properties"));        Properties settings = settingsAsProperties(            root.evalNode("settings"));        loadCustomVfs(settings);        loadCustomLogImpl(settings);        typeAliasesElement(root.evalNode("typeAliases"));        pluginElement(root.evalNode("plugins"));        objectFactoryElement(root.evalNode("objectFactory"));        objectWrapperFactoryElement(            root.evalNode("objectWrapperFactory"));        reflectorFactoryElement(            root.evalNode("reflectorFactory"));        settingsElement(settings);        environmentsElement(root.evalNode("environments"));        databaseIdProviderElement(            root.evalNode("databaseIdProvider"));        typeHandlerElement(root.evalNode("typeHandlers"));        mapperElement(root.evalNode("mappers"));    } catch (Exception e) {thrownew BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);    }}

这个顺序不是随意排的。几个值得注意的点:

  • properties 最先解析:后面的 settings、dataSource 等节点可能引用 ${placeholder},properties 不先加载,后面全是问号——跟做菜一个道理,调料得先备好,不然下锅的时候才发现盐罐子是空的
  • settings 先读后用:先读成 Properties 对象,中间穿插了 VFS 和日志配置的处理,最后才通过 settingsElement() 把设置值写入 Configuration。因为 settings 里有些配置会影响后续组件的行为(比如 cacheEnabled 决定要不要给 Executor 套一层 CachingExecutor 装饰器),得先把前面的杂事干完再应用
  • mappers 压轴解析:Mapper 解析依赖前面所有配置就位——typeAliases、typeHandlers、plugins 缺一不可。把它放最后,就像上菜前得先把桌布、餐具、调料碟都摆好

四、几个关键节点的解析细节

4.1 properties:三级覆盖——谁说了算?

propertiesElement 处理 <properties> 节点时的优先级,从低到高:

  1. <properties> 节点 resource 或 url 属性引用的外部文件(最软)
  2. <properties> 节点中 <property> 子标签定义的属性
  3. 方法参数传入的 Properties(通过 build() 的第三个参数传入,最硬)

覆盖规则:硬的覆盖软的。代码传入的参数是钦定的,谁也改不了;外部文件里的属性是默认值,随时可以被覆盖。这个设计跟 Spring 的属性覆盖策略思路一致——运行时参数应该优先于配置文件。毕竟线上出了问题要临时改个配置,总不能改完配置文件再重新打包部署吧?

源码里的实现方式很直白——先加载低优先级的,再用高优先级的 putAll 覆盖:

privatevoidpropertiesElement(XNode context)throws Exception {if (context != null) {        Properties defaults = context.getChildrenAsProperties();        String resource = context.getStringAttribute("resource");        String url = context.getStringAttribute("url");if (resource != null && url != null) {thrownew BuilderException("The properties element cannot specify both "                + "a URL and a resource based property file reference.");        }if (resource != null) {            defaults.putAll(                Resources.getResourceAsProperties(resource));        } elseif (url != null) {            defaults.putAll(                Resources.getUrlAsProperties(url));        }        Properties vars = configuration.getVariables();if (vars != null) {            defaults.putAll(vars);        }        parser.setVariables(defaults);        configuration.setVariables(defaults);    }}

注意一个小细节:resource 和 url 不能同时指定。这是 DTD 约束之外的一个运行时校验——MyBatis 在"你不能既要又要"这件事上立场很坚定。

4.2 settings:框架行为的全局开关

settings 节点控制 MyBatis 的核心行为,可以理解为框架的"控制面板"。settingsAsProperties 先把 XML 里的设置项读成 Properties,然后 settingsElement 把这些值逐个设到 Configuration 上。

一些能直接影响架构行为的 settings:

设置项
默认值
影响
一句话解释
cacheEnabled
true
是否启用二级缓存
开了就给 Executor 穿马甲
lazyLoadingEnabled
false
延迟加载开关
关了就是饿汉,开了就是懒汉
defaultExecutorType
SIMPLE
Executor 类型
SIMPLE/REUSE/BATCH 三选一
localCacheScope
SESSION
一级缓存范围
SESSION 级别可能踩坑
aggressiveLazyLoading
false (3.4.1+)
任意方法触发延迟加载
开了就是"一碰就加载"

settingsElement 方法逻辑简单但代码很长——逐个读取配置值并调用 Configuration 的 setter。有个细节值得夸:如果你配了一个不认识的 key,直接抛异常,不会静默忽略。这比那些"配错了也不报错,只是默默不生效"的框架厚道多了——早发现比晚排查好一百倍。

4.3 environments:数据源和事务——选哪个环境?

environmentsElement 解析 <environments> 节点,核心逻辑是根据 default 属性选中一个 <environment>,然后分别解析事务工厂和数据源:

privatevoidenvironmentsElement(XNode context)throws Exception {if (context != null) {if (environment == null) {            environment = context.getStringAttribute("default");        }for (XNode child : context.getChildren()) {            String id = child.getStringAttribute("id");if (isSpecifiedEnvironment(id)) {                TransactionFactory txFactory =                    transactionManagerElement(                        child.evalNode("transactionManager"));                DataSourceFactory dsFactory =                    dataSourceElement(                        child.evalNode("dataSource"));                DataSource dataSource = dsFactory.getDataSource();                Environment.Builder environmentBuilder =new Environment.Builder(id)                        .transactionFactory(txFactory)                        .dataSource(dataSource);                configuration.setEnvironment(                    environmentBuilder.build());            }        }    }}

Environment 是 Configuration 的一个内部组件,封装了 TransactionFactory 和 DataSource。这里用了 Builder 模式,因为 Environment 一旦构建就不应该被修改——数据源和事务工厂在运行时切换,好比飞机在飞行途中换引擎,出事概率不小。

事务管理器的类型通过 type 属性指定:JDBC 对应 JdbcTransactionFactory(自己管事务),MANAGED 对应 ManagedTransactionFactory(交给容器管)。数据源同理:UNPOOLED(来一个连一个)、POOLED(连池子里捞)、JNDI(从容器借)分别对应不同的 DataSourceFactory 实现。这些别名映射都是通过 TypeAliasRegistry 完成的。

4.4 mappers:SQL 映射的加载——压轴大戏

这是初始化过程中最复杂的部分。mapperElement 方法处理四种映射方式,就像食堂有四个窗口:

privatevoidmapperElement(XNode parent)throws Exception {if (parent != null) {for (XNode child : parent.getChildren()) {if ("package".equals(child.getName())) {// 1. 按包名扫描——自助餐模式,一扫一大片                String mapperPackage =                    child.getStringAttribute("name");                configuration.addMappers(mapperPackage);            } else {                String resource =                    child.getStringAttribute("resource");                String url =                    child.getStringAttribute("url");                String mapperClass =                    child.getStringAttribute("class");if (resource != null && url == null                        && mapperClass == null) {// 2. 指定 XML 资源路径——按菜单点菜                    ErrorContext.instance().resource(resource);                    InputStream inputStream =                        Resources.getResourceAsStream(resource);                    XMLMapperBuilder mapperParser =new XMLMapperBuilder(inputStream,                            configuration, resource,                            configuration.getSqlFragments());                    mapperParser.parse();                } elseif (resource == null && url != null                        && mapperClass == null) {// 3. 指定 XML 的 URL——叫外卖                    ErrorContext.instance().resource(url);                    InputStream inputStream =                        Resources.getUrlAsStream(url);                    XMLMapperBuilder mapperParser =new XMLMapperBuilder(inputStream,                            configuration, url,                            configuration.getSqlFragments());                    mapperParser.parse();                } elseif (resource == null && url == null                        && mapperClass != null) {// 4. 指定 Mapper 接口类——直接找厨师                    Class<?> mapperInterface =                        Resources.classForName(mapperClass);                    configuration.addMapper(mapperInterface);                } else {thrownew BuilderException("A mapper element may only specify "                        + "a url, resource or class, "                        + "but not a combination.");                }            }        }    }}

四种方式,每次只能选一种——又是"不能既要又要"。不管走哪条路,最终都会到达 XMLMapperBuilder.parse() 或 MapperAnnotationBuilder。前者解析 XML 形式的 Mapper,后者处理接口上的注解(比如 @Select)。

XMLMapperBuilder.parse() 做的事,按顺序排列:

  1. 检查是否已加载过(通过 configuration.isResourceLoaded)——防止重复加载
  2. 解析 <cache-ref> 和 <cache> 缓存配置
  3. 解析 <resultMap> 结果映射
  4. 解析 <parameterMap>(已过时但保留兼容——老项目的命也是命)
  5. 解析 <sql> SQL 片段
  6. 逐个解析 <select>/<insert>/<update>/<delete>,每个标签生成一个 MappedStatement 对象
  7. 处理未解析的引用(跨 Mapper 的缓存引用等)

这里有个容易忽略的细节:MappedStatement 的 id 是 namespace + "." + id,所以同一个 namespace 下不能有重复 id。不同 namespace 下倒是可以有相同 id——它们是不同的 MappedStatement。这就像同一栋楼里不同公司可以有同名员工,但同一家公司里不允许重名。

五、Configuration:MyBatis 的"上帝对象"

经过上面所有步骤的解析,Configuration 对象被填充完毕。它是 MyBatis 运行时的"上帝对象"——几乎所有组件都直接或间接依赖它。如果你在源码里迷路了,找 Configuration,它就是那张总地图。

Configuration 里存了什么?挑重点列:

// 核心运行时配置——决定框架行为protectedboolean cacheEnabled = true;protectedboolean lazyLoadingEnabled = false;protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;// 组件注册器——四大金刚protectedfinal TypeAliasRegistry typeAliasRegistry;protectedfinal TypeHandlerRegistry typeHandlerRegistry;protectedfinal MapperRegistry mapperRegistry;protectedfinal InterceptorChain interceptorChain;// SQL 映射——所有 SQL 都在这里protectedfinal Map<String, MappedStatement> mappedStatements;protectedfinal Map<String, Cache> caches;protectedfinal Map<String, ResultMap> resultMaps;// 环境配置——数据库连接在哪protected Environment environment;

Configuration 还承担了工厂职责——newExecutor()newStatementHandler()newParameterHandler()newResultSetHandler() 这些方法都在这里。创建组件的同时,它会调用 interceptorChain.pluginAll() 给组件套上插件代理。这个设计把组件创建和插件植入统一管理了——不然代理创建逻辑散落各处,维护起来是噩梦。

六、DefaultSqlSessionFactory:启动流程的终点

回到 SqlSessionFactoryBuilder.build(),最终返回的是 DefaultSqlSessionFactory

public SqlSessionFactory build(Configuration config){returnnew DefaultSqlSessionFactory(config);}

就这么一行。DefaultSqlSessionFactory 持有 Configuration 引用,它的 openSession() 方法才是运行时的起点。但那是下一篇的故事了——这篇已经够长了。

七、初始化流程总览

把完整链路画出来,一图胜千言:

mybatis-config.xml       │       ▼SqlSessionFactoryBuilder.build()       │       ▼XMLConfigBuilder 构造  ├── 创建 XPathParser(DOM 解析 + DTD 验证)  ├── 创建 Configuration(注册内置别名和处理器)  └── 设置 parsed = false       │       ▼XMLConfigBuilder.parse()       │       ▼parseConfiguration()  ├── properties     → configuration.variables       (调料先备好)  ├── settings       → configuration 各字段           (开关先设好)  ├── typeAliases    → typeAliasRegistry              (别名注册好)  ├── plugins        → interceptorChain               (插件挂上去)  ├── objectFactory  → configuration.objectFactory    (对象工厂就位)  ├── environments   → configuration.environment      (数据源连上)  ├── typeHandlers   → typeHandlerRegistry            (类型处理器注册好)  └── mappers        → mapperRegistry + mappedStatements (SQL 映射加载完)       │       ▼new DefaultSqlSessionFactory(configuration)

八、几个值得琢磨的设计点

为什么 Configuration 用"上帝对象"而不是拆成多个小类?

Configuration 确实体量大,字段多到让人头皮发麻。但它解决了一个实际问题:各组件之间的配置是一致的。如果拆成多个配置对象,组件之间传递配置的复杂度会急剧上升——你传五个小对象进来,还得保证它们之间的数据不矛盾。MyBatis 选了简单粗暴但有效的方案:一个对象持有所有状态。在框架场景下,这是合理的权衡。就像总控室——虽然一堆屏幕看着眼花,但至少信息都集中在一个地方,不用跑来跑去。

为什么 XMLConfigBuilder 只能用一次?

因为解析过程是增量修改 Configuration 的过程。如果允许重复调用 parse(),同一个 namespace 的 Mapper 会被重复注册,mappedStatements 会出现重复 key 或覆盖。用 parsed 标志位一刀切,虽然粗暴但安全——就像一次性筷子,用完就扔,不用担心上一个人用过。

为什么 mappers 放在最后解析?

Mapper 解析依赖 typeAliases、typeHandlers、plugins 这些前置配置。比如 <resultType> 里的别名需要 TypeAliasRegistry 已就绪,<resultMap> 里的 typeHandler 需要 TypeHandlerRegistry 已就绪,插件的注册需要在 Mapper 解析之前完成。解析顺序 = 依赖顺序——先铺路,再跑车。

下篇预告

下一篇从 DefaultSqlSessionFactory.openSession() 开始,跟踪 SqlSession 的创建过程。看看 Executor 是怎么被选型和初始化的——SIMPLE、REUSE、BATCH 三兄弟到底有啥区别,还有 Mapper 接口的动态代理是怎么绑上去的。那才是 SQL 真正开始执行的地方。


本系列文章基于 MyBatis 3.5.x 源码,写作时对照源码逐行验证。如果发现有问题的地方,欢迎指正。