> For the complete documentation index, see [llms.txt](https://ldbmcs.gitbook.io/java/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ldbmcs.gitbook.io/java/java-frameworks-1/mybatis/mybatis-shi-san-zheng-he-spring.md).

# MyBatis(十三) - 整合Spring

## 1. 整合

## 2. 整合原理

回到 `spring-mybatis.xml` 配置文件中，MyBatis 整合 SpringFramework 的核心就是那个 `**SqlSessionFactoryBean**` ，我们的切入点就是从这里出发。

### 2.1 SqlSessionFactoryBean的设计

先简单看一下 `SqlSessionFactoryBean` 的内部成员，一上来我们就可以看到两个非常核心的东西：

```java
public class SqlSessionFactoryBean
        implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {

    // ......

    private Resource configLocation;

    private Configuration configuration;
```

嚯，MyBatis 的配置文件路径，以及 MyBatis 的核心 `**Configuration**` 对象！有这些东西，想必构造 `SqlSessionFactory` 也应该不成问题吧。

再往下看，更多的熟悉的东西都会映入我们眼帘：

```java
    private Resource[] mapperLocations;
    private DataSource dataSource;
    private TransactionFactory transactionFactory;
    private Properties configurationProperties;
    private Interceptor[] plugins;
    private TypeHandler<?>[] typeHandlers;
    private String typeHandlersPackage;
    // ......
```

这些属性都不需要小册一一解释吧，各位都是从前面一路学过来的，这些也都能看得懂。

另外，观察这个 `SqlSessionFactoryBean` 实现的接口：

* `FactoryBean<SqlSessionFactory>` ：可以通过 `getObject` 方法构造 `SqlSessionFactory`
* `InitializingBean` ：有初始化逻辑（猜想会不会就是这个初始化的切入点加载了 MyBatis 的相关东西呢？）
* `ApplicationListener<ApplicationEvent>` ：监听一些事件（实际上它只监听了 `ContextRefreshedEvent` 事件）

所以接下来的着眼点，我们就可以从这几个接口对应的方法往下看。按照 SpringFramework 对这几个接口的处理，我们先来看 `InitializingBean` 接口的 `afterPropertiesSet` 方法。

### 2.2 afterPropertiesSet

这个 `afterPropertiesSet` 方法只有一句话，但是这句话至关重要：

```java
@Override
public void afterPropertiesSet() throws Exception {
    // 判断 ......
    this.sqlSessionFactory = buildSqlSessionFactory();
}
```

构建 `**SqlSessionFactory**` ！！！这动作可太关键了，我们一定要好好看下去。但是这个方法非常非常长，所以小册会拆解成多段来看。

#### 2.2.1 处理MyBatis全局配置对象

```java
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

    final Configuration targetConfiguration;

    XMLConfigBuilder xmlConfigBuilder = null;
    // 事先构造过Configuration，直接处理
    if (this.configuration != null) {
        targetConfiguration = this.configuration;
        if (targetConfiguration.getVariables() == null) {
            targetConfiguration.setVariables(this.configurationProperties);
        } else if (this.configurationProperties != null) {
            targetConfiguration.getVariables().putAll(this.configurationProperties);
        }
    } else if (this.configLocation != null) {
        // 传入全局配置文件路径
        xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
        targetConfiguration = xmlConfigBuilder.getConfiguration();
    } else {
        // 啥也没有，一切走默认
        targetConfiguration = new Configuration();
        Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
    }
    // ......
```

首先的这一段，它会先准备一个 MyBatis 的全局配置对象 `Configuration` ，并根据是否事先注入 `configuration` 对象或者传入全局配置文件路径，决定是否准备 `XMLConfigBuilder` 。如果确实需要 `XMLConfigBuilder` 的处理，在下面会有调用它的 `parse` 方法，后面我们会看到。

#### 2.2.2 处理内置组件

```java
Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);
Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory);
Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl);
```

这三个组件就不用多解释了吧，之前第 7 章也遇见了，这里的设置只是为了考虑到 MyBatis 整合 SpringFramework 时，有些配置是在 `<bean>` 中配置而没有在 MyBatis 全局配置文件中而进行的分别处理。

#### 2.2.3 别名处理

```java
    // ......
    if (hasLength(this.typeAliasesPackage)) {
        scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream()
            .filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface())
            .filter(clazz -> !clazz.isMemberClass()).forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);
    }

    if (!isEmpty(this.typeAliases)) {
        Stream.of(this.typeAliases).forEach(typeAlias -> {
            targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
            LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");
        });
    }
    // ......
```

别名的包扫描，和某些特定类的别名设置，这个也都很简单了，也很容易理解。

默认情况下，包扫描注册的别名就是类名（首字母大写），如果类上有标注 `@Alias` 注解，则取注解属性值。

#### 2.2.4 处理插件、类型处理器

```java
    // ......
    if (!isEmpty(this.plugins)) {
        Stream.of(this.plugins).forEach(plugin -> {
            targetConfiguration.addInterceptor(plugin);
            LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
        });
    }

    if (hasLength(this.typeHandlersPackage)) {
        scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass())
            .filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
            .forEach(targetConfiguration.getTypeHandlerRegistry()::register);
    }

    if (!isEmpty(this.typeHandlers)) {
        Stream.of(this.typeHandlers).forEach(typeHandler -> {
            targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
            LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");
        });
    }

    targetConfiguration.setDefaultEnumTypeHandler(defaultEnumTypeHandler);
    // ......
```

接下来是处理 MyBatis 插件，以及 `TypeHandler` ，内容也非常简单，扫一眼即可。

留一个小细节，默认情况下最后一行的那个 `defaultEnumTypeHandler` 为 null ，除非我们在注册 `SqlSessionFactoryBean` 时注入过，否则 MyBatis 处理枚举类型时仍会使用默认的处理器。

#### 2.2.5 处理边角组件

```java
    // ......
    if (!isEmpty(this.scriptingLanguageDrivers)) {
        Stream.of(this.scriptingLanguageDrivers).forEach(languageDriver -> {
            targetConfiguration.getLanguageRegistry().register(languageDriver);
            LOGGER.debug(() -> "Registered scripting language driver: '" + languageDriver + "'");
        });
    }
    Optional.ofNullable(this.defaultScriptingLanguageDriver)
        .ifPresent(targetConfiguration::setDefaultScriptingLanguage);

    if (this.databaseIdProvider != null) {// fix #64 set databaseId before parse mapper xmls
        try {
            targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
        } catch (SQLException e) {
            throw new NestedIOException("Failed getting a databaseId", e);
        }
    }

    Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache);
    // ......
```

接下来的一些组件都是边角性的了，我们平时也不会主动碰它，所以直接忽略就好啦。

#### 2.2.6 解析MyBatis全局配置文件

```java
    // ......
    if (xmlConfigBuilder != null) {
        try {
            xmlConfigBuilder.parse();
            LOGGER.debug(() -> "Parsed configuration file: '" + this.configLocation + "'");
        } catch (Exception ex) {
            throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
        } finally {
            ErrorContext.instance().reset();
        }
    }
    // ......
```

上面的边边角角东西都处理的差不多了，接下来就是加载全局配置文件了，由于 `xmlConfigBuilder` 在执行构造器时已经把全局 `Configuration` 注入进去了，所以这个 `parse` 动作完事后，配置文件的内容也就都进入 `Configuration` 中了。

#### 2.2.7 处理数据源和事务工厂

```java
    // ......
    targetConfiguration.setEnvironment(new Environment(this.environment,
            this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
            this.dataSource));
    // ......
```

这段话只有一句代码，但这里面包含了两部分：**数据源与事务工厂**，注意默认情况下 MyBatis 与 SpringFramework 整合之后底层使用的事务工厂不再是 `JdbcTransactionFactory` ，而是 `SpringManagedTransactionFactory` ，不过底层的逻辑与原生 jdbc 并无太大差别。

#### 2.2.8 处理Mapper

```java
    // ......
    if (this.mapperLocations != null) {
        if (this.mapperLocations.length == 0) {
            LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
        } else {
            for (Resource mapperLocation : this.mapperLocations) {
                if (mapperLocation == null) {
                    continue;
                }
                try {
                    XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                            targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
                    xmlMapperBuilder.parse();
                } catch (Exception e) {
                    throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
                } finally {
                    ErrorContext.instance().reset();
                }
                LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
            }
        }
    } else {
        LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
    }

    return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}
```

因为 `SqlSessionFactoryBean` 只能传入 mapper.xml 的路径，所以这里的处理逻辑只有加载和解析 mapper.xml ，至于 Mapper 接口的话，那是另外的 `MapperScannerConfigurer` 了，我们马上就说它。

经过这么一大段 `afterPropertiesSet` 的逻辑，`SqlSessionFactory` 也就创建出来了。

### 2.3 事件监听

别忘了，最上面我们在看 `SqlSessionFactoryBean` 的接口实现中，还实现了一个 `ApplicationListener` 呢，那对应的 `onApplicationEvent` 方法我们也应该关注一下：

```java
@Override
public void onApplicationEvent(ApplicationEvent event) {
    if (failFast && event instanceof ContextRefreshedEvent) {
        // fail-fast -> check all statements are completed
        this.sqlSessionFactory.getConfiguration().getMappedStatementNames();
    }
}
```

注意看，它只监听了 `ContextRefreshedEvent` 类型的事件，那就意味着等 IOC 容器刷新完毕后，这个逻辑就会随之执行，先不看逻辑具体是什么，看一下这行注释：

fail-fast -> check all statements are completed.

快速失败->检查所有语句是否已完成。

所以说这个动作相当于是 MyBatis 初始化完成后的一个收尾检查性质的处理咯。点进去看一下 `getMappedStatementNames` 方法都干了些啥：

```java
public Collection<String> getMappedStatementNames() {
    buildAllStatements();
    return mappedStatements.keySet();
}
```

等一下，这个方法是有返回值的，但上面没用到，所以这个动作只是为了触发 `buildAllStatements();` 这一行代码咯，那为什么不直接调用它呢？

废话，要不是这个方法设置成 protected 或者 private ，你觉得 MyBatis 不会直接调用它吗？（狗头）

聊回来，我们看看这个 `buildAllStatements` 方法都干了些什么：

```java
protected void buildAllStatements() {
    parsePendingResultMaps();
    // 处理失败的cache-ref再处理一遍
    if (!incompleteCacheRefs.isEmpty()) {
        synchronized (incompleteCacheRefs) {
            incompleteCacheRefs.removeIf(x -> x.resolveCacheRef() != null);
        }
    }
    // 处理失败的statement再处理一遍
    if (!incompleteStatements.isEmpty()) {
        synchronized (incompleteStatements) {
            incompleteStatements.removeIf(x -> {
                x.parseStatementNode();
                return true;
            });
        }
    }
    // 处理失败的Mapper方法再处理一遍
    if (!incompleteMethods.isEmpty()) {
        synchronized (incompleteMethods) {
            incompleteMethods.removeIf(x -> {
                x.resolve();
                return true;
            });
        }
    }
}
```

可以发现，之前我们在第 10 章和第 13 章中看到的那些没有处理好的东西，在 IOC 容器刷新完毕后，MyBatis 还是不想放弃它们，想再给它们一次机会，于是就把它们又尝试着处理了一次。当然，这也是初始化阶段的最后一次机会了，这次要是再处理不了，那就拉倒了，真不管了。

到此为止，整个 `SqlSessionFactoryBean` 的任务也就全部完成了，大概抓住几个要点即可：

* 几乎可以代替 MyBatis 全局配置文件
* 可以传入全局配置文件，供 MyBatis 解析和处理
* 代替 MyBatis 处理数据源和事务工厂
* 只处理和解析 mapper.xml

`SqlSessionFactoryBean` 只负责 mapper.xml 的处理，那 Mapper 接口怎么办呢？哎别急，下面还有一个组件呢。

### 2.4 MapperScannerConfigurer

从类名上也能看得出来，它就是扫描 Mapper 接口的核心了。

#### 2.4.1 类的设计

首先请各位注意一点，这个家伙的本质是一个 `BeanDefinitionRegistryPostProcessor` ：

```java
public class MapperScannerConfigurer
        implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
```

这个细节相当重要，在 Spring 小册的第 28 章中我们有提过它，`BeanDefinitionRegistryPostProcessor` 的处理阶段最好不要直接使用 `getBean` 或者相似的方法去获取 IOC 容器中的 Bean ，如果只是为了检查或者依赖相关的 bean 的话，可以只依赖 bean 的 name 。

MyBatis 当然帮我们考虑到这个问题了，于是这个组件的内部设计是这样的：

```java
    private SqlSessionFactory sqlSessionFactory;

    private SqlSessionTemplate sqlSessionTemplate;

    private String sqlSessionFactoryBeanName;

    private String sqlSessionTemplateBeanName;
```

对应的，setter 方法也做了特殊处理：

```java
    // 标注为Deprecated，提示我们不要用它
    @Deprecated
    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
    }

    public void setSqlSessionFactoryBeanName(String sqlSessionFactoryName) {
        this.sqlSessionFactoryBeanName = sqlSessionFactoryName;
    }
```

可以说是非常贴心了哈，我们在实际用的时候也要注意为好，否则真的会出一些预期之外的问题。

#### 2.4.2 核心处理逻辑

说完了这个要注意的点，我们还是要看核心的处理逻辑，`BeanDefinitionRegistryPostProcessor` 的核心方法是 `postProcessBeanDefinitionRegistry` ，这个方法会向 `BeanDefinitionRegistry` 中注册新的 `BeanDefinition` ，具体的逻辑我们可以来看看源码逻辑：

```java
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
        processPropertyPlaceHolders();
    }

    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    // 一大堆set方法
    scanner.registerFilters();
    scanner.scan(
            StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
```

很明显，这里面的核心是这个 `**ClassPathMapperScanner**` ，它会执行包扫描的动作，并且将扫描到的 Mapper 接口都收集起来，构造为一个一个的 `MapperFactoryBean` （ `MapperFactoryBean` 想必各位都不会觉得陌生吧，它就是适配 Mapper 接口的工厂 Bean ）。那么核心的逻辑也就是 `ClassPathMapperScanner` 的 scan 方法了。

**2.4.2.1 scan**

很不巧，这个 `scan` 方法其实是 `ClassPathBeanDefinitionScanner` 的，因为 `ClassPathMapperScanner` 继承自 `ClassPathBeanDefinitionScanner` ，所以会先来到 `ClassPathBeanDefinitionScanner` 中：

```java
public int scan(String... basePackages) {
    int beanCountAtScanStart = this.registry.getBeanDefinitionCount();

    doScan(basePackages);

    // Register annotation config processors, if necessary.
    if (this.includeAnnotationConfig) {
        AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
    }

    return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}
```

嚯，又是 SpringFramework 的经典套路了，**xxx 方法最终调用 doXxx 方法，真正负责干活的是 doXxx 方法**。其实读到这里我们应该有一种感觉：**doXxx 方法应该是类似于模板方法那样，让子类重写了**吧！OK 我们继续往下看。

**2.4.2.2 doScan**

```java
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

    if (beanDefinitions.isEmpty()) {
        LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
                    + "' package. Please check your configuration.");
    } else {
        // 注意看这里
        processBeanDefinitions(beanDefinitions);
    }

    return beanDefinitions;
}
```

`ClassPathMapperScanner` 重写了 `doScan` 方法，也仅仅是多了一层 `BeanDefinition` 的处理而已，包扫描的逻辑还是用的 `ClassPathBeanDefinitionScanner` 的，那我们就不关心具体如何扫描了，主要是看扫描到 Mapper 接口之后都干了什么。

**2.4.2.3 processBeanDefinitions**

方法体略微有点长，不过没必要拆解开了，小册给源码中关键逻辑都加了注释，所以不用担心看起来费劲：

```java
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
        definition = (GenericBeanDefinition) holder.getBeanDefinition();
        // 获取Mapper接口的全限定名
        String beanClassName = definition.getBeanClassName();
        // logger ......

        // the mapper interface is the original class of the bean
        // but, the actual class of the bean is MapperFactoryBean
        // MapperFactoryBean的构造方法需要传入Mapper接口名
        definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
        definition.setBeanClass(this.mapperFactoryBeanClass);

        definition.getPropertyValues().add("addToConfig", this.addToConfig);

        // 给MapperFactoryBean传入SqlSessionFactory
        boolean explicitFactoryUsed = false;
        if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
            definition.getPropertyValues().add("sqlSessionFactory",
                    new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
            explicitFactoryUsed = true;
        } else if (this.sqlSessionFactory != null) {
            definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
            explicitFactoryUsed = true;
        }
        // 同样的逻辑处理SqlSessionTemplate ......

        if (!explicitFactoryUsed) {
            LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
            definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        }
        definition.setLazyInit(lazyInitialization);
    }
}
```

大体走下来，就这么两大步骤：拿着 Mapper 接口的全限定名创建 `MapperFactoryBean` ，注入 `SqlSessionFactory` 和 `SqlSessionTemplate` 。

处理好这些 `BeanDefinition` 之后，其实 `BeanDefinitionRegistry` 中就有这些 `MapperFactoryBean` 的定义了，后续的 bean 实例化阶段也就都能创建出对应的代理对象了，程序执行阶段也就可以拿得到了。

到这里，MyBatis 整合 SpringFramework 的两个重要组件的原理剖析也就全部完成了，内容不是很难，不过需要小伙伴们前期熟悉 SpringFramework 中 IOC 的一些高级特性，以便更好地理解。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ldbmcs.gitbook.io/java/java-frameworks-1/mybatis/mybatis-shi-san-zheng-he-spring.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
