概述
如果说 Spring 是一个拥有无数插槽、遵循统一电气标准的巨大插座,那么 MyBatis-Spring 就是一个设计精巧、完美适配的插头。它不仅为 Spring 生态接入了强大的 ORM 能力,其整合过程本身,就是对 Spring 扩展点体系最经典、最优雅的诠释。
在前面的 Spring 核心容器系列中,我们系统性地深入了 IoC 容器、Bean 生命周期、依赖注入,更掌握了 @Import、ImportBeanDefinitionRegistrar、FactoryBean、BeanPostProcessor、TransactionSynchronizationManager 等关键的扩展点接口。现在,是时候运用这些强大的“武器库”知识,去解剖 MyBatis-Spring 这个极其成功的整合案例了。
MyBatis 的 Mapper 接口本身只是一个接口,无法实例化,更无法直接注入到 Service 中。但通过 @MapperScan 一个注解,这些接口就神奇地变成了可以 @Autowired 的、功能完备的 Bean。这背后的魔法并非“黑科技”,而是 Spring 扩展点体系的经典演绎:ImportBeanDefinitionRegistrar 负责扫描和动态注册,FactoryBean 负责在运行时创建代理对象,TransactionSynchronizationManager 则作为桥梁,巧妙地保证了事务的一致性。
本文将带你用你已掌握的扩展点知识作为钥匙,打开 MyBatis-Spring 整合的黑箱。我们不仅会分析整合的每个步骤,更要深入到 MapperProxy 的内部,看清每一次方法调用是如何精准地转化为 SQL 执行,并理解这种设计为什么堪称“最佳实践”。
核心要点
- 你已掌握的关键知识:
@Import、ImportBeanDefinitionRegistrar、FactoryBean、BeanFactoryPostProcessor、InitializingBean、TransactionSynchronizationManager。 - MyBatis 整合的核心步骤:
- 注解驱动入口:
@MapperScan通过@Import触发MapperScannerRegistrar。 - 动态Bean定义注册:
MapperScannerRegistrar利用ClassPathMapperScanner扫描接口,并为每个接口向BeanDefinitionRegistry动态注册一个类型为MapperFactoryBean的 BeanDefinition。 - 代理对象创建:Spring 容器创建
MapperFactoryBean实例时,其getObject()方法通过SqlSession.getMapper()返回一个MapperProxy动态代理对象。 - 方法调用转SQL:
MapperProxy.invoke()方法拦截所有接口方法调用,根据方法签名定位 MappedStatement,委托给SqlSession执行,完成从 Java 方法到 JDBC 操作的转换。 - 事务与线程安全:
SqlSessionTemplate通过内部代理,利用TransactionSynchronizationManager实现SqlSession的线程安全管理和与 Spring 事务的自动同步。
- 注解驱动入口:
- 用 Spring 知识解读:MyBatis 团队遵循了 Spring 定义的“契约”(即扩展点接口),而无需修改 Spring 核心代码。这种“插拔式”集成的能力,正是 Spring 设计哲学中“开放-闭合”原则的精髓所在。
文章组织架构图
下面这张 Mermaid 流程图清晰地描绘了本文的认知路径和知识模块间的层级关系。
graph TD
subgraph A ["认知起点"]
1["1. 前置知识 重温Spring扩展点武器库"]
end
subgraph B ["整合入口与动态注册"]
2["2. 整合入口 MapperScan与Import的握手"]
3["3. 动态注册 MapperScannerRegistrar如何无中生有"]
end
subgraph C ["代理创建与事务同步"]
4["4. 双重代理创建 FactoryBean SqlSessionTemplate与MapperProxy"]
5["5. 事务同步与缓存 MyBatis如何融入Spring事务管理"]
end
subgraph D ["高级应用与全局视角"]
6["6. 多数据源支持 MapperScan高级属性的原理"]
7["7. 整体流程串联 整合时序图与速查表"]
end
subgraph E ["反思与实战闭环"]
8["8. 设计反思 为什么说这是扩展点的最佳实践"]
9["9. 生产事故排查专题"]
10["10. 面试高频专题"]
end
1 --> 2
2 --> 3
3 --> 4
4 --> 5
5 --> 6
6 --> 7
7 --> 8
8 --> 9
9 --> 10
style A fill:#e1f5fe,stroke:#01579b
style B fill:#fff3e0,stroke:#e65100
style C fill:#e8f5e9,stroke:#1b5e20
style D fill:#f3e5f5,stroke:#4a148c
style E fill:#fce4ec,stroke:#880e4f
架构图说明
- 总览说明:全文 10 个模块遵循一条清晰的认知链条。我们从你已掌握的 Spring 扩展点(模块 1)出发,首先找到整合的注解入口(模块 2),然后深入底层的 BeanDefinition 动态注册过程(模块 3)。接着,重点剖析双重代理的创建和协作(模块 4),并解释事务同步的机制(模块 5)。在此基础上,探讨多数据源等高级应用(模块 6),并通过一张完整的时序图将所有步骤串联起来(模块 7)。最后,通过设计反思(模块 8)、事故排查(模块 9)和面试专题(模块 10)完成从理论到实践的闭环。
- 逐模块说明:每个模块都旨在用 Spring 扩展点的语言来解释 MyBatis 的行为。例如,模块 3 将
MapperScannerRegistrar视为ImportBeanDefinitionRegistrar的实践;模块 4 将MapperFactoryBean视为FactoryBean的经典案例;模块 5 用TransactionSynchronizationManager的视角解读线程安全与事务同步。 - 关键结论:深入理解 Spring 扩展点后,你会发现任何一个类似 MyBatis 的第三方框架整合,其本质都是在遵循并利用这套扩展契约。MyBatis-Spring 不仅是优秀的工具,更是学习 Spring 扩展点体系的最佳教学案例。
1. 前置知识:重温 Spring 扩展点武器库
如同顶级外科医生在术前会检视所有手术器械一样,在我们开始“解剖” MyBatis-Spring 整合原理之前,有必要重温一下那些我们将反复用到的 Spring 核心扩展点“手术刀”。这些接口并非新知识,而是我们前文《Spring 容器扩展点大全》中的老朋友。现在,我们将以它们的“契约”和“能力”为核心,快速回顾。
-
@Import(Spring Context)- 契约:标注在配置类上,用于导入一个或多个组件。可以导入普通配置类、
ImportSelector的实现类或ImportBeanDefinitionRegistrar的实现类。 - 能力:它是 Spring 注解驱动编程模式的核心枢纽之一。它允许我们将一组相关的配置或注册逻辑打包成一个独立的模块,通过一个简单的注解就能引入。
- 本文关联:
@MapperScan注解的核心就是@Import(MapperScannerRegistrar.class)。它通过@Import这根线,将 MyBatis 的注册逻辑拉入 Spring 容器的启动流程。
- 契约:标注在配置类上,用于导入一个或多个组件。可以导入普通配置类、
-
ImportBeanDefinitionRegistrar(Spring Context)- 契约:
void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)。 - 能力:这是一个强大的、允许我们通过编程方式向
BeanDefinitionRegistry动态注册BeanDefinition的接口。它工作在 Bean 实例化之前,是“无中生有”地向容器贡献新组件的最佳方式。importingClassMetadata参数还携带着导入该 Registrar 的类上的所有注解信息,可以用于获取自定义属性。 - 本文关联:
MapperScannerRegistrar就是这个接口的实现,它正是通过此方法,将扫描到的 Mapper 接口定义转化为MapperFactoryBean的BeanDefinition并注册到容器中。
- 契约:
-
FactoryBean<T>(Spring Beans)- 契约:
T getObject()、Class<?> getObjectType()、default boolean isSingleton()。 - 能力:这是一个用于定制 Bean 创建逻辑的工厂 Bean。当一个 Bean 的实例化过程很复杂(例如需要创建代理),或者需要返回的类型与 Bean 本身的类型不同时,
FactoryBean是最佳选择。Spring 容器会调用它的getObject()方法来获取最终放入容器的 Bean 实例,而FactoryBean本身可以通过&beanName来获取。 - 本文关联:
MapperFactoryBean是FactoryBean的完美实现。它的beanClass是MapperFactoryBean,但它的getObject()返回的却是 Mapper 接口的代理对象。这就是我们能够注入并使用 Mapper 接口的根本原因。
- 契约:
-
InitializingBean(Spring Beans)- 契约:
void afterPropertiesSet()。 - 能力:由 Spring 容器调用,允许 Bean 在所有属性设置完毕之后执行自定义的初始化逻辑,例如校验关键配置。
- 本文关联:
MapperFactoryBean实现了此接口,在其afterPropertiesSet()方法中会校验非空的关键属性(如SqlSessionFactory或SqlSessionTemplate)。
- 契约:
-
TransactionSynchronizationManager(Spring Tx)- 契约:提供一组静态方法,如
bindResource()、getResource()、unbindResource()等。 - 能力:它是 Spring 事务管理的核心基础设施,用于在每个线程中管理事务相关的资源,如数据库连接
Connection。它将资源绑定到当前线程,确保在同一个事务中的所有操作都能共享同一个底层资源。 - 本文关联:
SqlSessionTemplate的内部代理SqlSessionInterceptor就是通过TransactionSynchronizationManager.getResource()来获取当前事务绑定的SqlSession,从而实现SqlSession的线程安全和事务同步。 - 关键结论:这一系列接口构成了 Spring 强大的扩展点体系。MyBatis-Spring 整合的秘诀,并非修改 Spring 源码,而是精妙地实现了这些接口的“契约”,从而将自己完美地“卡”入 Spring 的生命周期和管理体系中。
- 契约:提供一组静态方法,如
2. 整合入口:@MapperScan(及 @Mapper)与 @Import 的握手
整合的第一步,是找到整个魔法开始的“开关”。这个开关,就是 @MapperScan 注解。
2.1 @MapperScan 注解源码分析
我们直接来看 @MapperScan 注解的定义。
// 类全限定名: org.mybatis.spring.annotation.MapperScan
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
// 【Spring扩展点】通过 @Import 导入 MapperScannerRegistrar
@Import(MapperScannerRegistrar.class)
public @interface MapperScan {
// 扫描的基础包,支持占位符
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
// BeanName生成策略
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
// 自定义的注解过滤,默认为 @Mapper
Class<? extends Annotation> annotationClass() default Annotation.class;
// 标记器接口过滤
Class<?> markerInterface() default Class.class;
// 引用的 SqlSessionTemplate Bean 名称,用于多数据源
String sqlSessionTemplateRef() default "";
// 引用的 SqlSessionFactory Bean 名称,用于多数据源
String sqlSessionFactoryRef() default "";
// 自定义的 MapperFactoryBean 类型
Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;
}
这段代码虽短,却蕴含玄机。最关键的便是元注解 @Import(MapperScannerRegistrar.class)。它告诉我们,@MapperScan 本身除了提供一些配置属性外,不执行任何逻辑。真正的逻辑触发点,是 Spring 在处理 @Import 注解时,会实例化并调用 MapperScannerRegistrar。
2.2 @Mapper 注解的补充说明与对比
除了 @MapperScan 批量扫描,MyBatis 还提供了 @Mapper 注解来标记单个接口。它的定义如下:
// 类全限定名: org.apache.ibatis.annotations.Mapper
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Mapper {
}
这个注解非常“干净”,没有任何元注解。它本身只是一个标记。在纯 Spring 环境下,单独使用 @Mapper 注解是无效的,因为没有处理器能识别它。它必须配合 @MapperScan 的 annotationClass 属性(默认就会扫描标注了 @Mapper 的接口),或者由 MyBatis-Spring 的 MapperScannerConfigurer 来处理。
用 Spring 知识解释:
@MapperScan 遵循了 Spring 的“注解驱动”模式。我们可以对比 @ComponentScan -> @Import(ComponentScanRegistrar.class) 或者 @EnableTransactionManagement -> @Import(TransactionManagementConfigurationSelector.class)。这些模式都是“声明式注解 + @Import 导入处理器”的经典组合。Spring 只负责解析 @Import 并调用处理器,而具体要扫描什么、如何注册,完全由处理器(Registrar)决定。这就是“开放-闭合”原则:Spring 平台对外开放,但对修改闭合,保证了核心的稳定。
3. 动态注册:MapperScannerRegistrar 如何“无中生有”
@Import 触发的 MapperScannerRegistrar 是整个整合流程的“发动机”。它实现自 ImportBeanDefinitionRegistrar,我们来看它是如何工作的。
3.1 MapperScannerRegistrar 源码解读
// 类全限定名: org.mybatis.spring.annotation.MapperScannerRegistrar
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
// ...
// 【Spring扩展点】importingClassMetadata 是 @MapperScan 所在配置类的元数据
// registry 就是 BeanDefinitionRegistry,我们可以向它注册新的Bean定义
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 1. 从 @MapperScan 注解中获取所有属性值
AnnotationAttributes annoAttrs = AnnotationAttributes
.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
if (annoAttrs == null) {
return;
}
// 2. 创建 ClassPathMapperScanner,这是真正负责扫描的类
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// 3. 将注解属性注入到 scanner 中,实现配置传递
scanner.setSqlSessionTemplateRef(annoAttrs.getString("sqlSessionTemplateRef"));
scanner.setSqlSessionFactoryRef(annoAttrs.getString("sqlSessionFactoryRef"));
scanner.setAnnotationClass(annoAttrs.getClass("annotationClass"));
scanner.setMarkerInterface(annoAttrs.getClass("markerInterface"));
scanner.setMapperFactoryBean(annoAttrs.getClass("factoryBean"));
// ... 其他属性设置
// 4. 注册各种过滤器
scanner.registerFilters();
// 5. 确定扫描的包路径,并调用 scanner.doScan() 启动扫描
List<String> basePackages = new ArrayList<>();
// ... 解析 value/basePackages/basePackageClasses 属性,填充 basePackages
scanner.doScan(StringUtils.toStringArray(basePackages));
}
}
逐段解读:
registerBeanDefinitions方法:这是ImportBeanDefinitionRegistrar接口的核心方法。importingClassMetadata参数允许我们获取@MapperScan注解的所有配置,而registry参数则给了我们向容器动态注册 Bean 的能力。- 创建
ClassPathMapperScanner:MapperScannerRegistrar本身不直接扫描,它将具体的扫描和注册任务委托给ClassPathMapperScanner。这种职责分离的设计非常清晰。 - 配置传递:
MapperScannerRegistrar作了一个桥梁,它从注解中读取sqlSessionFactoryRef等配置,然后设置给ClassPathMapperScanner。这是注解驱动与编程式配置的结合。 - 启动扫描:最后调用
scanner.doScan(basePackages),在指定的包下进行 Bean 的扫描和注册。
3.2 ClassPathMapperScanner.doScan 的核心逻辑:“偷梁换柱”
ClassPathMapperScanner 继承了 Spring 的 ClassPathBeanDefinitionScanner。Spring 的原生扫描器只能为带有 @Component 等注解的类生成 BeanDefinition,并把这些类本身作为 beanClass。而我们的 Mapper 接口显然不符合要求。MyBatis-Spring 在这里玩了一个漂亮的“偷梁换柱”。
// 类全限定名: org.mybatis.spring.mapper.ClassPathMapperScanner
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
// ...
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
// 1. 【关键】调用父类(Spring)的doScan方法,它会扫描包下的所有候选类/接口
// 并生成一个 beanClass 为接口本身的 BeanDefinitionHolder 集合。
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
logger.warn("No MyBatis mapper was found in ...");
} else {
// 2. 【偷梁换柱】遍历所有扫描到的BeanDefinition
for (BeanDefinitionHolder holder : beanDefinitions) {
GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();
// ... 省略部分日志 ...
// 【核心】将 BeanDefinition 的 beanClass 从 Mapper接口 替换为 MapperFactoryBean.class
// 定义构造函数参数的值,即 Mapper 接口本身
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
// 执行替换!
definition.setBeanClassName(this.mapperFactoryBean.getClass().getName());
// 3. 【自动装配模式】设置按类型注入,这样 SqlSessionFactory 或 SqlSessionTemplate 就能被自动注入
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
}
return beanDefinitions;
}
}
逐段解读:
- 复用 Spring 扫描能力:
super.doScan(basePackages)这行代码非常关键。它直接复用了 Spring 强大的类路径扫描和资源解析能力,可以自动找到包下所有符合条件的接口(默认是标记了@Mapper或有自定义注解的接口),并生成初步的BeanDefinition,此时beanClass还是com.xxx.UserMapper这样的接口全限定名。 - 核心替换逻辑:这是整个 MyBatis-Spring 整合中最具“魔术性”的一步。
- 保存原始接口:
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName())这行代码先将原始的com.xxx.UserMapper接口全限定名作为构造函数参数的值保存起来。这为后面MapperFactoryBean实例化时,通过构造器注入mapperInterface做好了准备。 - 替换 beanClass:
definition.setBeanClassName(this.mapperFactoryBean.getClass().getName())将beanClass从接口类型替换为org.mybatis.spring.mapper.MapperFactoryBean。这样一来,当 Spring 去创建这个 Bean 的实例时,它将不再尝试去实例化一个无法实例化的接口,而是去实例化一个实实在在的MapperFactoryBean。
- 保存原始接口:
- 设置自动装配:
AUTOWIRE_BY_TYPE意味着当MapperFactoryBean实例化后,Spring 会自动根据其setter方法的参数类型,从容器中查找并注入匹配的 Bean(如SqlSessionFactory或SqlSessionTemplate)。
用 Spring 知识解释:
这一步完美展现了 BeanDefinitionRegistry 和 BeanFactoryPostProcessor 的威力。ClassPathMapperScanner 在“运行时”动态地修改了 Bean 的定义(BeanDefinition),将“生产指令”从“制造一个 UserMapper”改成了“制造一个 MapperFactoryBean,并将 UserMapper 作为参数传入”。整个过程发生在 Bean 实例化之前,对后续的用户完全透明。这就是 Spring 留给开发者修改容器蓝图的最直接路径。
4. 双重代理创建:MapperFactoryBean、SqlSessionTemplate 与 MapperProxy 的协作
当 MapperFactoryBean 的 BeanDefinition 注册成功后,Spring 容器就将按照这个“新蓝图”来创建 Bean。而 MapperFactoryBean 则承担起创建最终 Mapper 代理对象的“双重代理”中的第一重。
4.1 第一重代理:MapperFactoryBean
MapperFactoryBean 同时实现了 FactoryBean<T> 和 InitializingBean 接口,这是一个经典的 Spring 整合模式。
// 类全限定名: org.mybatis.spring.mapper.MapperFactoryBean<T>
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T>, InitializingBean {
private Class<T> mapperInterface;
public MapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
// 【Spring扩展点】InitializingBean接口实现,在所有属性设置完成后调用,用于校验
@Override
public final void afterPropertiesSet() throws Exception {
// 校验关键属性,确保 SqlSessionFactory 或 SqlSessionTemplate 已经被注入
super.afterPropertiesSet();
// 校验 Mapper 接口是否为 null
if (this.mapperInterface == null) {
throw new IllegalArgumentException("Property 'mapperInterface' is required");
}
// 将 Mapper 接口添加到 MyBatis Configuration 的映射中,使其被 MyBatis 识别
getConfiguration().addMapper(this.mapperInterface);
}
// 【Spring扩展点】FactoryBean接口实现,返回真正的Bean实例
@Override
public T getObject() throws Exception {
// 核心:从SqlSession中获取Mapper接口的代理对象
return getSqlSession().getMapper(this.mapperInterface);
}
// 返回Bean的类型,即Mapper接口类型
@Override
public Class<T> getObjectType() {
return this.mapperInterface;
}
// 是否单例,通常Mapper代理应该是单例的
@Override
public boolean isSingleton() {
return true;
}
}
逐段解读:
afterPropertiesSet()(InitializingBean):在 Bean 的属性被注入后,afterPropertiesSet被调用。它做了两件事:一是通过父类方法校验SqlSessionFactory或SqlSessionTemplate不为空;二是调用getConfiguration().addMapper(this.mapperInterface),这一步非常关键,它告诉 MyBatis 的Configuration对象,要开始处理这个 Mapper 接口,解析其 XML 映射文件或注解上的 SQL,为后续的方法调用做准备。getObject()(FactoryBean):这是本模块的核心。它直接调用了getSqlSession().getMapper(this.mapperInterface)。getSqlSession()会返回其持有的SqlSessionTemplate实例。这行代码返回的就是 Mapper 接口的 JDK 动态代理对象(即MapperProxy)。从此,Spring 容器中以userMapper为名的 Bean,实际上就是这个代理对象。
图示:MapperFactoryBean 的创建与调用序列
sequenceDiagram
participant Container as Spring IoC Container
participant MFB as MapperFactoryBean
participant ST as SqlSessionTemplate
participant SF as SqlSessionFactory
participant MP as MapperProxy
Container->>MFB: 1. 实例化 (通过构造器注入mapperInterface)
Container->>MFB: 2. 自动注入(AUTOWIRE_BY_TYPE) SqlSessionFactory/SqlSessionTemplate
Container->>MFB: 3. 调用 afterPropertiesSet()
MFB->>MFB: 校验属性
MFB->>SF: getConfiguration().addMapper(mapperInterface)
SF-->>MFB: Mapper接口注册成功
Container->>MFB: 4. 调用 getObject() 获取Bean实例
MFB->>ST: getMapper(mapperInterface)
ST->>SF: getConfiguration().getMapper(mapperInterface, this)
SF-->>MP: 创建 MapperProxy 实例
ST-->>MFB: 返回 MapperProxy 代理对象
MFB-->>Container: 返回 MapperProxy 代理对象
Note over Container: 容器持有MapperProxy,并注入给其他Bean
图表主旨概括:该图展示了 Spring 容器如何与 MapperFactoryBean 交互,最终获取到 Mapper 代理对象。
逐层/逐元素分解:
- Spring IoC Container: 作为整个流程的驱动者,它严格遵循 Bean 的生命周期,调用
InitializingBean.afterPropertiesSet()和FactoryBean.getObject()这两个Spring扩展点。 - MapperFactoryBean: 作为
FactoryBean,它的职责是隐藏复杂的创建逻辑,将MapperProxy提供给容器。 - SqlSessionTemplate/SqlSessionFactory:
MapperFactoryBean内部持有SqlSessionTemplate,并通过它获取Configuration,最终委托MapperProxy完成代理创建。
设计原理映射:这正是“工厂方法模式”在 Spring 容器级别的应用。MapperFactoryBean 是具体工厂,MapperProxy 是产品。FactoryBean 接口就是这个设计模式在 Spring 中的契约。
工程联系与关键结论:在 Service 中通过 @Autowired 注入的 Mapper 接口实例,其真实身份就是这里的 MapperProxy 动态代理对象。理解 FactoryBean 是理解 MyBatis 与 Spring 整合的关键一步。
4.2 深入 MapperProxy:JDK 动态代理的经典案例
现在我们进入了最核心的地带。getObject() 返回的 MapperProxy 对象,是如何将 userMapper.getUserById(1) 这样的调用,最终变成 select * from user where id = 1 的呢?
MapperProxy 是 JDK 动态代理中的 InvocationHandler。
// 类全限定名: org.apache.ibatis.binding.MapperProxy<T>
public class MapperProxy<T> implements InvocationHandler, Serializable {
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
// 方法缓存,用于加速,key是Method,value是MapperMethodInvoker
private final Map<Method, MapperMethodInvoker> methodCache;
// ...
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 如果调用的是 Object 类中的方法(如 toString, hashCode),直接调用
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 2. 如果是默认方法(Java 8+),则用专门的处理方式
if (method.isDefault()) {
return invokeDefaultMethod(proxy, method, args);
}
// 3. 【核心】获取缓存中的 MapperMethodInvoker 并执行
// 首次访问时会创建一个 PlainMethodInvoker 并放入缓存
final MapperMethodInvoker invoker = cachedInvoker(method);
return invoker.invoke(proxy, method, args, sqlSession);
}
}
// 类全限定名: org.apache.ibatis.binding.MapperProxy.PlainMethodInvoker
private static class PlainMethodInvoker implements MapperMethodInvoker {
private final MapperMethod mapperMethod;
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
// 直接委托给 MapperMethod
return mapperMethod.execute(sqlSession, args);
}
}
逐段解读(MapperProxy.invoke):
- 过滤非业务方法:
Object类的方法和default方法是不会被 MyBatis 拦截处理的,保证了代理对象的正常运行。 - 方法调用分发:通过
cachedInvoker获取一个MapperMethodInvoker。这是一种缓存机制,避免每次都解析Method。PlainMethodInvoker是默认的实现,它内部封装了一个MapperMethod对象。 MapperMethod.execute:这是将 Java 方法调用转换为 JDBC 操作的关键。
// 类全限定名: org.apache.ibatis.binding.MapperMethod
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
// 根据 SQL 命令类型(INSERT, UPDATE, DELETE, SELECT, FLUSH)进行分发
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case SELECT: {
// 根据方法返回类型决定调用 selectOne, selectList, selectMap etc.
Object param = method.convertArgsToSqlCommandParam(args);
if (method.returnsVoid() || method.getResultHandler() != null) {
// ...
} else if (method.returnsMany()) {
result = sqlSession.selectList(command.getName(), param);
} else if (method.returnsMap()) {
result = sqlSession.selectMap(command.getName(), param, method.getMapKey());
} else {
result = sqlSession.selectOne(command.getName(), param);
}
break;
}
// ... 其他命令类型
}
return result;
}
}
逐段解读(MapperMethod.execute):
SqlCommand:封装了 SQL 语句的 ID(即命名空间+方法名,如com.xxx.UserMapper.getUserById)和 SQL 命令类型(SELECT,INSERT等)。MethodSignature:封装了 Mapper 接口方法的签名信息,如返回值类型、参数等。- 命令分发:
execute方法是一个典型的命令模式应用。它根据SqlCommand的类型,将调用委托给SqlSession的对应方法(selectOne,insert,update,delete等)。它还会通过method.convertArgsToSqlCommandParam(args)将传入的多个参数转换成 MyBatis 需要的格式(对应#{param1, param2}或@Param注解)。
图示:MapperProxy 内部调用时序
sequenceDiagram
participant Service as UserService
participant MP as MapperProxy<br/>(InvocationHandler)
participant MM as MapperMethod
participant SS as SqlSession
Service->>MP: userMapper.getUserById(1)
MP->>MP: 缓存:根据Method获取MapperMethodInvoker
MP->>MM: PlainMethodInvoker.invoke()
MM->>MM: 解析方法签名,根据命令类型(SELECT)分发
alt 命令类型为 SELECT
MM->>SS: sqlSession.selectOne("com.xxx.UserMapper.getUserById", 1)
else 命令类型为 INSERT
MM->>SS: sqlSession.insert("com.xxx.UserMapper.insertUser", user)
end
SS-->>MM: 返回结果
MM-->>MP: 返回结果
MP-->>Service: 返回最终结果
图表主旨概括:此图详细刻画了 Mapper 接口方法调用,在 MapperProxy 内部是如何一步步被解析并最终委托给 SqlSession 执行的。
逐层/逐元素分解:
- MapperProxy: 扮演
InvocationHandler角色,是整个调用的入口和调度中心。 - MapperMethod: 扮演命令模式中的命令角色,封装了执行 SQL 所需的全部信息(命令名称、类型)和方法签名。它屏蔽了不同 SQL 命令的执行差异。
- SqlSession: 扮演执行者角色,它不再关心是谁调用了它,只负责执行具体的增删改查操作。
设计原理映射:这里综合运用了JDK 动态代理、命令模式和缓存。通过代理实现接口的无侵入调用拦截,通过命令模式将方法调用与 SQL 执行解耦,通过缓存提升性能。
工程联系与关键结论:我们写的每一行 mapper.insert() 或 mapper.selectOne(),都会被 MapperProxy 拦截,然后由 MapperMethod 精准地翻译成 sqlSession.insert("namespace.id", param)。这个翻译过程,正是 MyBatis 的核心功能之一。
4.3 第二重代理:SqlSessionTemplate 的线程安全机制
MapperProxy 将调用委托给了 SqlSession,那这个 SqlSession 是什么?在 MyBatis-Spring 中,它并不是 MyBatis 原生的 DefaultSqlSession,因为后者不是线程安全的。取而代之的是 SqlSessionTemplate,它是线程安全的,这构成了“双重代理”中的第二重。
SqlSessionTemplate 实现了 SqlSession 接口,但其内部所有方法都委托给了一个通过 JDK 动态代理创建的 SqlSession 代理对象。
// 类全限定名: org.mybatis.spring.SqlSessionTemplate
public class SqlSessionTemplate implements SqlSession, DisposableBean {
private final SqlSessionFactory sqlSessionFactory;
private final SqlSession sqlSessionProxy; // 内部的代理
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
// ...
// 创建 SqlSession 的动态代理,SqlSessionInterceptor 是 InvocationHandler
this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
SqlSession.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
}
// 所有 SqlSession 接口方法都委托给内部代理
@Override
public <T> T selectOne(String statement) {
return this.sqlSessionProxy.selectOne(statement);
}
// ... 其他方法类似
// 【核心】内部代理的 InvocationHandler
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 【Spring事务同步】尝试从事务管理器获取当前线程绑定的SqlSession
SqlSession sqlSession = (SqlSession) TransactionSynchronizationManager.getResource(sqlSessionFactory);
if (sqlSession != null) {
// 2. 如果当前有事务,直接复用
return method.invoke(sqlSession, args);
} else {
// 3. 如果没有事务,创建一个新的 SqlSession,用后即关闭
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.SIMPLE, autoCommit)) {
return method.invoke(session, args);
}
}
}
}
}
逐段解读:
getResource(sqlSessionFactory)(TransactionSynchronizationManager):这是 Spring 事务同步的核心。它尝试从当前线程的绑定资源中,获取以sqlSessionFactory为 Key 的SqlSession。如果 Spring 事务管理器开启了一个事务,它就会将一个SqlSession对象绑定到这里。- 存在事务时(
sqlSession != null):直接复用这个已存在的SqlSession。这保证了当前操作能参与到 Spring 管理的事务中,并能利用该SqlSession的一级缓存。 - 不存在事务时(
sqlSession == null):创建一个全新的SqlSession,执行完方法后立即关闭(try-with-resources)。这符合每次数据库操作都应该是无状态、独立的原则。
用 Spring 知识解释:
SqlSessionTemplate 的设计是**TransactionSynchronizationManager + JDK 动态代理**的完美结合。TransactionSynchronizationManager 作为线程级别的资源管理器,提供了事务上下文感知能力;而动态代理则提供了一种非侵入式的拦截方式,将事务感知逻辑透明地织入到 SqlSession 的每个方法调用中。这保证了在非事务环境下每个 SqlSession 请求都是隔离的,而在事务环境下它们又能自动关联,从而保证了线程安全与事务一致性。
5. 事务同步与一级缓存:MyBatis 如何融入 Spring 事务管理
通过上一节,我们已经看到 SqlSessionTemplate 如何利用 TransactionSynchronizationManager 获取 SqlSession。那么,这个 SqlSession 最初是如何被绑定上去的呢?这就要看 Spring 的 DataSourceTransactionManager。
5.1 Spring 事务管理与 MyBatis 的连接复用
当我们在 Service 方法上标注 @Transactional 并指定了 DataSourceTransactionManager 时,事务管理器的执行流程如下:
- 开启事务:
DataSourceTransactionManager从DataSource中获取一个Connection,并设置autoCommit=false。 - 绑定资源:调用
TransactionSynchronizationManager.bindResource(this.dataSource, connection),将Connection绑定到当前线程。 - 同步
SqlSession:MyBatis 的SqlSessionFactoryBean或SqlSessionTemplate在哪里绑定呢?实际上,SqlSessionTemplate的SqlSessionInterceptor在首次发现事务存在但还没有绑定SqlSession时,会创建一个新的SqlSession,并把它绑定到sqlSessionFactory上。关键在于// 伪代码描述 SqlSessionInterceptor 中的逻辑 SqlSession sqlSession = TransactionSynchronizationManager.getResource(sqlSessionFactory); if (sqlSession == null) { // 从 DataSourceTransactionManager 管理的 DataSource 中获取 Connection // 这个 Connection 已经是事务管理器的那个 Connection Connection conn = DataSourceUtils.getConnection(this.dataSource); // 用这个 Connection 创建 SqlSession sqlSession = sqlSessionFactory.openSession(conn); // 将创建好的 SqlSession 绑定到当前线程 TransactionSynchronizationManager.bindResource(this.sqlSessionFactory, sqlSession); } return method.invoke(sqlSession, args);DataSourceUtils.getConnection(this.dataSource)这行代码。它并不会创建一个新的连接,而是会检查TransactionSynchronizationManager中是否已经绑定了同DataSource的连接。由于DataSourceTransactionManager已经事先绑定了,DataSourceUtils就会直接返回那个被事务管理的同一个Connection。这样,MyBatis 的SqlSession就自然而然地运行在 Spring 事务的连接之上。
5.2 传播行为对一级缓存的影响
MyBatis 的一级缓存是 SqlSession 级别的,其生命周期与 SqlSession 绑定。由于 Spring 管理了 SqlSession 的生命周期,所以一级缓存的生命周期就由 Spring 事务边界决定。这在不同的传播行为下有不同的表现。
-
REQUIRED(默认)传播行为- 外部方法调用
userService.methodA(),Spring 开启事务,绑定SqlSession1。 methodA内部调用userMapper.getUserById(1),命中SqlSession1,查询结果放入SqlSession1的一级缓存。methodA内部再次调用userMapper.getUserById(1),SqlSessionInterceptor发现事务中已有SqlSession1,直接复用。此次查询命中SqlSession1的一级缓存。- 事务提交,
SqlSession1关闭,缓存失效。
- 结论:在同一个事务内,MyBatis 一级缓存正常工作,可以有效减少数据库查询。
- 外部方法调用
-
REQUIRES_NEW传播行为- 外部方法调用
userService.methodA()(PROPAGATION_REQUIRED),Spring 开启事务Tx1,绑定SqlSession1和Connection1。 methodA调用userService.methodB()(PROPAGATION_REQUIRES_NEW)。- Spring 挂起
Tx1:TransactionSynchronizationManager.unbindResource(dataSource)解除Connection1的绑定。 - Spring 创建并开启新事务
Tx2,获取Connection2,并绑定到线程。 methodB内部调用userMapper.getUserById(1)。SqlSessionInterceptor检查到没有绑定的SqlSession(因为Tx1的SqlSession1可能也已被挂起移除),于是根据Connection2创建一个新的SqlSession2并绑定。- 查询结果放入
SqlSession2的一级缓存。 methodB结束,Tx2提交,SqlSession2关闭,缓存失效。Tx1恢复:Spring 将Connection1重新绑定。methodA再次调用userMapper.getUserById(1)。SqlSessionInterceptor发现Tx1中绑定的SqlSession1(或重新创建绑定),复用SqlSession1。它无法命中先前在methodB中SqlSession2里的一级缓存,会再次发送 SQL 查询。
- 结论:
REQUIRES_NEW会挂起并创建新的事务和SqlSession,导致一级缓存在不同传播行为间被物理隔离,无法共享。 如果程序逻辑高度依赖一级缓存来避免重复查询,而数据库在高并发下数据又会变化,可能导致奇怪的“同一个事务里两次查询结果不同”的现象。
- 外部方法调用
用 Spring 知识解释:
MyBatis 通过将事务和资源管理完全托管给 Spring,自身变得非常轻量。DataSourceUtils 是连接复用的桥梁,TransactionSynchronizationManager 是资源绑定的核心。MyBatis 一级缓存的“不可预测”行为,本质上是 Spring 事务所管理资源的生命周期变化。掌握 TransactionSynchronizationManager 的状态流转,就能完全掌控一级缓存的边界。
6. 多数据源支持:@MapperScan 高级属性的原理
在现代应用中,读写分离或多数据源场景十分常见。@MapperScan 通过 sqlSessionTemplateRef 和 sqlSessionFactoryRef 属性提供了优雅的支持。
6.1 属性与注入原理
回顾 @MapperScan 注解的属性:
sqlSessionFactoryRef(String):指定要使用的SqlSessionFactory的 Bean 名称。sqlSessionTemplateRef(String):指定要使用的SqlSessionTemplate的 Bean 名称。
这两个属性会通过 MapperScannerRegistrar 传递给 ClassPathMapperScanner,并最终设置到为该包下所有 Mapper 接口创建的 MapperFactoryBean 的 BeanDefinition 中。
在 MapperFactoryBean 内部,其父类 SqlSessionDaoSupport 提供了 setSqlSessionFactory 和 setSqlSessionTemplate 方法。
// 类全限定名: org.mybatis.spring.support.SqlSessionDaoSupport
public abstract class SqlSessionDaoSupport extends DaoSupport {
// ...
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
}
}
public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
this.sqlSessionTemplate = sqlSessionTemplate;
}
// ...
}
当我们在配置类中为不同的包指定不同的 sqlSessionFactoryRef 时,本质上是在告诉 Spring:“请为这个包下的 MapperFactoryBean 注入那个特定名字的 SqlSessionFactory”。
图示:多数据源场景下的绑定原理
flowchart LR
subgraph Configuration [Spring 配置]
A[@MapperScan<br/>basePackages=com.xxx.order<br/>sqlSessionFactoryRef=orderSF]
B[@MapperScan<br/>basePackages=com.xxx.user<br/>sqlSessionFactoryRef=userSF]
end
subgraph Container [Spring 容器]
C[orderMapperFactoryBean]
D[userMapperFactoryBean]
E[orderSqlSessionFactory<br/>(name=orderSF)]
F[userSqlSessionFactory<br/>(name=userSF)]
G[(OrderDB)]
H[(UserDB)]
end
A --> C
B --> D
C -- AUTOWIRE_BY_TYPE/按名注入 --> E
D -- AUTOWIRE_BY_TYPE/按名注入 --> F
E --> G
F --> H
style C fill:#fff3e0,stroke:#e65100
style D fill:#e8f5e9,stroke:#1b5e20
图表主旨概括:本图清晰地表达了一个 Spring 应用中,不同业务模块的 Mapper 通过配置不同的 sqlSessionFactoryRef,是如何连接到不同数据库的。
逐层/逐元素分解:
@MapperScan配置层:通过sqlSessionFactoryRef属性显式指定要使用的SqlSessionFactoryBean 名称。- Spring 容器层:
orderMapperFactoryBean和userMapperFactoryBean虽然都是MapperFactoryBean类型,但它们在注入SqlSessionFactory时,Spring 的自动装配机制会结合@Qualifier或 Bean 名称,找到对应的orderSF和userSF。 - 数据源层:不同的
SqlSessionFactory连接不同的数据源,从而实现了数据层面的完全隔离。
设计原理映射:这是利用 Spring IoC 的依赖注入和 Bean 生命周期管理能力,实现了对多数据源的优雅支持。它不需要修改任何一行 MyBatis 或 Spring 的源码,完全是声明式的。
工程联系与关键结论:通过在不同的 @MapperScan 注解中精确指定 sqlSessionFactoryRef,可以实现不同包的 Mapper 操作不同数据源,这是实现读写分离或垂直分库的常用基础技术。这正是 Spring 依赖注入灵活性的体现。
7. 整体流程串联:一张完整的整合时序图与速查表
通过前面的剖析,我们已经逐个击破了各个关键环节。现在,让我们用一张完整的时序图,将所有步骤串联起来。
7.1 完整整合时序图
sequenceDiagram
participant Dev as 开发者
participant AC as AnnotationConfig<br/>ApplicationContext
participant Reg as MapperScanner<br/>Registrar
participant Scan as ClassPath<br/>MapperScanner
participant Regs as BeanDefinition<br/>Registry
participant MFB as MapperFactoryBean
participant ST as SqlSessionTemplate
participant MP as MapperProxy
participant SC as Service层
Dev->>AC: new AnnotationConfigApplicationContext(AppConfig.class)
AC->>AC: 内部处理 AppConfig
AC->>AC: 解析 @MapperScan 上的 @Import
AC->>Reg: 实例化 MapperScannerRegistrar
AC->>Reg: 调用 registerBeanDefinitions(metadata, registry)
Reg->>Scan: 创建 ClassPathMapperScanner(registry)
Reg->>Scan: 设置扫描属性 (注解类, 工厂类, sqlSessionFactoryRef等)
Reg->>Scan: 调用 doScan(basePackages)
Scan->>Scan: 调用 super.doScan() 找到 UserMapper 等接口
Scan->>Regs: 修改 BeanDefinition: beanClass -> MapperFactoryBean
Scan->>Regs: 添加构造参数: UserMapper.class
Scan-->>Reg: 返回 BeanDefinitionHolder 集合
Reg-->>AC: 注册完成
Note over AC: ... 容器刷新,开始实例化 Bean ...
AC->>MFB: 获取并实例化 UserMapper 的 BeanDefinition
AC->>MFB: 注入 SqlSessionTemplate/SqlSessionFactory
AC->>MFB: 调用 afterPropertiesSet() 校验
AC->>MFB: 调用 getObject()
MFB->>ST: getMapper(UserMapper.class)
ST->>MP: new MapperProxy(...)
MP-->>MFB: 返回 MapperProxy 代理对象
MFB-->>AC: 返回 MapperProxy 代理对象
AC->>SC: 从容器获取 UserService
SC->>SC: @Autowired UserMapper (注入 MapperProxy)
SC->>MP: 调用 userMapper.getUser(1)
MP->>ST: selectOne("namespace.getUser", 1)
ST->>ST: SqlSessionInterceptor 检查事务
ST->>ST: 获取或创建 SqlSession
ST->>ST: SqlSession.selectOne(...) 执行 SQL
ST-->>MP: 返回结果
MP-->>SC: 返回结果
图表主旨概括:此图合并了前文的多个序列图,全景式地展示了从 @MapperScan 到最终执行业务方法的完整过程,清晰地展现了 MyBatis 如何融入 Spring 的生命周期。
逐层/逐元素分解:
- 容器启动注册阶段:由
@Import触发,MapperScannerRegistrar和ClassPathMapperScanner合作,通过BeanDefinitionRegistry动态改变 Bean 的“生产蓝图”。 - Bean 实例化阶段:容器根据修改后的“蓝图”创建
MapperFactoryBean,并利用InitializingBean和FactoryBean扩展点,完成校验和代理对象MapperProxy的创建。 - 运行时调用阶段:业务代码调用
MapperProxy,MapperProxy将请求委托给SqlSessionTemplate。SqlSessionTemplate的内部代理则利用TransactionSynchronizationManager保证线程安全和事务同步。
设计原理映射:整个过程严格遵循了 Spring 的“开放-闭合”原则。Spring 核心只负责管理扩展点接口的调用时机和顺序,而 MyBatis-Spring 只需提供这些扩展点的具体实现即可。
工程联系与关键结论:从 @MapperScan 到最终的 SQL 执行,中间没有任何“黑魔法”,而是一个精心编排的、基于 Spring 扩展点接口的协作流程。理解这个时序图,就理解了 MyBatis-Spring 整合的 90%。
7.2 流程步骤速查表
| 步骤 | 涉及组件 | 核心 Spring 扩展点 | 关键 MyBatis 实现类 |
|---|---|---|---|
| 1. 触发注册 | @MapperScan 注解 | @Import | MapperScannerRegistrar |
| 2. 动态修改定义 | ClassPathMapperScanner | BeanDefinitionRegistry, BeanDefinition | --- |
| 3. 实例化校验 | MapperFactoryBean | InitializingBean | --- |
| 4. 创建代理对象 | MapperFactoryBean | FactoryBean | SqlSessionTemplate, MapperProxy |
| 5. 线程安全&事务 | SqlSessionTemplate | TransactionSynchronizationManager, JDK 动态代理 | SqlSessionInterceptor |
| 6. 调用的转化 | MapperProxy | InvocationHandler | MapperMethod, MapperMethodInvoker |
| 7. 多数据源绑定 | MapperFactoryBean | 依赖注入 (@Autowired, @Qualifier) | SqlSessionDaoSupport |
8. 整合设计反思:为什么说这是扩展点的最佳实践
MyBatis-Spring 的整合,无疑是 Spring 扩展点体系下的一个教科书级案例。它不仅解决了一个具体的集成问题,更向我们展示了一种优雅的框架整合范式。
-
MyBatis-Spring 使用的扩展点组合:
@Import+ImportBeanDefinitionRegistrar:负责从注解到 Bean 定义的链接和注册。FactoryBean+InitializingBean:负责复杂对象创建和生命周期校验。TransactionSynchronizationManager+ JDK 动态代理:负责非侵入式的线程安全与事务同步。
-
横向对比:
- Spring Cloud OpenFeign:定义了
@FeignClient注解,通过@Import(FeignClientsRegistrar.class)导入一个ImportBeanDefinitionRegistrar实现,该实现扫描并注册FeignClientFactoryBean,其getObject()方法同样返回一个 JDK 动态代理。代理内部负责将方法调用转为 HTTP 请求。模式如出一辙。 - Spring Data Redis:其核心 API
RedisTemplate本身就是一个线程安全的模板类。它的Connection管理和事务支持也深度集成了 Spring 的事务同步基础设置,Redis 的@Transactional支持原理和 MyBatis 类似。 - 提炼通用模式:声明式注解 ->
@Import(Registrar)->Registrar注册FactoryBean->FactoryBean创建代理 -> 代理内部集成模板类 -> 模板类利用TransactionSynchronizationManager管理资源同步。
- Spring Cloud OpenFeign:定义了
-
与 Spring Boot 自动配置的对比:
- 在纯 Spring 中,用户的显式配置(
AppConfig)是整合的触发点。 - 在 Spring Boot 中,
MybatisAutoConfiguration读取配置,自动创建SqlSessionFactory等 Bean,并通过@MapperScan或MapperScannerConfigurer完成自动注册。 - 核心原理完全一致。Spring Boot 的自动配置只是将这些配置步骤自动化、约定化,但底层使用的扩展点(
FactoryBean,Registrar等)是一样的。
- 在纯 Spring 中,用户的显式配置(
-
思考与启示: 如果我们自己要从零设计一个类似 MyBatis 的、需要与 Spring 深度集成的框架,我们也可以遵循同样的“配方”:
- 设计一个或多个自定义注解,用于标记和配置。
- 通过
@Import导入我们的ImportBeanDefinitionRegistrar。 - 在
Registrar中,找到所有候选接口,为每个接口注册一个FactoryBean的BeanDefinition。 - 我们的
FactoryBean实现getObject方法,返回一个基于 JDK 或 CGLIB 的代理,这个代理负责核心逻辑。 - 封装一个内部模板类(类似
SqlSessionTemplate),管理客户端连接的生命周期、线程安全和事务同步。
9. 生产事故排查专题
案例 1:@MapperScan 包路径写错,启动不报错但无法注入 Mapper
- 现象:项目启动成功,无任何异常日志。但当访问一个尝试注入
UserMapper的 Controller 时,抛出NoSuchBeanDefinitionException。 - 排查:检查配置类,
@MapperScan("com.xxx.user.mappper")发现mapper多打了一个p。 - 根因(结合整合原理):
@MapperScan背后的ClassPathMapperScanner在扫描时找不到任何匹配的资源,因此没有向BeanDefinitionRegistry注册任何MapperFactoryBean的BeanDefinition。Spring 容器会正常启动,但容器中自然就没有这个 Bean。doScan方法中的if (beanDefinitions.isEmpty())仅打印一个warn日志,在多日志项目中极易被忽略。 - 解决:修正
@MapperScan的basePackages属性为正确的包路径。 - 最佳实践:
- 仔细检查
@MapperScan的包路径,精确到具体包,不要过度使用宽泛的包名以免扫描过多不必要类,也避免写错时范围太大不易排查。 - 在开发环境,将
ClassPathMapperScanner的日志级别调整为DEBUG,以便在启动时就看到它扫描了哪些包。 - 可以为
MapperFactoryBean设置lazy-init=false(默认即是),这样任何 Mapper 找不到的问题在启动时就会暴露。
- 仔细检查
案例 2:Spring 事务不生效导致一级缓存幻觉
- 现象:开发者在一个 Service 方法
methodA上标注了@Transactional,方法内两次调用userMapper.getUserById(1)。第一次查询有 SQL 日志,第二次查询没有,开发者认为缓存生效。但在另一个未加事务的methodB中,用同一个条件查询,依然没有 SQL 日志。这导致数据更新后,methodB读到的是旧数据。 - 排查:排查
methodA的事务配置,发现其类上使用了AOP限制,导致@Transactional注解未生效。两次调用实际上都是在非事务环境下进行的。 - 根因(结合整合原理):如果
methodA的事务未生效,两次userMapper.getUserById(1)的调用,在SqlSessionTemplate的SqlSessionInterceptor看来都是无事务调用,会各自创建新的SqlSession并立即关闭。第二次查询应该依旧有 SQL 日志才对。为何第一次之后没有?大概率是二级缓存开启了且命中。如果二级缓存都关闭,那可能是单元测试或日志查看的偏差。 - 再分析:如果是一个真正生效的
@Transactional(propagation=Propagation.REQUIRES_NEW),如methodB调用methodA,且methodA使用了REQUIRES_NEW。那么在methodA中会创建新的SqlSession,一级缓存位于该新SqlSession中,对外部的methodB是不可见的。当methodA返回后,其SqlSession已关闭,一级缓存失效。外部的methodB再进行同样查询,会再次访问数据库。 - 解决:
- 检查并使
@Transactional生效(正确引入 AOP、配置正确的TransactionManager等)。 - 充分理解
PROPAGATION_REQUIRES_NEW会隔离事务和一级缓存。
- 检查并使
- 最佳实践:
- 避免业务逻辑过度依赖一级缓存。它只是一个
SqlSession级别的“鸡肋”缓存。 - 在跨
REQUIRES_NEW的场景下,如果确实需要共享查询结果,考虑使用二级缓存或显式在更高层传递数据对象。 - 关键业务流,可以在核心查询上打印 MyBatis SQL 日志,真实地了解缓存命中情况。
- 避免业务逻辑过度依赖一级缓存。它只是一个
10. 面试高频专题
1. Spring 和 MyBatis 是如何整合的?核心扩展点有哪些?
- 回答:整合核心是利用 Spring 的扩展点体系将 MyBatis 的组件无缝挂载到容器生命周期中。过程分三步:1)
@MapperScan通过@Import导入MapperScannerRegistrar;2)该Registrar扫描接口,并用MapperFactoryBean替换接口的BeanDefinition;3)MapperFactoryBean通过FactoryBean.getObject()返回 Mapper 的 JDK 代理。核心扩展点包括@Import/ImportBeanDefinitionRegistrar,FactoryBean/InitializingBean,TransactionSynchronizationManager。 - 追问与加分:
- 追问1:
@Mapper注解和@MapperScan的区别?答:@Mapper是一个标记注解,无法单独工作。@MapperScan是利用@Import注册了扫描器,扫描器会扫描带有@Mapper(或自定义)注解的接口。 - 追问2:
ImportBeanDefinitionRegistrar比BeanFactoryPostProcessor优势在哪?答:Registrar更早期,它接收AnnotationMetadata,可直接获取导入它的注解的元数据,逻辑更内聚。 - 追问3: 如果一个接口没加
@Mapper也没在任何@MapperScan的包下,能当 Mapper 用吗?答:不能,因为容器中没有对应的MapperFactoryBean的BeanDefinition。
- 追问1:
2. @MapperScan 注解是如何工作的?背后使用了哪些 Spring 扩展接口?
- 回答:
@MapperScan是一个合成注解,其内部标记了@Import(MapperScannerRegistrar.class)。其工作流程:IoC启动时处理@Import,实例化MapperScannerRegistrar并调用其registerBeanDefinitions方法。此方法会解析@MapperScan的各种属性(如包路径、sqlSessionFactoryRef),委托ClassPathMapperScanner去具体扫描和注册。使用了@Import,ImportBeanDefinitionRegistrar,BeanDefinitionRegistry。 - 追问与加分:
- 追问1:
ClassPathMapperScanner为什么能修改BeanDefinition?答:因为父类扫描接口后返回的是BeanDefinitionHolder,它在post-process这些定义的阶段,可以用setBeanClassName修改其beanClass,这是完全合法且常用的做法。 - 追问2:
sqlSessionFactoryRef是如何生效的?答:该值被传入MapperFactoryBean的BeanDefinition中,最终在依赖注入时,Spring结合@Qualifier按名称查找对应SqlSessionFactory。 - 追问3: 如果不使用
@MapperScan,如何用 XML 实现类似功能?答:可以用MapperScannerConfigurer,它本质上就是MapperScannerRegistrar的 XML 版本实现,实现了BeanFactoryPostProcessor。
- 追问1:
3. MapperFactoryBean 的作用是什么?它是如何创建 Mapper 代理的?
- 回答:它是一个
FactoryBean,是“生产” Mapper 代理对象的工厂。当 Spring 需要获取一个userMapperBean时,会调用MapperFactoryBean.getObject()。该方法内部通过getSqlSession().getMapper(mapperInterface)获取 Mapper 接口的 JDK 动态代理对象,并返回。 - 追问与加分:
- 追问1:
getSqlSession().getMapper()内部发生了什么?答:最终会走到Configuration.getMapper(),它会为接口创建一个MapperProxy(JDK 的InvocationHandler),并通过Proxy.newProxyInstance创建代理对象。 - 追问2:
afterPropertiesSet()有什么用?答:它实现了InitializingBean,用于在属性注入后执行初始化检查,确保SqlSessionTemplate等关键依赖不为空,并将 Mapper 接口注册到 MyBatisConfiguration中。 - 追问3: 为什么不直接把
MapperProxy注册为一个单例 Bean,而要用FactoryBean包装一层?答:因为创建 MapperProxy 需要mapperInterface、sqlSession等上下文信息,这些信息对每个 Mapper 都不同。FactoryBean提供了封装这种“一个Bean对应一个工厂”的创建逻辑的最佳方式。
- 追问1:
4. SqlSessionTemplate 为什么是线程安全的?它如何与 Spring 事务同步?
- 回答:MyBatis 原生的
DefaultSqlSession是线程不安全的。SqlSessionTemplate通过内部的一个动态代理(SqlSessionInterceptor)实现了线程安全。每次方法调用,SqlSessionInterceptor都会通过TransactionSynchronizationManager.getResource(SqlSessionFactory)查找当前线程绑定的SqlSession。如果在 Spring 事务中,则复用该SqlSession;否则新建一个并在方法后关闭。这保证了同一事务内共享,非事务下隔离,是线程安全的。 - 追问与加分:
- 追问1:
SqlSession最初是谁绑定到TransactionSynchronizationManager的?答:是SqlSessionInterceptor自己。当它第一次在活动事务中调用时,发现没有绑定,就会通过DataSourceUtils获取事务连接,创建一个SqlSession并绑定上去。 - 追问2: 为什么非事务场景要“用后即关”?答:防止连接泄露,并确保无状态,避免下次借用时读到过时的一级缓存。
- 追问3:
TransactionSynchronizationManager绑定资源用的 Key 是什么?答:是SqlSessionFactory实例对象,这样保证了即使有多个不同的SqlSessionFactory(多数据源),它们的SqlSession也能被正确区分和管理。
- 追问1:
5. 如果项目中有多个数据源,如何让不同 Mapper 使用不同的 SqlSessionFactory?
- 回答:在
@MapperScan注解中使用sqlSessionFactoryRef属性,指定不同SqlSessionFactoryBean 的名称。内部原理是:MapperScannerRegistrar将该属性传递给MapperFactoryBean的BeanDefinition,最终在依赖注入时,MapperFactoryBean会通过setSqlSessionFactory按名称注入特定 Bean。 - 追问与加分:
- 追问1: 如果既配置了
sqlSessionFactoryRef又配置了sqlSessionTemplateRef会怎样?答:sqlSessionTemplateRef优先级更高,因为它封装了更完整的行为。 - 追问2: 在多数据源+JTA分布式事务场景下,这种机制还能工作吗?答:原理是兼容的,但需要配置支持 XA 的
DataSource和 JTA 的TransactionManager。TransactionSynchronizationManager绑定的将是 JTA 的Transaction对象。 - 追问3: 能否在一个 Mapper 里头操作两个不同的数据源?答:Spring 的声明式事务通常绑定单一数据源。如果硬要这么做,需要通过编程式事务或显式注入不同的
SqlSessionTemplate,但非常不推荐,违背了 Mapper 单一数据源职责的约定。
- 追问1: 如果既配置了
(系统设计题)10. 你要设计一个类似 MyBatis 的声明式远程调用框架,要求使用者只需定义接口并加上注解,就能像调用本地方法一样调用远程服务。请借鉴 MyBatis-Spring 的整合原理,利用 Spring 扩展点设计一个整合方案,并写出核心的 Registrar、FactoryBean 和类似 SqlSessionTemplate 的客户端代理伪代码。
- 回答:
- 设计与扩展点使用:完全借鉴 MyBatis-Spring 的四件套:
@RemoteCallScan+@Import(RemoteCallRegistrar.class)->RemoteCallRegistrar(ImportBeanDefinitionRegistrar)->RemoteCallFactoryBean(FactoryBean)->RemoteCallTemplate。 @RemoteCallScan:定义basePackages,remoteCallTemplateRef等属性,并标记@Import(RemoteCallRegistrar.class)。RemoteCallRegistrar:实现ImportBeanDefinitionRegistrar,在registerBeanDefinitions中扫描指定包下的接口,并将它们的BeanDefinition的beanClass替换为RemoteCallFactoryBean.class。RemoteCallFactoryBean:实现FactoryBean<T>。在其getObject()方法中,通过Proxy.newProxyInstance创建一个基于接口T的代理。该代理的InvocationHandler中,根据方法名和参数构造出 HTTP 请求,委托给RemoteCallTemplate执行。RemoteCallTemplate:类似SqlSessionTemplate的线程安全模板类,内部封装 HTTP 客户端池和负载均衡等逻辑。
- 追问与加分:
- 追问1: 如何支持
@RemoteCall注解定义每个方法的URL?答:在InvocationHandler中通过method.getAnnotation(RemoteCall.class)获取注解信息,动态解析 URL。 - 追问2: 如何在框架中集成 Spring 的负载均衡(
@LoadBalanced)?答:RemoteCallTemplate内部可以整合 Spring Cloud 的LoadBalancerClient,在执行 HTTP 调用时,根据服务名选择具体的实例地址。 - 追问3: 这种方式的优势是什么?答:对使用者完全透明,从调用远程服务变成调用本地接口,体验一致。
- 追问1: 如何支持
- 设计与扩展点使用:完全借鉴 MyBatis-Spring 的四件套:
MyBatis 整合扩展点与流程图速查表
| Spring 扩展点/机制 | MyBatis 实现类/组件 | 核心作用 | 调用/扩展时机 | 对应模块 |
|---|---|---|---|---|
@Import | MapperScannerRegistrar | 导入整合入口,将注解与注册逻辑关联 | 容器启动,解析配置类时 | 2 |
ImportBeanDefinitionRegistrar | MapperScannerRegistrar | 编程式注册 MapperFactoryBean 的 BeanDefinition | invokeBeanFactoryPostProcessors 阶段 | 3 |
BeanDefinitionRegistry | ClassPathMapperScanner | 动态注册和修改 BeanDefinition | 与上一步同时,在扫描和注册阶段 | 3 |
FactoryBean | MapperFactoryBean | 创建 Mapper 接口的代理对象,隐藏创建细节 | Bean 实例化阶段,调用 getObject() 时 | 4 |
InitializingBean | MapperFactoryBean | 初始化校验与 Mapper 接口注册 | Bean 属性注入完毕,在 getObject() 之前 | 4 |
| JDK 动态代理 | MapperProxy | 拦截 Mapper 接口方法调用,并转换为 SQL 命令调用 | 运行时,每次调用 Mapper 方法时 | 4 |
| JDK 动态代理 | SqlSessionInterceptor | 拦截 SqlSession 方法调用,实现线程安全 | 运行时,每次调用 SqlSession 方法时 | 4, 5 |
TransactionSynchronizationManager | SqlSessionTemplate | 绑定和管理事务中的 SqlSession,实现事务同步 | 事务开启/挂起/恢复/提交/回滚时 | 5 |
| 依赖注入 | SqlSessionDaoSupport | 注入 SqlSessionFactory 或 SqlSessionTemplate | Bean 属性填充阶段 | 6 |
延伸阅读
- mybatis-spring 官方文档:最权威的整合说明,涵盖了所有配置项和集成原理概览。
- 《MyBatis 3 源码深度解析》:江荣波著,详细解读了 MyBatis 核心模块的实现,包括
MapperProxy、Executor等,是深入 MyBatis 内部的绝佳读物。 - 前文《Spring 容器扩展点大全》:本系列关于
ImportBeanDefinitionRegistrar、FactoryBean、BeanPostProcessor等扩展点的深度讲解,是本文的 Spring 知识基础。 - Spring Framework 官方文档:尤其是关于
TransactionSynchronizationManager和事务管理的章节,是理解事务同步的基石。 - 《Spring 揭秘》:王福强著,深入浅出地讲解了 Spring 的设计哲学和扩展机制,有助于从更高维度理解 MyBatis-Spring 的设计。