刚接触 MyBatis 的时候,是不是总被 SqlSession
、SqlSessionFactory
这些“工厂”和“会话”的关系搞懵?为什么有时候明明关了 SqlSession
还是报连接泄漏?Mapper 接口到底该不该重复创建?今天咱们就从实际开发中的痛点出发,把 MyBatis 的三大核心组件的作用域扒个底朝天!
MyBatis 能高效操作数据库,离不开三个核心组件:
SqlSession
(会话)。Connection
,执行 SQL、管事务都靠它。UserMapper.java
其实是个接口,真正干活的是 MyBatis 动态生成的代理对象。这三个角色的“作用域”(生命周期、线程安全、使用范围)直接决定了 MyBatis 能不能用对、用得稳。咱们一个一个拆解!
SqlSessionFactory
是 MyBatis 的“命门”——它一旦创建,就会加载所有配置(比如数据库连接池、映射文件、插件),后续所有的 SqlSession
都得靠它来“生”。
想象一下:如果你每次操作数据库都新建一个 SqlSessionFactory
,那相当于每次都要重新读取配置文件、解析映射、初始化连接池……这得多耗时?而且连接池这种资源只能全局共享,重复创建只会浪费内存,甚至导致连接泄漏!
结论:SqlSessionFactory
必须是全局单例(整个应用生命周期只创建一次),就像你家的总电闸,只装一个!
通常用 SqlSessionFactoryBuilder
读取配置文件生成:
// 读取 mybatis-config.xml 配置
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 构建 SqlSessionFactory(全局单例,只在应用启动时执行一次!)
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
在 Spring 里更简单,直接通过 @Bean
注册成单例,完全不用自己操心~
SqlSession
是咱们和数据库“对话”的桥梁:执行 select
、insert
要靠它,开启事务、提交回滚也要靠它。但它有个致命特点——线程不安全!
假设你在一个线程里创建了 SqlSession
,然后在另一个线程里接着用它——这时候 SqlSession
内部的连接状态(比如当前事务是否开启、缓存的 SQL 结果)可能已经被前一个线程改乱了,轻则数据错误,重则直接抛异常!
举个真实踩坑案例:
之前有个同事在 Controller
里创建 SqlSession
,然后传给 Service
层用,结果高并发下频繁报 Invalid bound statement
(找不到 SQL 映射)。后来发现是因为 SqlSession
被多个线程共享,内部状态混乱,把刚加载的 Mapper 缓存给冲掉了……
SqlSession
的生命周期应该和一次业务操作绑定(比如一次 HTTP 请求),用完必须关闭(释放数据库连接回连接池)。
用 try-with-resources
自动关闭(Java 7+ 支持),安全又省心:
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectById(1); // 业务操作
} // 离开作用域自动关闭 SqlSession,连接归还连接池!
Spring 用 SqlSessionTemplate
替代了原生 SqlSession
,并且通过 ThreadLocal
实现了线程隔离。你在 Service
里注入 UserMapper
,底层会自动从当前线程的 SqlSession
里拿代理对象,完全不用手动关!
注意:如果用了 @Transactional
注解,Spring 会在事务结束后自动关闭 SqlSession
,但如果自己手动调用了 sqlSession.close()
,可能会导致事务失效,别手贱!
Mapper 接口(比如 UserMapper
)本身是个普通接口,MyBatis 会通过 JDK 动态代理生成它的实现类。这个代理对象的生命周期完全依赖 SqlSession
——SqlSession
什么时候关,代理对象就什么时候失效!
在同一个 SqlSession
里,不管调用多少次 sqlSession.getMapper(UserMapper.class)
,拿到的都是同一个代理对象(MyBatis 内部缓存了)。所以别想着在 SqlSession
里重复创建 Mapper,浪费资源!
在 Spring 里,通过 @MapperScan
扫描 Mapper 接口,会把它们注册为 Spring Bean(默认 singleton
作用域)。但这和你手动创建的 Mapper 代理对象不冲突——Spring 底层用 SqlSessionTemplate
包装了原生 SqlSession
,每次调用 Mapper 方法时,会从当前线程的 SqlSession
里动态获取代理对象,线程安全又省心!
为了方便记忆,咱们总结个表格:
组件 | 作用域 | 线程安全吗? | 怎么管生命周期? | 常见错误 |
---|---|---|---|---|
SqlSessionFactory | 全局单例(应用级) | 安全(无状态) | 应用启动时创建,全局只用一个 | 重复创建导致连接池耗尽 |
SqlSession | 线程局部(请求级) | 不安全(勿共享) | 一次业务操作创建,用完立刻关闭 | 跨线程共享、未关闭导致连接泄漏 |
Mapper 接口代理 | 依赖 SqlSession | 由 SqlSession 决定 | 随 SqlSession 创建/销毁 | 跨 SqlSession 使用、手动关闭后调用 |
SqlSession
存到全局变量、static
里,更别传给其他线程!try-with-resources
,Spring 里别手贱调用 close()
!SqlSession
后,Mapper 代理对象会失效,调用方法会报错!最后唠叨一句:MyBatis 的作用域设计其实很简单——谁创建谁管理,线程隔离保安全。只要记住这三个组件的“生命周期”和“线程安全”特性,基本不会踩大坑!下次写代码时,不妨多检查下 SqlSession
的关闭逻辑,保证你的应用又稳又快~
觉得有用的话,点个赞收藏,开发时遇到问题再来翻!