Spring 深度内核-核心容器与扩展机制-MyBatis 与 Spring 整合原理:用 Spring 扩展点知识拆解 @MapperScan 的秘密

3 阅读37分钟

概述

如果说 Spring 是一个拥有无数插槽、遵循统一电气标准的巨大插座,那么 MyBatis-Spring 就是一个设计精巧、完美适配的插头。它不仅为 Spring 生态接入了强大的 ORM 能力,其整合过程本身,就是对 Spring 扩展点体系最经典、最优雅的诠释。

在前面的 Spring 核心容器系列中,我们系统性地深入了 IoC 容器、Bean 生命周期、依赖注入,更掌握了 @ImportImportBeanDefinitionRegistrarFactoryBeanBeanPostProcessorTransactionSynchronizationManager 等关键的扩展点接口。现在,是时候运用这些强大的“武器库”知识,去解剖 MyBatis-Spring 这个极其成功的整合案例了。

MyBatis 的 Mapper 接口本身只是一个接口,无法实例化,更无法直接注入到 Service 中。但通过 @MapperScan 一个注解,这些接口就神奇地变成了可以 @Autowired 的、功能完备的 Bean。这背后的魔法并非“黑科技”,而是 Spring 扩展点体系的经典演绎:ImportBeanDefinitionRegistrar 负责扫描和动态注册,FactoryBean 负责在运行时创建代理对象,TransactionSynchronizationManager 则作为桥梁,巧妙地保证了事务的一致性。

本文将带你用你已掌握的扩展点知识作为钥匙,打开 MyBatis-Spring 整合的黑箱。我们不仅会分析整合的每个步骤,更要深入到 MapperProxy 的内部,看清每一次方法调用是如何精准地转化为 SQL 执行,并理解这种设计为什么堪称“最佳实践”。

核心要点

  • 你已掌握的关键知识@ImportImportBeanDefinitionRegistrarFactoryBeanBeanFactoryPostProcessorInitializingBeanTransactionSynchronizationManager
  • MyBatis 整合的核心步骤
    1. 注解驱动入口@MapperScan 通过 @Import 触发 MapperScannerRegistrar
    2. 动态Bean定义注册MapperScannerRegistrar 利用 ClassPathMapperScanner 扫描接口,并为每个接口向 BeanDefinitionRegistry 动态注册一个类型为 MapperFactoryBean 的 BeanDefinition。
    3. 代理对象创建:Spring 容器创建 MapperFactoryBean 实例时,其 getObject() 方法通过 SqlSession.getMapper() 返回一个 MapperProxy 动态代理对象。
    4. 方法调用转SQLMapperProxy.invoke() 方法拦截所有接口方法调用,根据方法签名定位 MappedStatement,委托给 SqlSession 执行,完成从 Java 方法到 JDBC 操作的转换。
    5. 事务与线程安全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 接口定义转化为 MapperFactoryBeanBeanDefinition 并注册到容器中。
  • FactoryBean<T> (Spring Beans)

    • 契约T getObject()Class<?> getObjectType()default boolean isSingleton()
    • 能力:这是一个用于定制 Bean 创建逻辑的工厂 Bean。当一个 Bean 的实例化过程很复杂(例如需要创建代理),或者需要返回的类型与 Bean 本身的类型不同时,FactoryBean 是最佳选择。Spring 容器会调用它的 getObject() 方法来获取最终放入容器的 Bean 实例,而 FactoryBean 本身可以通过 &beanName 来获取。
    • 本文关联MapperFactoryBeanFactoryBean 的完美实现。它的 beanClassMapperFactoryBean,但它的 getObject() 返回的却是 Mapper 接口的代理对象。这就是我们能够注入并使用 Mapper 接口的根本原因。
  • InitializingBean (Spring Beans)

    • 契约void afterPropertiesSet()
    • 能力:由 Spring 容器调用,允许 Bean 在所有属性设置完毕之后执行自定义的初始化逻辑,例如校验关键配置。
    • 本文关联MapperFactoryBean 实现了此接口,在其 afterPropertiesSet() 方法中会校验非空的关键属性(如 SqlSessionFactorySqlSessionTemplate)。
  • 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 注解是无效的,因为没有处理器能识别它。它必须配合 @MapperScanannotationClass 属性(默认就会扫描标注了 @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));
  }
}

逐段解读

  1. registerBeanDefinitions 方法:这是 ImportBeanDefinitionRegistrar 接口的核心方法。importingClassMetadata 参数允许我们获取 @MapperScan 注解的所有配置,而 registry 参数则给了我们向容器动态注册 Bean 的能力。
  2. 创建 ClassPathMapperScannerMapperScannerRegistrar 本身不直接扫描,它将具体的扫描和注册任务委托给 ClassPathMapperScanner。这种职责分离的设计非常清晰。
  3. 配置传递MapperScannerRegistrar 作了一个桥梁,它从注解中读取 sqlSessionFactoryRef 等配置,然后设置给 ClassPathMapperScanner。这是注解驱动与编程式配置的结合。
  4. 启动扫描:最后调用 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;
  }
}

逐段解读

  1. 复用 Spring 扫描能力super.doScan(basePackages) 这行代码非常关键。它直接复用了 Spring 强大的类路径扫描和资源解析能力,可以自动找到包下所有符合条件的接口(默认是标记了 @Mapper 或有自定义注解的接口),并生成初步的 BeanDefinition,此时 beanClass 还是 com.xxx.UserMapper 这样的接口全限定名。
  2. 核心替换逻辑:这是整个 MyBatis-Spring 整合中最具“魔术性”的一步。
    • 保存原始接口definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()) 这行代码先将原始的 com.xxx.UserMapper 接口全限定名作为构造函数参数的值保存起来。这为后面 MapperFactoryBean 实例化时,通过构造器注入 mapperInterface 做好了准备。
    • 替换 beanClassdefinition.setBeanClassName(this.mapperFactoryBean.getClass().getName())beanClass 从接口类型替换为 org.mybatis.spring.mapper.MapperFactoryBean。这样一来,当 Spring 去创建这个 Bean 的实例时,它将不再尝试去实例化一个无法实例化的接口,而是去实例化一个实实在在的 MapperFactoryBean
  3. 设置自动装配AUTOWIRE_BY_TYPE 意味着当 MapperFactoryBean 实例化后,Spring 会自动根据其 setter 方法的参数类型,从容器中查找并注入匹配的 Bean(如 SqlSessionFactorySqlSessionTemplate)。

用 Spring 知识解释: 这一步完美展现了 BeanDefinitionRegistryBeanFactoryPostProcessor 的威力。ClassPathMapperScanner 在“运行时”动态地修改了 Bean 的定义(BeanDefinition),将“生产指令”从“制造一个 UserMapper”改成了“制造一个 MapperFactoryBean,并将 UserMapper 作为参数传入”。整个过程发生在 Bean 实例化之前,对后续的用户完全透明。这就是 Spring 留给开发者修改容器蓝图的最直接路径。

4. 双重代理创建:MapperFactoryBean、SqlSessionTemplate 与 MapperProxy 的协作

MapperFactoryBeanBeanDefinition 注册成功后,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 被调用。它做了两件事:一是通过父类方法校验 SqlSessionFactorySqlSessionTemplate 不为空;二是调用 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

  1. 过滤非业务方法Object 类的方法和 default 方法是不会被 MyBatis 拦截处理的,保证了代理对象的正常运行。
  2. 方法调用分发:通过 cachedInvoker 获取一个 MapperMethodInvoker。这是一种缓存机制,避免每次都解析 MethodPlainMethodInvoker 是默认的实现,它内部封装了一个 MapperMethod 对象。
  3. 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);
        }
      }
    }
  }
}

逐段解读

  1. getResource(sqlSessionFactory) (TransactionSynchronizationManager):这是 Spring 事务同步的核心。它尝试从当前线程的绑定资源中,获取以 sqlSessionFactory 为 Key 的 SqlSession。如果 Spring 事务管理器开启了一个事务,它就会将一个 SqlSession 对象绑定到这里。
  2. 存在事务时(sqlSession != null:直接复用这个已存在的 SqlSession。这保证了当前操作能参与到 Spring 管理的事务中,并能利用该 SqlSession 的一级缓存。
  3. 不存在事务时(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 时,事务管理器的执行流程如下:

  1. 开启事务DataSourceTransactionManagerDataSource 中获取一个 Connection,并设置 autoCommit=false
  2. 绑定资源:调用 TransactionSynchronizationManager.bindResource(this.dataSource, connection),将 Connection 绑定到当前线程。
  3. 同步 SqlSession:MyBatis 的 SqlSessionFactoryBeanSqlSessionTemplate 在哪里绑定呢?实际上,SqlSessionTemplateSqlSessionInterceptor 在首次发现事务存在但还没有绑定 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(默认)传播行为

    1. 外部方法调用 userService.methodA(),Spring 开启事务,绑定 SqlSession1
    2. methodA 内部调用 userMapper.getUserById(1),命中 SqlSession1,查询结果放入 SqlSession1 的一级缓存。
    3. methodA 内部再次调用 userMapper.getUserById(1)SqlSessionInterceptor 发现事务中已有 SqlSession1,直接复用。此次查询命中 SqlSession1 的一级缓存。
    4. 事务提交,SqlSession1 关闭,缓存失效。
    • 结论:在同一个事务内,MyBatis 一级缓存正常工作,可以有效减少数据库查询。
  • REQUIRES_NEW 传播行为

    1. 外部方法调用 userService.methodA()PROPAGATION_REQUIRED),Spring 开启事务 Tx1,绑定 SqlSession1Connection1
    2. methodA 调用 userService.methodB()PROPAGATION_REQUIRES_NEW)。
    3. Spring 挂起 Tx1TransactionSynchronizationManager.unbindResource(dataSource) 解除 Connection1 的绑定。
    4. Spring 创建并开启新事务 Tx2,获取 Connection2,并绑定到线程。
    5. methodB 内部调用 userMapper.getUserById(1)SqlSessionInterceptor 检查到没有绑定的 SqlSession(因为 Tx1SqlSession1 可能也已被挂起移除),于是根据 Connection2 创建一个新的 SqlSession2 并绑定。
    6. 查询结果放入 SqlSession2 的一级缓存。
    7. methodB 结束,Tx2 提交,SqlSession2 关闭,缓存失效。
    8. Tx1 恢复:Spring 将 Connection1 重新绑定。
    9. methodA 再次调用 userMapper.getUserById(1)SqlSessionInterceptor 发现 Tx1 中绑定的 SqlSession1(或重新创建绑定),复用 SqlSession1它无法命中先前在 methodBSqlSession2 里的一级缓存,会再次发送 SQL 查询。
    • 结论REQUIRES_NEW 会挂起并创建新的事务和 SqlSession,导致一级缓存在不同传播行为间被物理隔离,无法共享。 如果程序逻辑高度依赖一级缓存来避免重复查询,而数据库在高并发下数据又会变化,可能导致奇怪的“同一个事务里两次查询结果不同”的现象。

用 Spring 知识解释: MyBatis 通过将事务和资源管理完全托管给 Spring,自身变得非常轻量。DataSourceUtils 是连接复用的桥梁,TransactionSynchronizationManager 是资源绑定的核心。MyBatis 一级缓存的“不可预测”行为,本质上是 Spring 事务所管理资源的生命周期变化。掌握 TransactionSynchronizationManager 的状态流转,就能完全掌控一级缓存的边界。

6. 多数据源支持:@MapperScan 高级属性的原理

在现代应用中,读写分离或多数据源场景十分常见。@MapperScan 通过 sqlSessionTemplateRefsqlSessionFactoryRef 属性提供了优雅的支持。

6.1 属性与注入原理

回顾 @MapperScan 注解的属性:

  • sqlSessionFactoryRef(String):指定要使用的 SqlSessionFactory 的 Bean 名称。
  • sqlSessionTemplateRef(String):指定要使用的 SqlSessionTemplate 的 Bean 名称。

这两个属性会通过 MapperScannerRegistrar 传递给 ClassPathMapperScanner,并最终设置到为该包下所有 Mapper 接口创建的 MapperFactoryBeanBeanDefinition 中。

MapperFactoryBean 内部,其父类 SqlSessionDaoSupport 提供了 setSqlSessionFactorysetSqlSessionTemplate 方法。

// 类全限定名: 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 属性显式指定要使用的 SqlSessionFactory Bean 名称。
  • Spring 容器层orderMapperFactoryBeanuserMapperFactoryBean 虽然都是 MapperFactoryBean 类型,但它们在注入 SqlSessionFactory 时,Spring 的自动装配机制会结合 @Qualifier 或 Bean 名称,找到对应的 orderSFuserSF
  • 数据源层:不同的 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 的生命周期。

逐层/逐元素分解

  1. 容器启动注册阶段:由 @Import 触发,MapperScannerRegistrarClassPathMapperScanner 合作,通过 BeanDefinitionRegistry 动态改变 Bean 的“生产蓝图”。
  2. Bean 实例化阶段:容器根据修改后的“蓝图”创建 MapperFactoryBean,并利用 InitializingBeanFactoryBean 扩展点,完成校验和代理对象 MapperProxy 的创建。
  3. 运行时调用阶段:业务代码调用 MapperProxyMapperProxy 将请求委托给 SqlSessionTemplateSqlSessionTemplate 的内部代理则利用 TransactionSynchronizationManager 保证线程安全和事务同步。

设计原理映射:整个过程严格遵循了 Spring 的“开放-闭合”原则。Spring 核心只负责管理扩展点接口的调用时机和顺序,而 MyBatis-Spring 只需提供这些扩展点的具体实现即可。

工程联系与关键结论@MapperScan 到最终的 SQL 执行,中间没有任何“黑魔法”,而是一个精心编排的、基于 Spring 扩展点接口的协作流程。理解这个时序图,就理解了 MyBatis-Spring 整合的 90%。

7.2 流程步骤速查表

步骤涉及组件核心 Spring 扩展点关键 MyBatis 实现类
1. 触发注册@MapperScan 注解@ImportMapperScannerRegistrar
2. 动态修改定义ClassPathMapperScannerBeanDefinitionRegistry, BeanDefinition---
3. 实例化校验MapperFactoryBeanInitializingBean---
4. 创建代理对象MapperFactoryBeanFactoryBeanSqlSessionTemplate, MapperProxy
5. 线程安全&事务SqlSessionTemplateTransactionSynchronizationManager, JDK 动态代理SqlSessionInterceptor
6. 调用的转化MapperProxyInvocationHandlerMapperMethod, 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 Boot 自动配置的对比

    • 在纯 Spring 中,用户的显式配置(AppConfig)是整合的触发点。
    • 在 Spring Boot 中,MybatisAutoConfiguration 读取配置,自动创建 SqlSessionFactory 等 Bean,并通过 @MapperScanMapperScannerConfigurer 完成自动注册。
    • 核心原理完全一致。Spring Boot 的自动配置只是将这些配置步骤自动化、约定化,但底层使用的扩展点(FactoryBean, Registrar 等)是一样的。
  • 思考与启示: 如果我们自己要从零设计一个类似 MyBatis 的、需要与 Spring 深度集成的框架,我们也可以遵循同样的“配方”:

    1. 设计一个或多个自定义注解,用于标记和配置。
    2. 通过 @Import 导入我们的 ImportBeanDefinitionRegistrar
    3. Registrar 中,找到所有候选接口,为每个接口注册一个 FactoryBeanBeanDefinition
    4. 我们的 FactoryBean 实现 getObject 方法,返回一个基于 JDK 或 CGLIB 的代理,这个代理负责核心逻辑。
    5. 封装一个内部模板类(类似 SqlSessionTemplate),管理客户端连接的生命周期、线程安全和事务同步。

9. 生产事故排查专题

案例 1:@MapperScan 包路径写错,启动不报错但无法注入 Mapper

  • 现象:项目启动成功,无任何异常日志。但当访问一个尝试注入 UserMapper 的 Controller 时,抛出 NoSuchBeanDefinitionException
  • 排查:检查配置类,@MapperScan("com.xxx.user.mappper") 发现 mapper 多打了一个 p
  • 根因(结合整合原理)@MapperScan 背后的 ClassPathMapperScanner 在扫描时找不到任何匹配的资源,因此没有向 BeanDefinitionRegistry 注册任何 MapperFactoryBeanBeanDefinition。Spring 容器会正常启动,但容器中自然就没有这个 Bean。doScan 方法中的 if (beanDefinitions.isEmpty()) 仅打印一个 warn 日志,在多日志项目中极易被忽略。
  • 解决:修正 @MapperScanbasePackages 属性为正确的包路径。
  • 最佳实践
    1. 仔细检查 @MapperScan 的包路径,精确到具体包,不要过度使用宽泛的包名以免扫描过多不必要类,也避免写错时范围太大不易排查。
    2. 在开发环境,将 ClassPathMapperScanner 的日志级别调整为 DEBUG,以便在启动时就看到它扫描了哪些包。
    3. 可以为 MapperFactoryBean 设置 lazy-init=false(默认即是),这样任何 Mapper 找不到的问题在启动时就会暴露。

案例 2:Spring 事务不生效导致一级缓存幻觉

  • 现象:开发者在一个 Service 方法 methodA 上标注了 @Transactional,方法内两次调用 userMapper.getUserById(1)。第一次查询有 SQL 日志,第二次查询没有,开发者认为缓存生效。但在另一个未加事务的 methodB 中,用同一个条件查询,依然没有 SQL 日志。这导致数据更新后,methodB 读到的是旧数据。
  • 排查:排查 methodA 的事务配置,发现其类上使用了 AOP 限制,导致 @Transactional 注解未生效。两次调用实际上都是在非事务环境下进行的。
  • 根因(结合整合原理):如果 methodA 的事务未生效,两次 userMapper.getUserById(1) 的调用,在 SqlSessionTemplateSqlSessionInterceptor 看来都是无事务调用,会各自创建新的 SqlSession 并立即关闭。第二次查询应该依旧有 SQL 日志才对。为何第一次之后没有?大概率是二级缓存开启了且命中。如果二级缓存都关闭,那可能是单元测试或日志查看的偏差。
  • 再分析:如果是一个真正生效的 @Transactional(propagation=Propagation.REQUIRES_NEW),如 methodB 调用 methodA,且 methodA 使用了 REQUIRES_NEW。那么在 methodA 中会创建新的 SqlSession,一级缓存位于该新 SqlSession 中,对外部的 methodB 是不可见的。当 methodA 返回后,其 SqlSession 已关闭,一级缓存失效。外部的 methodB 再进行同样查询,会再次访问数据库。
  • 解决
    1. 检查并使 @Transactional 生效(正确引入 AOP、配置正确的 TransactionManager 等)。
    2. 充分理解 PROPAGATION_REQUIRES_NEW 会隔离事务和一级缓存。
  • 最佳实践
    1. 避免业务逻辑过度依赖一级缓存。它只是一个 SqlSession 级别的“鸡肋”缓存。
    2. 在跨 REQUIRES_NEW 的场景下,如果确实需要共享查询结果,考虑使用二级缓存或显式在更高层传递数据对象。
    3. 关键业务流,可以在核心查询上打印 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: ImportBeanDefinitionRegistrarBeanFactoryPostProcessor 优势在哪?答:Registrar 更早期,它接收 AnnotationMetadata,可直接获取导入它的注解的元数据,逻辑更内聚。
    • 追问3: 如果一个接口没加 @Mapper 也没在任何 @MapperScan 的包下,能当 Mapper 用吗?答:不能,因为容器中没有对应的 MapperFactoryBeanBeanDefinition

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 是如何生效的?答:该值被传入 MapperFactoryBeanBeanDefinition 中,最终在依赖注入时,Spring结合 @Qualifier 按名称查找对应 SqlSessionFactory
    • 追问3: 如果不使用 @MapperScan,如何用 XML 实现类似功能?答:可以用 MapperScannerConfigurer,它本质上就是 MapperScannerRegistrar 的 XML 版本实现,实现了 BeanFactoryPostProcessor

3. MapperFactoryBean 的作用是什么?它是如何创建 Mapper 代理的?

  • 回答:它是一个 FactoryBean,是“生产” Mapper 代理对象的工厂。当 Spring 需要获取一个 userMapper Bean时,会调用 MapperFactoryBean.getObject()。该方法内部通过 getSqlSession().getMapper(mapperInterface) 获取 Mapper 接口的 JDK 动态代理对象,并返回。
  • 追问与加分
    • 追问1: getSqlSession().getMapper() 内部发生了什么?答:最终会走到 Configuration.getMapper(),它会为接口创建一个 MapperProxy(JDK 的 InvocationHandler),并通过 Proxy.newProxyInstance 创建代理对象。
    • 追问2: afterPropertiesSet() 有什么用?答:它实现了 InitializingBean,用于在属性注入后执行初始化检查,确保 SqlSessionTemplate 等关键依赖不为空,并将 Mapper 接口注册到 MyBatis Configuration 中。
    • 追问3: 为什么不直接把 MapperProxy 注册为一个单例 Bean,而要用 FactoryBean 包装一层?答:因为创建 MapperProxy 需要 mapperInterfacesqlSession 等上下文信息,这些信息对每个 Mapper 都不同。FactoryBean 提供了封装这种“一个Bean对应一个工厂”的创建逻辑的最佳方式。

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 也能被正确区分和管理。

5. 如果项目中有多个数据源,如何让不同 Mapper 使用不同的 SqlSessionFactory?

  • 回答:在 @MapperScan 注解中使用 sqlSessionFactoryRef 属性,指定不同 SqlSessionFactory Bean 的名称。内部原理是:MapperScannerRegistrar 将该属性传递给 MapperFactoryBeanBeanDefinition,最终在依赖注入时,MapperFactoryBean 会通过 setSqlSessionFactory 按名称注入特定 Bean。
  • 追问与加分
    • 追问1: 如果既配置了 sqlSessionFactoryRef 又配置了 sqlSessionTemplateRef 会怎样?答:sqlSessionTemplateRef 优先级更高,因为它封装了更完整的行为。
    • 追问2: 在多数据源+JTA分布式事务场景下,这种机制还能工作吗?答:原理是兼容的,但需要配置支持 XA 的 DataSource 和 JTA 的 TransactionManagerTransactionSynchronizationManager 绑定的将是 JTA 的 Transaction 对象。
    • 追问3: 能否在一个 Mapper 里头操作两个不同的数据源?答:Spring 的声明式事务通常绑定单一数据源。如果硬要这么做,需要通过编程式事务或显式注入不同的 SqlSessionTemplate,但非常不推荐,违背了 Mapper 单一数据源职责的约定。

(系统设计题)10. 你要设计一个类似 MyBatis 的声明式远程调用框架,要求使用者只需定义接口并加上注解,就能像调用本地方法一样调用远程服务。请借鉴 MyBatis-Spring 的整合原理,利用 Spring 扩展点设计一个整合方案,并写出核心的 Registrar、FactoryBean 和类似 SqlSessionTemplate 的客户端代理伪代码。

  • 回答
    1. 设计与扩展点使用:完全借鉴 MyBatis-Spring 的四件套:@RemoteCallScan + @Import(RemoteCallRegistrar.class) -> RemoteCallRegistrar(ImportBeanDefinitionRegistrar) -> RemoteCallFactoryBean(FactoryBean) -> RemoteCallTemplate
    2. @RemoteCallScan:定义 basePackages, remoteCallTemplateRef 等属性,并标记 @Import(RemoteCallRegistrar.class)
    3. RemoteCallRegistrar:实现 ImportBeanDefinitionRegistrar,在 registerBeanDefinitions 中扫描指定包下的接口,并将它们的 BeanDefinitionbeanClass 替换为 RemoteCallFactoryBean.class
    4. RemoteCallFactoryBean:实现 FactoryBean<T>。在其 getObject() 方法中,通过 Proxy.newProxyInstance 创建一个基于接口 T 的代理。该代理的 InvocationHandler 中,根据方法名和参数构造出 HTTP 请求,委托给 RemoteCallTemplate 执行。
    5. RemoteCallTemplate:类似 SqlSessionTemplate 的线程安全模板类,内部封装 HTTP 客户端池和负载均衡等逻辑。
    • 追问与加分
      • 追问1: 如何支持 @RemoteCall 注解定义每个方法的URL?答:在 InvocationHandler 中通过 method.getAnnotation(RemoteCall.class) 获取注解信息,动态解析 URL。
      • 追问2: 如何在框架中集成 Spring 的负载均衡(@LoadBalanced)?答:RemoteCallTemplate 内部可以整合 Spring Cloud 的 LoadBalancerClient,在执行 HTTP 调用时,根据服务名选择具体的实例地址。
      • 追问3: 这种方式的优势是什么?答:对使用者完全透明,从调用远程服务变成调用本地接口,体验一致。

MyBatis 整合扩展点与流程图速查表

Spring 扩展点/机制MyBatis 实现类/组件核心作用调用/扩展时机对应模块
@ImportMapperScannerRegistrar导入整合入口,将注解与注册逻辑关联容器启动,解析配置类时2
ImportBeanDefinitionRegistrarMapperScannerRegistrar编程式注册 MapperFactoryBeanBeanDefinitioninvokeBeanFactoryPostProcessors 阶段3
BeanDefinitionRegistryClassPathMapperScanner动态注册和修改 BeanDefinition与上一步同时,在扫描和注册阶段3
FactoryBeanMapperFactoryBean创建 Mapper 接口的代理对象,隐藏创建细节Bean 实例化阶段,调用 getObject()4
InitializingBeanMapperFactoryBean初始化校验与 Mapper 接口注册Bean 属性注入完毕,在 getObject() 之前4
JDK 动态代理MapperProxy拦截 Mapper 接口方法调用,并转换为 SQL 命令调用运行时,每次调用 Mapper 方法时4
JDK 动态代理SqlSessionInterceptor拦截 SqlSession 方法调用,实现线程安全运行时,每次调用 SqlSession 方法时4, 5
TransactionSynchronizationManagerSqlSessionTemplate绑定和管理事务中的 SqlSession,实现事务同步事务开启/挂起/恢复/提交/回滚时5
依赖注入SqlSessionDaoSupport注入 SqlSessionFactorySqlSessionTemplateBean 属性填充阶段6

延伸阅读

  1. mybatis-spring 官方文档:最权威的整合说明,涵盖了所有配置项和集成原理概览。
  2. 《MyBatis 3 源码深度解析》:江荣波著,详细解读了 MyBatis 核心模块的实现,包括 MapperProxyExecutor 等,是深入 MyBatis 内部的绝佳读物。
  3. 前文《Spring 容器扩展点大全》:本系列关于 ImportBeanDefinitionRegistrarFactoryBeanBeanPostProcessor 等扩展点的深度讲解,是本文的 Spring 知识基础。
  4. Spring Framework 官方文档:尤其是关于 TransactionSynchronizationManager 和事务管理的章节,是理解事务同步的基石。
  5. 《Spring 揭秘》:王福强著,深入浅出地讲解了 Spring 的设计哲学和扩展机制,有助于从更高维度理解 MyBatis-Spring 的设计。