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 是一个空项目。
在这里插入图片描述
在 pom 文件中,mybatis 引入了一个 mybatis-spring-boot-autoconfigure 模块,这是实现自动化配置的原因所在。
在这里插入图片描述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 是个不错的做法,也可以看出,开源框架的设计确实值得我们学习。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring项目和Spring Boot项目的区别是什么? MyBatis在这两种项目中的使用方式又有何不同? 纯Spring项目是指使用Spring框架进行开发的项目。在纯Spring项目中,需要手动配置和管理各个组件的依赖关系和配置文件。开发人员需要编写繁琐的配置代码来整合Spring MVC和MyBatis。同时,纯Spring项目需要手动配置和管理应用的部署和运行环境。 而Spring Boot是基于Spring框架的快速开发工具,它简化了Spring项目的配置和部署过程。在Spring Boot项目中,开发人员只需要添加相关依赖,并使用注解来配置和管理组件之间的依赖关系。Spring Boot提供了自动配置和默认配置,大大减少了开发人员的配置工作量。此外,Spring Boot还提供了内嵌服务器,可以方便地进行项目的打包和部署。 对于MyBatis的使用方式,在纯Spring项目中,需要手动配置MyBatis的相关配置文件和Bean。而在Spring Boot项目中,可以直接使用Spring Boot提供的自动配置功能,只需添加相应的依赖即可。同时,Spring Boot还提供了MyBatis的集成插件,可以方便地进行数据库访问和持久化操作。 总的来说,纯Spring项目需要手动配置和管理各个组件的依赖关系和配置文件,而Spring Boot大大简化了项目的配置和部署过程。在MyBatis的使用上,纯Spring项目需要手动配置和管理相关的配置文件和Bean,而Spring Boot提供了自动配置和集成插件,简化了MyBatis的使用方式。<span class="em">1</span><span class="em">2</span> #### 引用[.reference_title] - *1* [Spring整合MybatisSpringBoot整合Mybatis原理分析](https://download.csdn.net/download/sinat_37914371/85154013)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [基于SpringBoot + MyBatis + Layui的后台权限管理系统.zip](https://download.csdn.net/download/weixin_55305220/85958960)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

大鹏cool

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

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

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

打赏作者

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

抵扣说明:

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

余额充值