引言:为什么 一行注解就能让 Mapper 接口变成 Spring Bean
如果你只用过 Spring Boot 集成的 MyBatis,可能会觉得 @MapperScan("com.example.mapper") 是魔法——为什么一行注解就能让所有 Mapper 接口变成可以被 @Autowired 注入的 Spring Bean?答案藏在三个关键组件中:SqlSessionFactoryBean(用 FactoryBean 模式创建 SqlSessionFactory)、MapperScannerConfigurer(用 BDRPP 扫描并注册 Mapper 接口的 Bean 定义)、SqlSessionTemplate(线程安全的 SqlSession 包装器)。这篇文章把这三个组件的原理、Spring 事务如何接管 MyBatis 的 SqlSession 生命周期、以及整合中常见的坑讲清楚。
一、SqlSessionFactoryBean:用 FactoryBean 创建 SqlSessionFactory


为什么需要 FactoryBean
在 MyBatis 原生用法中,SqlSessionFactory 是通过 SqlSessionFactoryBuilder.build() 创建的,需要手动传入 mybatis-config.xml 的 InputStream。在 Spring 中,SqlSessionFactory 需要作为一个 Spring Bean 被管理,但它的创建过程不适合放在 @Bean 方法中——因为解析 XML 配置、注册 TypeHandler、加载插件等逻辑比较复杂。
SqlSessionFactoryBean 实现了 FactoryBean<SqlSessionFactory> 接口,将这个复杂的创建过程封装在 getObject() 方法中:
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean {
private DataSource dataSource;
private String configLocation; // mybatis-config.xml 路径
private String[ ] mapperLocations; // Mapper XML 文件路径
@Override
public SqlSessionFactory getObject() throws Exception {
return this.sqlSessionFactory; // 在 afterPropertiesSet() 中已创建
}
@Override
public void afterPropertiesSet() throws Exception {
// 1. 解析 mybatis-config.xml
// 2. 创建 Configuration 对象
// 3. 注册 DataSource 到 Configuration
// 4. 扫描并注册 Mapper XML
// 5. 注册插件(从 Spring 容器中获取 Interceptor Bean)
// 6. 调用 SqlSessionFactoryBuilder.build()
this.sqlSessionFactory = buildSqlSessionFactory();
}
}
关键点:SqlSessionFactoryBean 实现了 InitializingBean,在 afterPropertiesSet() 中完成 SqlSessionFactory 的构建。这意味着 SqlSessionFactory 在 Spring 容器初始化阶段就创建好了,而不是在第一次 getBean() 调用时才创建。
与 Spring Boot 自动配置的集成
Spring Boot 的 MybatisAutoConfiguration 自动配置了 SqlSessionFactoryBean:
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
// 设置 MyBatis 配置属性
factory.setConfiguration(mybatisProperties.getConfiguration());
// 设置 Mapper 文件位置
factory.setMapperLocations(mybatisProperties.resolveMapperLocations());
return factory.getObject();
}
二、MapperScannerConfigurer:自动扫描并注册 Mapper 接口

原理:BeanDefinitionRegistryPostProcessor
MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor,在 Spring 容器 refresh 的早期阶段(BDRPP 阶段)扫描指定包路径下的所有 Mapper 接口,为每个接口创建一个 BeanDefinition 并注册到 BeanDefinitionRegistry。
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor {
private String basePackage;
private Class<?> markerInterface; // 可选:只扫描继承了特定接口的 Mapper
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAnnotationClass(Mapper.class); // 只扫描带 @Mapper 注解的接口
scanner.registerFilters();
scanner.scan(basePackage); // 扫描并注册
}
}
注册的 BeanDefinition 是什么类型
ClassPathMapperScanner 扫描到 Mapper 接口后,创建的 BeanDefinition 的 beanClass 是 MapperFactoryBean.class 而不是 Mapper 接口本身。MapperFactoryBean 也是一个 FactoryBean,它的 getObject() 方法返回 sqlSession.getMapper(mapperInterface) 的代理对象。
public class MapperFactoryBean<T> implements FactoryBean<T> {
private Class<T> mapperInterface;
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
}
所以,@Autowired UserMapper userMapper 实际注入的是 MapperFactoryBean 的 getObject() 返回值——即 MapperProxy 代理对象。
@MapperScan 与 @Mapper 的区别
@MapperScan 是 Spring 的注解,用在 @Configuration 类上,批量扫描包路径下的 Mapper 接口。@Mapper 是 MyBatis 的注解,用在每个 Mapper 接口上,逐个标记。@MapperScan 可以不依赖 @Mapper 注解(通过 annotationClass 属性指定其他注解),但通常两者配合使用。
三、SqlSessionTemplate:线程安全的 SqlSession 包装器


为什么需要 SqlSessionTemplate
原生的 DefaultSqlSession 不是线程安全的——每个线程应该使用自己的 SqlSession。在 Spring 中,所有 Mapper 代理都共享同一个 SqlSessionFactory,但每个操作需要一个独立的 SqlSession。SqlSessionTemplate 通过动态代理解决了这个问题:
public class SqlSessionTemplate implements SqlSession {
private final SqlSessionFactory sqlSessionFactory;
private final SqlSession sqlSessionProxy; // 动态代理对象
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
SqlSession.class.getClassLoader(),
new Class[ ]{SqlSession.class},
new SqlSessionInterceptor());
}
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[ ] args) throws Throwable {
// 从 TransactionSynchronizationManager 获取当前线程绑定的 SqlSession
SqlSession sqlSession = SqlSessionUtils.getSqlSession(
sqlSessionFactory, ExecutorType.SIMPLE, null);
// 委托给实际的 SqlSession 执行
return method.invoke(sqlSession, args);
// 如果不在事务中,自动关闭 SqlSession
}
}
}
SqlSessionTemplate 的核心逻辑在 SqlSessionInterceptor 中:每次方法调用时,先从 TransactionSynchronizationManager 获取当前线程绑定的 SqlSession;如果当前有事务,使用事务中的 SqlSession(确保同一事务使用同一个 SqlSession);如果没有事务,创建一个新的 SqlSession,执行完后自动关闭。
与 Spring 事务的集成
当 Spring 事务(@Transactional)开启时,DataSourceTransactionManager 在 doBegin() 中获取一个 JDBC Connection 并绑定到当前线程。MyBatis 的 SqlSessionUtils.getSqlSession() 检测到当前有事务,会从 TransactionSynchronizationManager 获取或创建一个 SqlSession,并将其绑定到同一事务中。事务提交时,SqlSession.commit() 被调用;事务回滚时,SqlSession.rollback() 被调用。
这解释了为什么在 Spring 中,同一个事务内的多次 Mapper 调用共享同一个 SqlSession——因为这个 SqlSession 被绑定到了 TransactionSynchronizationManager 的线程本地存储中。
四、MyBatis-Spring 的完整初始化链路

当 Spring Boot 应用启动时,MyBatis 的自动配置执行以下步骤:
MybatisAutoConfiguration被激活(因为 classpath 上有SqlSessionFactory类)- 创建
SqlSessionFactoryBean,注入DataSource,调用getObject()获取SqlSessionFactory - 创建
SqlSessionTemplate,注入SqlSessionFactory MapperScannerConfigurer(由@MapperScan触发)扫描 Mapper 接口,为每个接口注册MapperFactoryBean的 Bean 定义- 每个
MapperFactoryBean在实例化时,从容器中获取SqlSessionTemplate,调用getMapper()创建 Mapper 代理
五、常见陷阱


陷阱一:MapperScannerConfigurer 中不能使用 @Autowired
MapperScannerConfigurer 实现了 BDRPP,在 Bean 实例化之前执行。如果它依赖其他 Bean(如 DataSource),不能通过 @Autowired 注入,必须通过 BeanFactoryPostProcessor 的方式显式设置。
陷阱二:多数据源时 Mapper 绑定到错误的数据源
当项目中有多个 DataSource 时,默认的 SqlSessionFactory 绑定到 @Primary 的 DataSource。如果某些 Mapper 需要使用非主数据源,需要创建额外的 SqlSessionFactory 和 SqlSessionTemplate,并通过 @MapperScan 的 sqlSessionFactoryRef 或 sqlSessionTemplateRef 属性指定。
陷阱三:SqlSession 泄漏
在非事务场景下,SqlSessionTemplate 会在每次操作后自动关闭 SqlSession。但如果你手动调用 sqlSessionTemplate.getConnection() 获取了 JDBC Connection,这个 Connection 不会自动关闭——SqlSession 关闭时不会关闭外部持有的 Connection 引用。始终在 try-with-resources 中管理手动获取的 Connection。
陷阱四:一级缓存在非事务场景下失效
SqlSessionTemplate 在非事务场景下,每次 Mapper 方法调用都会创建一个新的 SqlSession 并在调用后关闭。这意味着两次相同的查询不会命中一级缓存(因为 SqlSession 不同)。解决方式:开启 Spring 事务,确保同一事务内使用同一个 SqlSession。
陷阱五:@MapperScan 重复扫描
如果在多个 @Configuration 类上重复配置了 @MapperScan 且扫描范围有重叠,同一个 Mapper 接口会被注册多次(创建多个 MapperFactoryBean),导致 Bean 定义冲突。解决方式:确保 @MapperScan 只配置一次,或者在多个配置上使用不同的 basePackages。
小结:整合的本质是"生命周期管理"
MyBatis 与 Spring 整合的核心,不是技术上的创新,而是"生命周期管理"的转移:SqlSessionFactory 的创建从手动调用 SqlSessionFactoryBuilder 转移到了 FactoryBean;SqlSession 的创建和销毁从手动管理转移到了 SqlSessionTemplate 的代理;Mapper 接口的注册从手动调用 configuration.addMapper() 转移到了 MapperScannerConfigurer 的自动扫描。理解这三层"转移",你就理解了 MyBatis-Spring 整合的全部。
(以上正文阅读时长约 40-50 分钟,以下问答部分不计入正文阅读时长)
文末答疑:面试官会怎么问
问 1:**SqlSessionFactoryBean**为什么实现**InitializingBean**而不是在**getObject()**中创建**SqlSessionFactory**?这两者有什么区别?
答:InitializingBean.afterPropertiesSet() 在 Bean 属性设置完成后、getObject() 第一次调用之前执行。如果 SqlSessionFactory 在 getObject() 中创建,创建时机是"第一次 getBean(SqlSessionFactory.class) 调用时";如果 SqlSessionFactory 在 afterPropertiesSet() 中创建,创建时机是"容器初始化 SqlSessionFactoryBean 这个 Bean 时"。后者更早创建,意味着如果 SqlSessionFactory 的创建有错误(如 XML 解析失败),错误会在启动时暴露,而不是在第一次数据库操作时暴露——这符合"fail-fast"原则。此外,其他依赖 SqlSessionFactory 的 Bean(如 MapperFactoryBean)可以在容器初始化阶段就使用它,不需要等待懒加载。
问 2:**@MapperScan**和**@Mapper**注解,如果只用一个,能正常工作吗?什么情况下只需要其中一个?
答:@MapperScan 是 Spring 层面的注解,通过 ClassPathMapperScanner 扫描包路径,不依赖 @Mapper 注解。@Mapper 是 MyBatis 层面的注解,标记单个接口,但 Spring 不会自动识别 @Mapper 注解(除非你配置了 <mybatis:scan> 或 @MapperScan 的 annotationClass 属性包含 @Mapper)。两者的关系:@MapperScan 是"批量扫描",@Mapper 是"逐个标记";@MapperScan 可以不依赖 @Mapper(通过 markerInterface 或自定义注解),但 @Mapper 单独使用不会生效(需要配合 @MapperScan 或 XML 的 <mappers> 配置)。
问 3:**SqlSessionTemplate**的代理对象每次调用 Mapper 方法时都会创建一个新的**SqlSession**,那 MyBatis 的一级缓存还有意义吗?怎么让它生效?
答:在非事务场景下,SqlSessionTemplate 每次方法调用都创建新的 SqlSession 并关闭,一级缓存确实不生效。在 Spring 事务场景下,同一个事务内的所有 Mapper 方法调用共享同一个 SqlSession(因为 SqlSessionUtils.getSqlSession() 会从 TransactionSynchronizationManager 获取已绑定的 SqlSession),一级缓存生效。所以,如果你依赖一级缓存优化性能,必须确保查询在同一个 @Transactional 方法内执行。
问 4:如果你的项目有多个数据源,你怎么让不同的 Mapper 使用不同的数据源?关键配置是什么?
答:需要创建多套 DataSource → SqlSessionFactory → SqlSessionTemplate 的 Bean 链条。每个数据源对应一个 SqlSessionFactory(通过 SqlSessionFactoryBean.setDataSource() 指定),每个 SqlSessionFactory 对应一个 SqlSessionTemplate。然后通过 @MapperScan 的 sqlSessionTemplateRef 属性指定不同包的 Mapper 使用不同的 SqlSessionTemplate。关键是:@MapperScan 必须配置多个,每个指定不同的 basePackages 和 sqlSessionTemplateRef,确保扫描范围不重叠。
问 5:**MapperScannerConfigurer**在**BeanDefinitionRegistryPostProcessor**阶段执行,此时**DataSource**还没有被实例化。如果**MapperScannerConfigurer**需要**DataSource**怎么办?
答:MapperScannerConfigurer 在 BDRPP 阶段执行,但它不直接依赖 DataSource——它只是注册 MapperFactoryBean 的 Bean 定义,不创建 MapperFactoryBean 实例。MapperFactoryBean 的实例化发生在 Bean 初始化阶段,此时 DataSource 和 SqlSessionFactory 已经就绪。但如果 MapperScannerConfigurer 确实需要 DataSource(比如需要根据 DataSource 的配置动态决定扫描范围),有两种方案:第一,让 MapperScannerConfigurer 实现 BeanFactoryAware,通过 BeanFactory 获取 DataSource 的 BeanDefinition(不是实例),读取其属性;第二,不用 MapperScannerConfigurer,改用编程式注册:在 @Configuration 类中注入 BeanFactory,手动调用 ClassPathMapperScanner 扫描。但第二种方案失去了 BDRPP 阶段的自动扫描优势,通常不推荐。