1.3 SqlSessionFactory 的构建与解剖 本节摘要:SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession 构成 MyBatis 的三层对象链。本节解剖全局配置文件各配置块的职责,比较三种数据源实现的取舍,整理高频 settings 开关,并说明工厂为什么必须全局单例。 上一节我们把配置文件喂给了 Builder,拿到了工厂。这一节把这台"器械柜"拆开看:配置怎么被消化、环境怎么切换、哪些开关值得记住。 三层对象链:从图纸到手术通道 三层的生命周期一句话记牢:Builder 方法级、Factory 应用级、Session 请求级。
本节摘要:SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession 构成 MyBatis 的三层对象链。本节解剖全局配置文件各配置块的职责,比较三种数据源实现的取舍,整理高频 settings 开关,并说明工厂为什么必须全局单例。
上一节我们把配置文件喂给了 Builder,拿到了工厂。这一节把这台"器械柜"拆开看:配置怎么被消化、环境怎么切换、哪些开关值得记住。
// 三层对象链:Builder 用完即弃,Factory 全局单例,Session 一次一台 try (InputStream in = Resources.getResourceAsStream("mybatis-config.xml")) { // 第 1 层:Builder 解析 XML,产出 Factory。解析完就没用了 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(in); // 第 2 层:Factory 每次调用 openSession 产出一条新通道 SqlSession session = factory.openSession(); // 第 3 层:Session 承载一次工作单元,用完关闭 // ... 业务操作 ... session.close(); }
三层的生命周期一句话记牢:Builder 方法级、Factory 应用级、Session 请求级。Builder 内部把 XML 解析成一个 Configuration 大对象,然后连同 Factory 一起丢掉——它存在的意义只是"把图纸变成器械柜"这一次性动作。
配置的加载不止 XML 一条路。除了从流读取,还可以从 Reader 读取,或完全用 Java 代码拼装(无 XML 风格)。工程上 XML 仍是主流:SQL 与配置都集中在文件里,改语句不用重新编译。
// 纯 Java 配置:不写 XML 的等价写法(适合极简场景或动态组装) DataSource ds = PooledDataSource( "com.mysql.cj.jdbc.Driver", "jdbc:mysql://localhost:3306/mybatis_db", "root", "root123"); Environment env = new Environment("dev", new JdbcTransactionFactory(), ds); Configuration cfg = new Configuration(env); cfg.addMapper(UserMapper.class); // 注册映射 cfg.setMapUnderscoreToCamelCase(true); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(cfg);

解析有顺序讲究:properties 最先被读入(后面所有配置块都能引用它的占位符),mappers 最后注册(注册时会解析所有 SQL 节点)。把连接信息抽到独立的 properties 文件,是让密码离开主配置的第一步:
<!-- 主配置引用外部属性:数据库密码不进代码库 --> <properties resource="db.properties"/> <dataSource type="POOLED"> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource>
environments 的设计意图是一套配置文件装下多套环境(开发、测试、生产),靠 default 属性选一套生效:
<environments default="dev"> <environment id="dev"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="url" value="${dev.jdbc.url}"/> <!-- 其余省略 --> </dataSource> </environment> <environment id="prod"> <transactionManager type="MANAGED"/> <dataSource type="JNDI"> <!-- 生产环境从容器命名空间取数据源 --> <property name="initial_context" value="java:comp/env"/> <property name="data_source" value="jdbc/prodDs"/> </dataSource> </environment> </environments>
三种数据源的取舍值得单独记:
| 类型 | 行为 | 适用 |
|---|---|---|
| UNPOOLED | 每次请求新开连接,用完即关 | 小工具、测试代码 |
| POOLED | 内置连接池,复用连接 | 无容器环境的标准选择 |
| JNDI | 从外部容器取数据源 | 传统应用服务器部署 |
| 开关 | 默认 | 作用 |
|---|---|---|
| mapUnderscoreToCamelCase | false | 列 created_time 自动映射属性 createdTime |
| cacheEnabled | true | 二级缓存总开关(5.1 节展开) |
| lazyLoadingEnabled | false | 关联对象按需加载(3.2 节展开) |
| defaultExecutorType | SIMPLE | 执行器类型,可切 BATCH(2.3 节展开) |
| localCacheScope | SESSION | 一级缓存范围,可收窄为 STATEMENT |
| logImpl | 未设置 | 指定日志实现,调试期开 STDOUT_LOGGING |
这些开关在 XML 里配置,也有对应的 Java 方法(如 setMapUnderscoreToCamelCase),语义完全一致。
💡 关键直觉:全局配置在工厂构建时被一次性消化进 Configuration 对象——工厂建好后再改配置文件不会生效。这让"配置热更新"在原生 MyBatis 里没有立足点,也是工厂必须单例的深层原因:多份工厂意味着多份不一致的配置。
工厂持有连接池、全部映射语句的解析结果、缓存实现与插件链。建两个工厂,等于开了两个互不相通的连接池、两套互不相通的二级缓存——资源翻倍,收益为零。标准做法是用单例模式包一层:
// 单例工厂:双重检查锁,保证全局一份 public class FactoryHolder { private static volatile SqlSessionFactory instance; public static SqlSessionFactory get() { if (instance == null) { synchronized (FactoryHolder.class) { if (instance == null) { try (InputStream in = Resources.getResourceAsStream("mybatis-config.xml")) { instance = new SqlSessionFactoryBuilder().build(in); } catch (IOException e) { throw new IllegalStateException("工厂构建失败", e); } } } } return instance; } }
变式:给上面的单例加一个 build(Environment 指定环境) 的重载,让测试代码能显式选择一套环境构建工厂——这是多环境配置最常见的落地方式(其实 Builder 的 build 方法本就带 environment 参数,试试用它替换 default 属性方案,体会两种切换风格)。
器械柜认完了,下一节钻进从柜中取出的手术通道:SqlSession 的开合、方法族,以及一次真实的数据丢失排错。