MyBatis整合Spring核心原理:从配置到源码的完整解析(附实战示例)

关键词:MyBatis, Spring, SqlSessionFactoryBean, MapperScannerConfigurer, SqlSessionTemplate, 整合原理


在 Java 企业级开发中,MyBatisSpring 是两个不可或缺的框架。MyBatis 负责数据持久层,Spring 负责 IoC 容器和事务管理。那么,当我们将 MyBatis 整合到 Spring 中时,背后的原理是什么?SqlSessionMapper 代理对象是如何自动注入的?本文将从实战配置出发,深入源码剖析整合的核心原理,帮助你彻底理解这一过程。


📑 目录


一、MyBatis 整合 Spring 快速入门

1.1 添加相关依赖

首先,在 pom.xml 中添加 MyBatis-Spring 和 Spring 相关依赖:

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.0.4</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.1.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>5.1.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.1.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.14</version>
</dependency>

1.2 配置文件

将 MyBatis 整合到 Spring 中后,MyBatis 的配置文件可以简化甚至为空:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration 
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- MyBatis 配置可完全迁移到 Spring 配置文件中 -->
</configuration>

Spring 配置文件 applicationContext.xml

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop" 
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans 
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context 
           http://www.springframework.org/schema/context/spring-context-4.3.xsd
           http://www.springframework.org/schema/aop 
           http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
           http://www.springframework.org/schema/tx 
           http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
    
    <!-- 关联数据属性文件 -->
    <context:property-placeholder location="classpath:db.properties"/>
    
    <!-- 开启扫描 -->
    <context:component-scan base-package="com.bobo"/>

    <!-- 配置数据源 -->
    <bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    
    <!-- 整合 MyBatis:配置 SqlSessionFactory -->
    <bean class="org.mybatis.spring.SqlSessionFactoryBean" id="sqlSessionFactoryBean">
        <!-- 关联数据源 -->
        <property name="dataSource" ref="dataSource"/>
        <!-- 关联 MyBatis 配置文件 -->
        <property name="configLocation" value="classpath:mybatis-config-spring.xml"/>
        <!-- 指定映射文件的位置 -->
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>
        <!-- 添加别名 -->
        <property name="typeAliasesPackage" value="com.bobo.domain"/>
    </bean>
    
    <!-- 扫描 Mapper 接口 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.bobo.mapper"/>
    </bean>
</beans>

1.3 单元测试

整合完成后,通过 Spring 的单元测试验证:

@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
@RunWith(value = SpringJUnit4ClassRunner.class)
public class MyBatisSpringTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testQuery() {
        List<User> users = userMapper.selectUserList();
        for (User user : users) {
            System.out.println(user);
        }
    }
}

关键发现:整合到 Spring 后,原来需要手动操作的 SqlSessionFactorySqlSessiongetMapper() 都不见了,开发更加简洁!


二、SqlSessionFactory 的创建原理

把 MyBatis 集成到 Spring 中,并没有替换 MyBatis 的核心对象,只是做了一些包装或桥梁的工作。核心对象 SqlSessionFactorySqlSessionMapperProxy 仍然由 MyBatis jar 包提供。

理解整合原理,需要搞清楚三个问题:

  1. SqlSessionFactory 在哪创建的?
  2. SqlSession 在哪创建的?
  3. 代理类在哪创建的?

2.1 SqlSessionFactoryBean 的核心作用

Spring 配置文件中配置的 org.mybatis.spring.SqlSessionFactoryBean 是关键入口。它实现了三个重要接口:

接口方法作用
FactoryBeangetObject()返回由 FactoryBean 创建的 Bean 实例
InitializingBeanafterPropertiesSet()Bean 属性初始化完成后执行操作
ApplicationListeneronApplicationEvent()监听应用事件(如上下文刷新)

2.2 afterPropertiesSet():创建 SqlSessionFactory

public void afterPropertiesSet() throws Exception {
    Assert.notNull(this.dataSource, "Property 'dataSource' is required");
    Assert.notNull(this.sqlSessionFactoryBuilder, 
        "Property 'sqlSessionFactoryBuilder' is required");
    // 核心方法:构建 SqlSessionFactory
    this.sqlSessionFactory = this.buildSqlSessionFactory();
}

buildSqlSessionFactory() 方法完成了 SqlSessionFactory 的创建和配置文件解析:

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
    // 1. 创建 Configuration 对象
    Configuration targetConfiguration;
    
    // 2. 根据配置文件路径构建 XMLConfigBuilder
    if (this.configLocation != null) {  
        xmlConfigBuilder = new XMLConfigBuilder(
            this.configLocation.getInputStream(), 
            null, 
            this.configurationProperties
        );
        targetConfiguration = xmlConfigBuilder.getConfiguration();
    } else {
        targetConfiguration = new Configuration();
    }
    
    // 3. 设置 Configuration 属性
    // - 设置 objectFactory、objectWrapperFactory
    // - 扫描并注册 typeAliasesPackage 下的别名
    // - 注册 plugins(拦截器)
    // - 注册 typeHandlers(类型处理器)
    
    // 4. 解析全局配置文件
    if (xmlConfigBuilder != null) {
        xmlConfigBuilder.parse();
    }
    
    // 5. 设置 Environment(包含数据源和事务工厂)
    targetConfiguration.setEnvironment(new Environment(
        this.environment,
        this.transactionFactory == null 
            ? new SpringManagedTransactionFactory() 
            : this.transactionFactory,
        this.dataSource
    ));
    
    // 6. 解析 Mapper.xml 文件
    if (this.mapperLocations != null) {
        for (Resource mapperLocation : this.mapperLocations) {
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(
                mapperLocation.getInputStream(),
                targetConfiguration,
                mapperLocation.toString(),
                targetConfiguration.getSqlFragments()
            );
            xmlMapperBuilder.parse(); // 解析 Mapper 文件
        }
    }
    
    // 7. 创建并返回 DefaultSqlSessionFactory
    return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}

方法小结

  • 通过实现 InitializingBean 接口的 afterPropertiesSet() 方法
  • 在 Spring 初始化 Bean 属性后,自动完成配置解析和 SqlSessionFactory 创建

2.3 getObject():返回 SqlSessionFactory

SqlSessionFactoryBean 实现了 FactoryBean 接口,这意味着从 Spring 容器获取该 Bean 时,实际返回的是 getObject() 的结果:

public SqlSessionFactory getObject() throws Exception {
    if (this.sqlSessionFactory == null) {
        this.afterPropertiesSet(); // 懒加载机制
    }
    return this.sqlSessionFactory;
}

2.4 onApplicationEvent():快速失败检测

实现 ApplicationListener 接口可以监听应用事件。这里监听 ContextRefreshedEvent(上下文刷新事件),用于快速检测 MappedStatement 是否加载完毕:

public void onApplicationEvent(ApplicationEvent event) {
    if (this.failFast && event instanceof ContextRefreshedEvent) {
        this.sqlSessionFactory.getConfiguration().getMappedStatementNames();
    }
}

三、SqlSession 的线程安全问题与解决方案

3.1 DefaultSqlSession 的线程安全问题

在使用原生 MyBatis 时,通过 SqlSessionFactory.openSession() 获取的是 DefaultSqlSession

⚠️ 重要问题:DefaultSqlSession 是线程不安全的!

在 MyBatis 官方文档中明确指出:

每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此不能被共享。

因此,使用原生 MyBatis 时需要这样写:

try (SqlSession session = sqlSessionFactory.openSession()) {
    // 应用逻辑代码
}
// 或者
SqlSession session = null;
try {
    session = sqlSessionFactory.openSession();
    // 应用逻辑代码
} finally {
    session.close();
}

3.2 SqlSessionTemplate:线程安全的解决方案

在 MyBatis-Spring 中,提供了线程安全的 SqlSession 包装类:SqlSessionTemplate

特点

  • 线程安全,可以在所有 DAO 层共享一个实例(默认单例)
  • 实现了 SqlSession 接口,提供相同的 API
  • 内部使用代理模式管理 SqlSession 的生命周期

内部实现

public class SqlSessionTemplate implements SqlSession {
    
    private final SqlSessionFactory sqlSessionFactory;
    private final ExecutorType executorType;
    private final SqlSession sqlSessionProxy; // 代理对象

    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, 
                              ExecutorType executorType,
                              PersistenceExceptionTranslator exceptionTranslator) {
        this.sqlSessionFactory = sqlSessionFactory;
        this.executorType = executorType;
        this.exceptionTranslator = exceptionTranslator;
        
        // 创建 SqlSession 接口的代理对象
        this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
            SqlSessionFactory.class.getClassLoader(),
            new Class[] { SqlSession.class },
            new SqlSessionInterceptor()  // InvocationHandler
        );
    }
    
    // 所有方法都委托给 sqlSessionProxy
    @Override
    public <T> T selectOne(String statement) {
        return this.sqlSessionProxy.selectOne(statement);
    }
    // ... 其他方法
}

3.3 SqlSessionInterceptor:代理逻辑

SqlSessionInterceptorSqlSessionTemplate 的内部类,实现了 InvocationHandler

private class SqlSessionInterceptor implements InvocationHandler {
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 1. 获取 SqlSession(从 Spring 事务管理器或创建新的)
        SqlSession sqlSession = getSqlSession(
            SqlSessionTemplate.this.sqlSessionFactory,
            SqlSessionTemplate.this.executorType,
            SqlSessionTemplate.this.exceptionTranslator
        );
        
        try {
            // 2. 执行目标方法
            Object result = method.invoke(sqlSession, args);
            
            // 3. 检查是否被 Spring 管理事务
            if (!isSqlSessionTransactional(sqlSession, 
                    SqlSessionTemplate.this.sqlSessionFactory)) {
                // 如果没有被 Spring 管理,强制提交
                sqlSession.commit(true);
            }
            return result;
        } catch (Throwable t) {
            // 异常处理...
            throw t;
        } finally {
            // 4. 关闭 SqlSession(如果没有被 Spring 管理)
            if (!isSqlSessionTransactional(sqlSession, 
                    SqlSessionTemplate.this.sqlSessionFactory)) {
                closeSqlSession(sqlSession, 
                    SqlSessionTemplate.this.sqlSessionFactory);
            }
        }
    }
}

核心逻辑

调用 SqlSessionTemplate.selectOne() 
    ↓
代理到 SqlSessionInterceptor.invoke()
    ↓
获取 SqlSession(检查 Spring 事务上下文)
    ↓
执行目标方法
    ↓
提交/回滚(如果非 Spring 管理事务)
    ↓
关闭 SqlSession(如果非 Spring 管理事务)

总结:因为 DefaultSqlSession 自己做不到每次请求调用产生一个新的实例,MyBatis-Spring 通过代理模式,在任何一个方法被调用时都先创建一个 DefaultSqlSession 实例,再调用被代理对象的相应方法。

3.4 SqlSessionDaoSupport:简化 DAO 开发

为了让 DAO 层更方便地获取 SqlSessionTemplate,MyBatis-Spring 提供了 SqlSessionDaoSupport 抽象类:

public abstract class SqlSessionDaoSupport extends DaoSupport {
    
    private SqlSessionTemplate sqlSessionTemplate;

    public SqlSession getSqlSession() {
        return this.sqlSessionTemplate;
    }
    
    @Autowired(required = false)
    public final void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        if (!this.externalSqlSession) {
            this.sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
        }
    }
    // ...
}

使用方式

@Repository
public class UserDaoImpl extends SqlSessionDaoSupport implements UserMapper {
    
    @Autowired
    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        super.setSqlSessionFactory(sqlSessionFactory);
    }
    
    @Override
    public User selectById(Integer id) {
        return getSqlSession().selectOne(
            "com.bobo.mapper.UserMapper.selectById", id
        );
    }
}

更进一步,可以创建 BaseDao 封装通用操作:

public class BaseDao extends SqlSessionDaoSupport {
    
    @Autowired
    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        super.setSqlSessionFactory(sqlSessionFactory);
    }

    public Object selectOne(String statement, Object parameter) {
        return getSqlSession().selectOne(statement, parameter);
    }
    
    public List<?> selectList(String statement, Object parameter) {
        return getSqlSession().selectList(statement, parameter);
    }
    // ...
}

四、Mapper 接口代理对象的创建

在实战中,我们通常直接注入 Mapper 接口,而不需要创建实现类。这是如何实现的?

4.1 MapperScannerConfigurer:扫描 Mapper 接口

Spring 配置文件中的 MapperScannerConfigurer 是关键:

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.bobo.mapper"/>
</bean>

MapperScannerConfigurer 的继承结构

MapperScannerConfigurer
    ↓
implements BeanDefinitionRegistryPostProcessor
    ↓
postProcessBeanDefinitionRegistry()

实现了 BeanDefinitionRegistryPostProcessor 接口,可以在 Spring 创建 Bean 之前修改 Bean 定义:

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    // 1. 创建 ClassPathMapperScanner
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    scanner.setSqlSessionFactory(this.sqlSessionFactory);
    scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
    // ... 其他配置
    
    // 2. 注册过滤器
    scanner.registerFilters();
    
    // 3. 扫描 basePackage 指定的包及其子包
    scanner.scan(StringUtils.tokenizeToStringArray(
        this.basePackage, 
        ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS
    ));
}

4.2 doScan():扫描并注册 BeanDefinition

@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    // 1. 调用父类方法扫描所有接口
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

    if (beanDefinitions.isEmpty()) {
        LOGGER.warn(() -> "No MyBatis mapper was found...");
    } else {
        // 2. 处理 BeanDefinition
        processBeanDefinitions(beanDefinitions);
    }
    return beanDefinitions;
}

核心处理:将 Mapper 接口的 BeanClass 改为 MapperFactoryBean

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
        definition = (GenericBeanDefinition) holder.getBeanDefinition();
        String beanClassName = definition.getBeanClassName();
        
        // 添加构造函数参数:Mapper 接口类型
        definition.getConstructorArgumentValues()
            .addGenericArgumentValue(beanClassName);
        
        // 关键:将 BeanClass 设置为 MapperFactoryBean
        definition.setBeanClass(this.mapperFactoryBeanClass);
        
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
    }
}

核心思想:因为接口无法直接创建实例,所以在创建对象之前,将接口类型指向一个具体的 Java 类型 MapperFactoryBean

4.3 MapperFactoryBean:FactoryBean 的妙用

类图结构

MapperFactoryBean<T>
    ↓ extends
SqlSessionDaoSupport
    ↓ extends
DaoSupport
    ↓ implements
FactoryBean<T>

MapperFactoryBean 的作用

  1. 继承了 SqlSessionDaoSupport → 可以获取 SqlSessionTemplate
  2. 实现了 FactoryBean → 自定义 Bean 实例化逻辑
public class MapperFactoryBean<T> extends SqlSessionDaoSupport 
    implements FactoryBean<T> {
    
    private Class<T> mapperInterface;

    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    public T getObject() throws Exception {
        // 通过 SqlSessionTemplate 获取 Mapper 代理对象
        return getSqlSession().getMapper(this.mapperInterface);
    }

    @Override
    public Class<T> getObjectType() {
        return this.mapperInterface;
    }
    
    @Override
    public boolean isSingleton() {
        return true;
    }
}

最终调用链

@Autowired
private UserMapper userMapper;  // 注入时
    ↓
Spring 容器调用 MapperFactoryBean.getObject()
    ↓
getSqlSession().getMapper(UserMapper.class)
    ↓
SqlSessionTemplate.getMapper() → DefaultSqlSession.getMapper()
    ↓
Configuration.getMapper() → MapperRegistry.getMapper()
    ↓
MapperProxyFactory.newInstance() → Proxy.newProxyInstance()
    ↓
返回 Mapper 接口的 JDK 动态代理对象(MapperProxy)

执行 Mapper 方法时的流程

userMapper.selectById(1)
    ↓
MapperProxy.invoke()  // 代理对象的方法拦截
    ↓
进入 MyBatis 的 SQL 处理流程

4.4 整合原理总结

Spring 整合 MyBatis 的核心步骤:

步骤说明
1提供 SqlSessionTemplate 替代 DefaultSqlSession,线程安全
2提供 SqlSessionDaoSupport 获取 SqlSessionTemplate
3扫描 Mapper 接口,注册到容器中的是 MapperFactoryBean
4注入 Mapper 时,调用 getObject() 方法,注入 JDK 动态代理对象
5执行 Mapper 方法时,触发 MapperProxy,进入 SQL 处理流程

五、设计模式总结

MyBatis 和 MyBatis-Spring 中使用了大量的设计模式:

设计模式应用类说明
工厂模式SqlSessionFactory、ObjectFactory、MapperProxyFactory创建对象实例
建造者模式XMLConfigBuilder、XMLMapperBuilder复杂对象的分步构建
单例模式SqlSessionFactory、Configuration全局唯一实例
代理模式MapperProxy、Plugin、SqlSessionInterceptor方法拦截和增强
适配器模式Log 适配器适配不同日志框架
模板方法BaseExecutor定义算法骨架,子类实现细节
装饰器模式LoggingCache、LruCache、CachingExecutor动态添加功能
责任链模式InterceptorChain多个拦截器链式执行

六、核心对象生命周期总结

对象生命周期说明
SqlSessionTemplateSpring 管理SqlSession 的线程安全替代品,单例
SqlSessionDaoSupport抽象类用于获取 SqlSessionTemplate
SqlSessionInterceptor内部类代理对象,代理 DefaultSqlSession
MapperFactoryBeanSpring 管理继承 SqlSessionDaoSupport,实现 FactoryBean
SqlSessionHolder事务绑定控制 SqlSession 和事务的绑定关系

七、总结

本文从 MyBatis-Spring 的实战配置出发,深入剖析了整合的核心原理:

关键知识点:

  1. SqlSessionFactoryBean 实现了 InitializingBeanFactoryBeanApplicationListener 三个接口,在 Bean 初始化时完成 SqlSessionFactory 的创建

  2. SqlSessionTemplate 通过代理模式解决了 DefaultSqlSession 的线程安全问题,内部使用 SqlSessionInterceptor 管理 SqlSession 的生命周期

  3. MapperScannerConfigurer 扫描 Mapper 接口,将 BeanDefinition 的 BeanClass 修改为 MapperFactoryBean

  4. MapperFactoryBean 实现了 FactoryBean 接口,在 getObject() 中返回 Mapper 接口的 JDK 动态代理对象

  5. 最终调用 Mapper 方法时,仍然是 MapperProxyinvoke() 方法拦截,与原生 MyBatis 流程一致

使用建议:

  • 优先使用 MapperScannerConfigurer 自动扫描,减少手动配置
  • 注意 SqlSession 的线程安全问题,始终使用 SqlSessionTemplate
  • 结合 Spring 事务管理,确保数据库操作的一致性

希望这篇文章能帮助你深入理解 MyBatis 与 Spring 的整合原理!


💡 温馨提示:如果文章对你有帮助,欢迎点赞、收藏、关注!有任何问题欢迎在评论区留言讨论。


参考资源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

加倍巴巴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值