MyBatis 与 Spring 整合原理分析

简介: 前言我们常常将 Spring 与 MyBatis 结合在一起使用,由于篇幅问题,上篇《MyBatis 快速整合 Spring》仅介绍了将 MyBatis 整合到 Spring 的方式,这篇在上篇的基础上总结出几个问题,并尝试通过分析其底层源码进行回答。

前言


我们常常将 Spring 与 MyBatis 结合在一起使用,由于篇幅问题,上篇《MyBatis 快速整合 Spring》仅介绍了将 MyBatis 整合到 Spring 的方式,这篇在上篇的基础上总结出几个问题,并尝试通过分析其底层源码进行回答。


MyBatis 为何提出 SqlSessionFactoryBean?


Spring 环境下,我们常将所使用的对象交由 Spring IOC 容器来管理。MyBatis 执行 SQL 的入口是非线程安全的 SqlSession,因此可以将获取 SqlSession 的线程安全的 SqlSessionFactory 注册为 Spring bean,而 mybatis-spring 项目又提供了一个 SqlSessionFactoryBean 替代原生的 SqlSessionFactory,不免让人产生疑问,Why?


配置简化


在上篇文章中,我们将 SqlSessionFactoryBean 配置为 bean,这个类的功能类似于 SqlSessionFactoryBuilder,都可以创建 SqlSessionFactory ,SqlSessionFactoryBuilder 主要从 xml 文件中读取配置,我们手动配置 SqlSessionFactoryBean bean 的时候却没有指定配置文件地址,并且仅设置了少量的配置项,因此可以认为 SqlSessionFactoryBean 简化了创建 SqlSessionFactory 的配置。


Spring 事务支持


如果只是使用 SqlSessionFactoryBean 替代 SqlSessionFactoryBuilder 以此来取消对 xml 配置文件的使用,那么这个提升可以说并不明显。


springframework 项目中有一个 spring-tx 的模块,统一了事务管理,作为 ORM 框架的 mybatis 整合到 spring 中自然选择了对 Spring 事务管理的支持。那么对 mybatis 改造的入口就是 mybatis 的事务管理器,将这个事务管理器改造为支持 Spring 事务的事务管理器即可。事务管理器作为 mybatis 的配置项自然而然的放到 SqlSessionFactoryBean 创建 SqlSessionFactory 的逻辑中就可以了,当然这也意味着 mybatis 会忽略环境相关的所有配置。查看 SqlSessionFactoryBean 相关源码如下。


public class SqlSessionFactoryBean
    implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
    protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
        ... 省略部分代码
        targetConfiguration.setEnvironment(new Environment(this.environment,
            this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
            this.dataSource));
        ... 省略部分代码
  }
}


那么 mybatis 又是如何将事务管理器适配成支持 Spring 事务管理的事务管理器呢?跟踪 SpringManagedTransactionFactory 代码。


public class SpringManagedTransactionFactory implements TransactionFactory {
    @Override
    public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
        return new SpringManagedTransaction(dataSource);
    }
  ... 省略部分代码
}


SpringManagedTransactionFactory 作为事务工厂创建了一个类型为 SpringManagedTransaction,从名字也可以看出,这是一个 Spring 管理的事务对象,核心代码如下。


public class SpringManagedTransaction implements Transaction {
    @Override
    public Connection getConnection() throws SQLException {
        if (this.connection == null) {
            openConnection();
        }
        return this.connection;
    }
    private void openConnection() throws SQLException {
        this.connection = DataSourceUtils.getConnection(this.dataSource);
        this.autoCommit = this.connection.getAutoCommit();
        this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
    }
}


MyBatis 调用了DataSourceUtils#getConnection 获取 Connection,这是使用 Spring 事务管理的关键,Spring 会把事务相关的资源存储到 ThreadLocal,而 DataSource 就是其中一个 key,DataSourceUtils#getConnection 可以使用相同的 DataSource 从 ThreadLocal 中获取到 Spring 事务管理存储的 Connection,从而加入 Spring 的事务中。关于 Spring 事务管理,你还可以参考《如何正确打开 Spring 事务?》这篇文章进行了解。


线程安全的 SqlSessionTemplate 是如何实现的?


SqlSessionFactoryBean 的作用主要用来替代 SqlSessionFactoryBuilder 构建支持 Spring 事务的 SqlSessionFactory,SqlSessionTemplate 则是用来替代 SqlSession 保证线程安全,那它是如何实现的呢?先看其类定义。


public class SqlSessionTemplate implements SqlSession, DisposableBean {
}


SqlSessionTemplate 实现了 SqlSession 接口,因此可以说 SqlSessionTemplate 就是 SqlSession,查看其中一个实现方法如下。

public class SqlSessionTemplate implements SqlSession, DisposableBean {
    private final SqlSession sqlSessionProxy;
    @Override
    public <E> List<E> selectList(String statement) {
        return this.sqlSessionProxy.selectList(statement);
    }
}    


可以看出 SqlSessionTemplate 把实委托给了底层持有的 SqlSession,因此我们可以猜想,这个底层的 SqlSession 应该是线程安全的,那它是怎么实现线程安全的呢?先看看这个 SqlSession 从哪来的。


public class SqlSessionTemplate implements SqlSession, DisposableBean {
    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
                              PersistenceExceptionTranslator exceptionTranslator) {
        ...省略部分代码
        this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
            new Class[]{SqlSession.class}, new SqlSessionInterceptor());
    }
}


可以看到底层的 SqlSession 是 SqlSessionTemplate 在实例化时通过代理创建的,使用了 SqlSessionInterceptor 拦截方法的执行,那我们再跟踪 SqlSessionInterceptor。


public class SqlSessionTemplate implements SqlSession, DisposableBean {
    private class SqlSessionInterceptor implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
                SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
            try {
                Object result = method.invoke(sqlSession, args);
                if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
                    sqlSession.commit(true);
                }
                return result;
            } catch (Throwable t) {
                ... 省略部分代码
            } finally {
                if (sqlSession != null) {
                    closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                }
            }
        }
    }
}


SqlSessionInterceptor 是 SqlSessionTemplate 的内部类,它获取 SqlSession 的实例后再调用其方法,SqlSessionInterceptor 获取的 SqlSession 又有何特殊之处呢?


public final class SqlSessionUtils {
    public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
                                           PersistenceExceptionTranslator exceptionTranslator) {
        // 优先从 ThreadLocal 中获取
        SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
        SqlSession session = sessionHolder(executorType, holder);
        if (session != null) {
            return session;
        }
        // ThreadLocal 中不存在 SqlSession,新创建一个
        session = sessionFactory.openSession(executorType);
        // 尝试将 SqlSession 存放到 ThreadLocal 中
        registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
        return session;
    }
}


这里发现 MyBatis 将 SqlSessionFactory 作为参数调用了 Spring 的TransactionSynchronizationManager#getResource,这里 MyBatis 将 SqlSessionHolder 作为资源存储到了 ThreadLocal 中,这个 SqlSessionFactory 则是前面我们提到的 SqlSessionFactoryBean 生成的,通过 ThreadLocal 保证了 SqlSessionTemplate 的线程安全。


总结如下:


SqlSessionTemplate 实现接口 SqlSession,使用持有的 SqlSessionFactoryBean 生成的 SqlSessionFactory 创建了 SqlSession 的代理,使用这个代理实现各个方法。

SqlSession 代理利用 SqlSessionFactory 获取支持 Spring 事务的 SqlSession,并将 SqlSession 作为资源保存到 ThreadLocal 中,保证了线程安全。

Spring 环境下的 Mapper 接口底层是如何实现的?

对于单个 Mapper 接口的 Spring bean 注入,MyBatis 提供了一个 MapperFactoryBean 类,这个类是 Spring 的一个 FactoryBean,由这个类创建对应 Mapper 接口实现。关键代码如下。


public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
    @Override
    public T getObject() throws Exception {
        return getSqlSession().getMapper(this.mapperInterface);
    }
}


Mapper 接口还是通过 SqlSession 获取,其中#getSqlSession 方法由父类 SqlSessionDaoSupport 提供,跟踪实现如下。


public abstract class SqlSessionDaoSupport extends DaoSupport {
    private SqlSessionTemplate sqlSessionTemplate;
    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
            this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
        }
    }
    public SqlSession getSqlSession() {
        return this.sqlSessionTemplate;
    }
}


可以看到 Mapper 最终由根据 SqlSessionFactory 创建的 SqlSessionTemplate 获取,因此可以认为由 MapperFactoryBean 创建的 Mapper 接口实例和我们直接通过 SqlSessionTemplate 并无差别,只是 MyBatis 做了小小的封装,避免了我们手动创建 SqlSessionTemplate 实例而已。


MyBatis @MapperScan 如何扫描 Mapper 接口的?


MyBatis 虽然提供了 MapperFactoryBean 用于创建 Mapper 接口的实例作为 bean,但是如果 Mapper 接口过多,那么配置的工作量将大大增大,为了减少对 Mapper 接口的配置,MyBatis 又提供了一个 @MapperScan 注解,将它添加到配置类后 MyBatis 会扫描 Mapper 接口并自动向 Spring 注入 bean。注解定义如下。


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
    String[] value() default {};
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
    Class<? extends Annotation> annotationClass() default Annotation.class;
    Class<?> markerInterface() default Class.class;
    String sqlSessionTemplateRef() default "";
    String sqlSessionFactoryRef() default "";
    Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;
    String lazyInitialization() default "";
    String defaultScope() default AbstractBeanDefinition.SCOPE_DEFAULT;
}


@MapperScan 注解上添加了 @Import 注解,这是实现自动注入 bean 的关键,Spring 中 @Enable* 注解的实现大多如此,如果你感兴趣,还可以参阅《Spring 框架中的 @Enable* 注解是怎样实现的?》。


我们继续跟踪 @Import 的参数 MapperScannerRegistrar.class 源码,关键代码如下。


public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AnnotationAttributes mapperScanAttrs = AnnotationAttributes
            .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
        if (mapperScanAttrs != null) {
            registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
                generateBaseBeanName(importingClassMetadata, 0));
        }
    }
    void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
                                 BeanDefinitionRegistry registry, String beanName) {
        // 注册 MapperScannerConfigurer 类型的 bean,使该 bean 继续扫描包注册 mapper 为 bean
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
        ...省略属性设置代码
        registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
    }
}


MapperScannerRegistrar 拿到 @MapperScan 的参数后向 Spring 注册了一个类型为 MapperScannerConfigurer 的 bean,并将 @MapperScan 的参数设置到这个 bean 的属性中,那这个新注册的 bean 有何特殊之处呢?


public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    ... 省略部分代码
        ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
        scanner.setAddToConfig(this.addToConfig);
        scanner.setAnnotationClass(this.annotationClass);
        scanner.setMarkerInterface(this.markerInterface);
        scanner.setSqlSessionFactory(this.sqlSessionFactory);
        scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
        scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
        scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
        scanner.setResourceLoader(this.applicationContext);
        scanner.setBeanNameGenerator(this.nameGenerator);
        scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
        if (StringUtils.hasText(lazyInitialization)) {
            scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
        }
        if (StringUtils.hasText(defaultScope)) {
            scanner.setDefaultScope(defaultScope);
        }
        scanner.registerFilters();
        scanner.scan(
            StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
    }
}


MapperScannerConfigurer 是一个 BeanDefinitionRegistryPostProcessor,这是一个特殊的 BeanFactoryPostProcessor,会在 Spring 应用上下文的生命周期中回调其方法,回调方法使用 ClassPathMapperScanner 向 Spring 注入了 Mapper 作为 bean。可以看到,ClassPathMapperScanner 保存了 MapperFactoryBean 所需的信息,据此创建 Mapper 接口的代理对象。


总结如下:

@MapperScan 注入了一个类型为 MapperScannerConfigurer 的 bean,这个 bean 在应用上下文的生命周期回调中扫描包并使用 MapperFactoryBean 创建了 Mapper 接口的代理并向 Spring 注入了 bean。


SpringBoot 环境下 MyBatis 如何做自动化配置的?


mybatis-spring 项目添加了 spring 事务的支持,但是需要手动进行一些配置才可以在 spring 中使用,为了进一步简化 mybatis 在 springboot 项目中的使用,mybatis 又开发了一个 mybatis-spring-boot-starter 项目,这个项目充分利用了 springboot 自动化配置的特性,引入这个项目之后,我们可以在代码里直接注入 Mapper 接口,大大方便了 mybatis 的用户,下面我们来看它是如何实现的。


下载 mybatis-spring-boot 项目源码后,可以发现,mybatis-spring-boot-starter 是一个空项目。


image.png


在 pom 文件中,mybatis 引入了一个 mybatis-spring-boot-autoconfigure 模块,这是实现自动化配置的原因所在。


image.png


mybatis-spring-boot-autoconfigure 模块中 META-INF 目录下的 spring.factories 文件中指定了 MybatisAutoConfiguration 作为自动化的配置。


@org.springframework.context.annotation.Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean {
    public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,
                                    ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider,
                                    ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider,
                                    ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
        this.properties = properties;
        this.interceptors = interceptorsProvider.getIfAvailable();
        this.typeHandlers = typeHandlersProvider.getIfAvailable();
        this.languageDrivers = languageDriversProvider.getIfAvailable();
        this.resourceLoader = resourceLoader;
        this.databaseIdProvider = databaseIdProvider.getIfAvailable();
        this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
    }
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
      ...省略实现代码
    }
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
      ...省略实现代码
    }
}


MybatisAutoConfiguration 注入了 Spring 容器中 MyBatis 相关的组件,然后为我们自动配置了 SqlSessionFactory、SqlSessionTemplate 作为 bean。


除此之外,MybatisAutoConfiguration 中还包含两个静态类用于 Mapper 的扫描注册。


    @org.springframework.context.annotation.Configuration
    @Import(AutoConfiguredMapperScannerRegistrar.class)
    @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
    public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
    ...省略不重要代码
    }
    public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {
        private BeanFactory beanFactory;
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
      ...省略部分代码
            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
            ... 省略属性配置代码
            registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
        }
        @Override
        public void setBeanFactory(BeanFactory beanFactory) {
            this.beanFactory = beanFactory;
        }
    }    


通过上面的代码可以看到 MybatisAutoConfiguration 内部注入了 MapperScannerConfigurer 作为 bean,从而进一步注册 Mapper 接口作为 bean。


可以看出 mybatis-spring-boot-starter 对 mybatis 的配置和我们自己的配置基本是一致的,只是 mybatis 帮我们做了自动化的配置。


总结

MyBatis 作为一个独立的项目,从适配 springframework 到 springboot 自动化配置,可以说在不断简化使用方式,我们开发项目时借鉴 mybatis 是个不错的做法,也可以看出,开源框架的设计确实值得我们学习。


目录
相关文章
|
1天前
|
Dubbo Java 应用服务中间件
Dubbo 第四节: Spring与Dubbo整合原理与源码分析
DubboConfigConfigurationRegistrar的主要作⽤就是对propties⽂件进⾏解析并根据不同的配置项项⽣成对应类型的Bean对象。
|
4天前
|
IDE Java 应用服务中间件
基于Spring+mybatis的SSM超市消费积分管理系统代码实现含演示站
这是一个SSM超市消费积分管理系统。有2个角色:买家角色和管理员角色,现在开始分角色介绍下功能。买家角色核心功能有买家登录,查看网站首页,查看蔬菜详情,加入购物车,提交订单,查看我的订单。管理员角色核心功能有管理员登录,用户管理,管理员管理,商品管理,一级分类管理,二级分类管理,订单管理。更多的功能可以去演示站查看。
基于Spring+mybatis的SSM超市消费积分管理系统代码实现含演示站
|
15天前
|
设计模式 Java 测试技术
Spring依赖注入的魔法:深入DI的实现原理【beans 五】
Spring依赖注入的魔法:深入DI的实现原理【beans 五】
42 0
|
19天前
|
缓存 Java uml
SpringBoot2 | Spring AOP 原理深度源码分析(八)
SpringBoot2 | Spring AOP 原理深度源码分析(八)
31 0
|
19天前
|
Java 应用服务中间件 数据库连接
Spring5源码(51)-Servlet知识点回顾以及SpringMVC分析入口
Spring5源码(51)-Servlet知识点回顾以及SpringMVC分析入口
20 0
|
20天前
|
NoSQL Java 测试技术
字节二面:Spring Boot Redis 可重入分布式锁实现原理?
字节二面:Spring Boot Redis 可重入分布式锁实现原理?
94 0
|
21天前
|
Java Spring
Spring AOP之MethodInterceptor原理
Spring AOP之MethodInterceptor原理
43 0
|
22天前
|
设计模式 监控 Java
【Spring】Spring AOP原理
【Spring】Spring AOP原理
|
23天前
|
Java Spring
Spring5深入浅出篇:Spring工厂简单原理以及日志应用
Spring5深入浅出篇:Spring工厂简单原理以及日志应用
|
25天前
|
缓存 Java 数据库连接
微服务框架(六)Spring Boot集成Mybatis及Druid
  此系列文章将会描述Java框架Spring Boot、服务治理框架Dubbo、应用容器引擎Docker,及使用Spring Boot集成Dubbo、Mybatis等开源框架,其中穿插着Spring Boot中日志切面等技术的实现,然后通过gitlab-CI以持续集成为Docker镜像。   本文为Spring Boot集成Mybatis,包括mybatis-generator的使用

相关产品

  • 云迁移中心