写在前面
看 MyBatis 源码的人一般分两种:一种是面试被问得怀疑人生来补课的,另一种是真要在项目里搞深度定制(写分页插件、魔改缓存策略之类)。不管你是哪一种,入口都一样——你得先搞明白 MyBatis 启动的时候到底干了些什么勾当,那些 XML 配置是怎么一步步变成运行时能用的对象的。
这篇文章就把这条链路从头跟到尾。读完之后你应该能回答一个面试高频题:SqlSessionFactoryBuilder.build() 这一行代码背后,到底发生了多少事?
剧透:比你以为的多得多。
一、整体架构:三层楼,各干各的
先不急着看代码。MyBatis 的模块划分搞不清楚,后面读源码就像逛商场不带导览图——走到哪算哪,永远不知道自己在几楼。
MyBatis 的源码包结构大致分三层:
session | |||
executorscripting、mapping、builder | |||
typeio、logging、reflection、transaction |
一个 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);}两行关键代码,其余都是打杂的异常处理和资源关闭:
parser.parse()—— 解析 XML,构建Configuration对象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 引用和几个注册器(TypeAliasRegistry、TypeHandlerRegistry),子类共享这些注册器来完成类型转换和别名查找。这就好比后厨共享一个调料架——大家都能用,但调料架只有一份。
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 → Integer、string → 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> 节点时的优先级,从低到高:
<properties>节点resource或url属性引用的外部文件(最软)<properties>节点中<property>子标签定义的属性方法参数传入的 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 | |||
lazyLoadingEnabled | |||
defaultExecutorType | |||
localCacheScope | |||
aggressiveLazyLoading |
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() 做的事,按顺序排列:
检查是否已加载过(通过 configuration.isResourceLoaded)——防止重复加载解析 <cache-ref>和<cache>缓存配置解析 <resultMap>结果映射解析 <parameterMap>(已过时但保留兼容——老项目的命也是命)解析 <sql>SQL 片段逐个解析 <select>/<insert>/<update>/<delete>,每个标签生成一个MappedStatement对象处理未解析的引用(跨 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 源码,写作时对照源码逐行验证。如果发现有问题的地方,欢迎指正。
夜雨聆风