系列文章
第一节 从零开始手写 mybatis(一)MVP 版本 中我们实现了一个最基本的可以运行的 mybatis。
第二节 从零开始手写 mybatis(二)mybatis interceptor 插件机制详解
第三节 从零开始手写 mybatis(三)jdbc pool 从零实现数据库连接池
第四节 从零开始手写 mybatis(四)mybatis 事务机制详解
本节我们一起来学习下如何实现 mybatis 与 spring 的整合。
前言
很多人都是用 spring 整合 mybatis,但是对于其实现原理很少做探究。
本文一起来学习一下 mybatis 整合 spring 的原理。
长文预警:本文内容较多,建议收藏后再看。
关注的公众号讲技术的越来越少,讲解底层原理的更少,但是个人还是希望可以看到一点真正有价值的东西。
如果没有人来做,那就让我来做吧。
个人的技术博客,因为能力有限,大部分都是挺枯燥的,但是技术原理本身往往如此,乐趣应该来自于对于技术的运用。
本次以 mybatis-spring v1.3.1 版本为例,简单的看一下源码实现。主要是对于实现流程的梳理,引导如何阅读源码,方法比内容更加重要,愿你有所收获。
带着问题学习
SqlSessionFactory,SqlSession 如何生成?
Mapper 代理如何生成?如何运行?
配置示例
闲话休说,上代码!
为了便于大家理解,此处给出一个配置的例子作为参考。
MainDataSourceConfig.java
mybatis 与 mybatis-plus 的配置差异
这里是 mybatis-plus 的配置示例,mybatis 差别不大。主要是在配置中,将
MybatisSqlSessionFactoryBean
替换为
SqlSessionFactoryBean
。
SqlSessionFactory 是如何生成的?
SqlSessionFactory 这个方法中,我们设置了数据源,实体别名,插件,mapper 位置等等。
最后通过 sqlSessionFactoryBean.getObject() 获取。让我们一起来探究一下源码。
接口实现
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent>
其中 getObject() 就是对应的 FactoryBean.getObject() 的实现。
基本属性
一些基本属性,和上面看到的指定配置都是对应的。
这些类在前面 mybatis 手动实现中都提到过。
我们来看一看重头戏,getObject() 是如何实现的。
getObject()
- getObject()
这里调用了 afterPropertiesSet(),这个方法会在 spring 的配置初始化完成之后,实现我们自己的处理。
- afterPropertiesSet()
前面几句都是一些参数和状态的校验,我们看一下 buildSqlSessionFactory() 实现。
- buildSqlSessionFactory() 源码
这部分源码比较多,但是实际上做的事情还是比较简单的。
- 主要是构建 Configuration 中的各种配置,变量、ObjectFactory、别名、插件、类型转换处理器、databaseIdProvider、cache 等
- transactionFactory 事务管理器默认使用 spring 的事务管理器;设置环境信息。
- 对于 mapper 进行解析处理。
SqlSessionFactoryBuilder 的实现
这里默认使用的是:
所以实现的原理和 mybatis 就是一样了,此处不再深入。
SqlSession 如何获取
当我们拿到 SqlSessionFactory 之后,自然就可以创建 SqlSession 了。
SqlSessionTemplate 这个类就是 spring 与 mybatis 的一座桥梁。
SqlSessionTemplate 配置
回想一下我们的配置如下:
当然这个属性时机可以不做配置,文章后面会讲解。
我们先看一下这个实现。
接口
public class SqlSessionTemplate implements SqlSession, DisposableBean
这里就实现了 mybatis 中的 SqlSession 接口。
至于 DisposableBean 接口,则是 spring 框架中的,用于对于 bean 的销毁生命周期维护。
基本属性
构造器
我们配置中,直接使用了 SqlSessionFactory 进行对象创建。
对应实现:
实际做的就是对几个基本属性做一下初始化,最后实际的构造器如下:
SqlSession 方法实现
SqlSessionTemplate 实现了 SqlSession 接口,那么具体实现是怎么样的呢?
实际上所有的实现基本都是下面这个样子:
@Override
public T selectOne(String statement) {return this.sqlSessionProxy. selectOne(statement);
}
不难发现,主要是基于 sqlSessionProxy 实现的。
而 sqlSessionProxy 在上面的构造器中我们可以看到,是一个 jdk 动态代理。重点在于 SqlSessionInterceptor 这个类。
SqlSessionInterceptor
作为一个合格的动态代理方法,自然也就实现了 InvocationHandler 接口。
(1)getSqlSession 获取 session
这里一开始,通过 SqlSessionUtils 工具类,获取一个 session。
整体而言还是不难的,直接通过 sessionFactory.openSession(executorType) 获取,此处不再展开。
(2)执行原本的方法
此处就是直接反射调用
(3)事务验证
看一下是否有 spring 的事务管理,如果没有,直接 commit。
具体的事务管理,可以参考以前的文章。
mybatis 事务管理机制详 https://www.jianshu.com/p/f6ed628b37e6
(4)session 的关闭
作为一个用完就归还资源的好同志,最后需要将打开的 session 进行关闭。
通过 SqlSessionUtils 工具类,关闭一个 session。
到这里,我们就解决了开始的 2 个疑问:
- 如何构建 SqlSessionFactory?
- 如何获取 sqlSession
下面让我们看一下 mybatis-spring 整合的另一个精华部分,mapper 代理的处理。
Mapper 代理如何生成?如何运行?
mybatis 中的 mapper
在以前实现 mybatis 中,我们都知道 mapper 是基于动态代理,让接口和 xml 文件中的 sql 相互对应。
整合 spring 之后,我们只需要下面这样:
就可以像其他 ioc 中的 bean 一样,使用 mapper 方法了。
那么,是如何实现的呢?
扫描 mapper
配置中有一个扫描的注解:
这个注解告诉 spring 去指定包中扫描 mapper 方法。
我们首先来看一下这个注解的构成。
注解定义
朴实无华的注解定义,可以放在类上。
其中的各个参数,便于我们灵活的配置。
这里有一个
@Import(MapperScannerRegistrar.class)
属性成功的引起了我的注意,我们一起来看一下里面有什么玄机。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
public @interface MapperScan {
/**
* Alias for the {@link #basePackages()} attribute. Allows for more concise
* annotation declarations e.g.:
* {@code @EnableMyBatisMapperScanner("org.my.pkg")} instead of {@code
* @EnableMyBatisMapperScanner(basePackages= "org.my.pkg"})}.
*/
String[] value() default {};
/**
* Base packages to scan for MyBatis interfaces. Note that only interfaces
* with at least one method will be registered; concrete classes will be
* ignored.
*/
String[] basePackages() default {};
/**
* Type-safe alternative to {@link #basePackages()} for specifying the packages
* to scan for annotated components. The package of each class specified will be scanned.
*
Consider creating a special no-op marker class or interface in each package
* that serves no purpose other than being referenced by this attribute.
*/
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;
}
MapperScannerRegistrar.java
这个看名字也比较好理解,mapper 扫描的注册类。
接口
这两个接口都是 spring 中的,有一个属性 ResourceLoader。
ImportBeanDefinitionRegistrar 接口中的 registerBeanDefinitions 方法比较重要,spring 容器在启动的时候,会调用该方法。
registerBeanDefinitions 方法
这个内容还是比较多的,不过都看到这里了,无所畏惧。
其实看起来一大堆,但是做的事情还是比较清晰的。
主要是以下几步:
- 注解元信息的获取
- 基本属性的设置
- 构建扫描包信息
- 注册过滤器
- 进行包扫描
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//1. 注解元信息的获取
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
//2. 基本属性的设置
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// this check is needed in Spring 3.1
if (resourceLoader != null) {
scanner.setResourceLoader(resourceLoader);
}
Class extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
if (!Annotation.class.equals(annotationClass)) {
scanner.setAnnotationClass(annotationClass);
}
Class> markerInterface = annoAttrs.getClass("markerInterface");
if (!Class.class.equals(markerInterface)) {
scanner.setMarkerInterface(markerInterface);
}
Class extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
if (!BeanNameGenerator.class.equals(generatorClass)) {
scanner.setBeanNameGenerator(BeanUtils.instantiateClass(generatorClass));
}
Class extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
scanner.setMapperFactoryBean(BeanUtils.instantiateClass(mapperFactoryBeanClass));
}
scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));
//3. 构建扫描包信息
List basePackages = new ArrayList();for (String pkg : annoAttrs.getStringArray("value")) {if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}for (String pkg : annoAttrs.getStringArray("basePackages")) {if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}for (Class> clazz : annoAttrs.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}//4. 注册过滤器
scanner.registerFilters();//5. 实现扫描
scanner.doScan(StringUtils.toStringArray(basePackages));
}
最核心的就是最后一个方法,我们来看一下到底是如何实现扫描的。
doScan 包扫描实现
@Override
public Set doScan(String... basePackages) {
// spring 中的扫描方法
Set 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;
}
- processBeanDefinitions 实现
我们来看一下这里的
processBeanDefinitions(beanDefinitions)
,内容也有点多。
该方法会将制定包下的 Mapper 接口改成 mapperFactoryBean 的类型,Spring getBean() 返回的就是 mapperFactoryBean 类型。
循环了扫描到的所有 mapper,主要做了三件事情:
没错,我来鹅城只做三件事!
第一:改写 mapper 类型为 mapperFactoryBean;
第二:添加属性,addToConfig、sqlSessionFactory、sqlSessionTemplate;
第三:设置按照类型进行自动装配。
mapperFactoryBean 是什么?
我们做了这么多,构建了一个 mapperFactoryBean,那这个 bean 到底是什么?
这里类是在 mybatis-spring 包中定义的。
接口
这里继承了 SqlSessionDaoSupport 类,并且实现了 spring 的 FactoryBean 接口。
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
private Class mapperInterface;private boolean addToConfig = true;public MapperFactoryBean() {//intentionally empty
}public MapperFactoryBean(Class mapperInterface) {this.mapperInterface = mapperInterface;
}
}
获取 mapper 真正的实现
FactoryBean 是 spring 的接口,bean 工厂接口。
主要就是如何获取一个 getObject() 方法。
那么,mybatis-spring 是如何实现的呢?
其实看到这里,就可以和 mybatis 完全连上了。
那么 mapperInterface 是怎么来的?
这个实际上是构造器传入的:
至于这个构造器什么时候被调用的,实际上就是在扫描包的时候设置的。
processBeanDefinitions() 中的这句话:
读到这里,还是要赞叹一句的,设计的确实比较巧妙。
SqlSessionDaoSupport 做了什么?
这个类继承了 spring 的 DaoSupport,主要就是处理一下 SqlSession 的构建,以及 getSqlSession() 方法。
接口继承关系如下:
DaoSupport 的实现
这个是 spring 的实现,所以会自动调用 afterPropertiesSet() 方法。
我们的 MapperFactoryBean 中实际上实现了 checkDaoConfig(),做一下配置的处理:
其实做的事情就是,如果配置了 addToConfig 而且 getConfiguration 没有 mapper 接口信息的配置,就将 mapperInterface 属性设置到 configuration 中。
SqlSessionDaoSupport 的核心代码
其实比较简单,但是非常的重要。
这里有 SqlSessionFactory 和 SqlSessionTemplate 的属性设置,回想起我们扫包的属性设置,其实调用的就是这里。
会从 spring bean 工厂中获取对应的实现,如果没有指定 sqlSessionFactory 其实也可以,实际上会默认帮我们设置一个。
看了这里,spring 与 mybatis 整合的核心代码基本已经梳理完了。
我们来做一个整体流程的回顾。
Mapper 的创建流程
(1)根据
@MapperScan
注解,进行指定包的 Mapper 接口扫描。
将所有的 Mapper 接口的 Bean 定义都改成 FactoryBean 的子类 MapperFactoryBean,并将该 SqlSessionFactory 和 SqlSessionTemplate 属性添加到该类中。
(2)IOC 在实例化该 Bean 的时候,需要传入接口类型,并将 SqlSessionFactory 和 SqlSessionTemplate 注入到该 Bean 中。并调用 configuration 的 addMapper 方法,解析配置文件。
(3)当调用 MapperFactoryBean 的 getObject() 方法的时候,事实上是调用 SqSession.getMapper() 方法,而这个方法会返回一个动态代理对象。所有对这个对象的方法调用都是底层的 SqlSession 的方法。
小结
其实写到这里,mybatis 系列应该是告一段落了。
springboot 整合的实现并不难,但是可以提供很大的便利性,如果有机会的话,可以开一篇讲解下如何实现 springboot 的自动配置。
与大师的对话总是这样,仰之弥高,钻之弥坚。
让源码飞一会儿~
如果对你有帮助,也欢迎点个赞,鼓励一下作者。
从零实现 mybatis 开源地址:https://github.com/houbb/mybatis