Mybatis扩展之通用Mapper源码解析
这个组件是最近才开始真正去了解的,很早之间就关注了作者的微信公众号和QQ群,但抱着用上再看的原则,所以拖延到了现在。
1. 配置
我们先来看看相关配置,可以说是相当简单了
<!-- 通用Mapper配置, 单表操作零SQL -->
<!-- 与mybatis-spring.jar中同名类, 唯一区别是完整命名, 其首位由org更换为tk -->
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 指示满足通用Mapper契约的定义接口所在的package, 可以配置多个 -->
<property name="basePackage" value="com.kanq.train.dao" />
<!-- 指定所关联的MyBatis SqlSessionFactory, 如果不设置将交由Spring自动注入-->
<!-- 所以如果你有多个数据源, 就需要如下配置来显式指定, 否则会报错-->
<property name="sqlSessionFactoryBeanName" value="mainSqlSessionFactory" />
<!-- 以下配置属性的最终使用者是 通用Mapper的核心类 MapperHelper -->
<!-- 感兴趣的读者可以参见其内部定义的 MapperHelper.setProperties() 方法-->
<property name="properties">
<value>
mappers=com.xxx.yyy.core.Mapper
notEmpty=false
IDENTITY=ORACLE
</value>
</property>
</bean>
2. 解读
- 通观以上的配置,毫无疑问入口就是通用Mapper自定义的那个
MapperScannerConfigurer
了。 - 而观察自定义
MapperScannerConfigurer
的实现我们可以发现,逻辑主要位于其所实现的接口BeanDefinitionRegistryPostProcessor
中定义的postProcessBeanDefinitionRegistry()
方法内——通过自定义的ClassPathMapperScanner
类将用户配置的basePackage
属性指示的package中的指定类型扫描进行Spring容器中。 - 就像自定义
MapperScannerConfigurer
的直接借鉴自mybatis-spring中的同名类一样 ,自定义的ClassPathMapperScanner
类同样是直接借鉴自mybatis-spring中的同名类;并选择覆写了基类的doScan(String... basePackages)
方法。 - 对于自定义的
ClassPathMapperScanner
类覆写的doScan(String... basePackages)
方法,核心逻辑就是将扫描到的用户指定的basePackage
下的每个接口的Bean定义,将其beanClass属性更换为自定义的MapperFactoryBean<T>
类型(注意这个类型的由来和上面的ClassPathMapperScanner
和MapperScannerConfigurer
类似),以及为该MapperFactoryBean<T>
实例注入相应的依赖(例如关键性的MapperHelper
实例字段等等)。 - 关于上一步的扫描逻辑,还有一个细节就是自定义的
ClassPathMapperScanner
类还覆写了基类的isCandidateComponent(AnnotatedBeanDefinition beanDefinition)
方法,声明只有接口才可能满足条件。所以其实这里声明一个空的标志性接口也是可以被扫描进去的。 - 接下来的关注点就是
MapperFactoryBean<T>
,通观其继承链我们可以发现两个关键性接口InitializingBean
,FactoryBean<T>
。
a. 接口InitializingBean
,这个间接实现的Spring接口,相关的逻辑位于覆写的checkDaoConfig()
方法,在此覆写方法中,将回调Mybatis中的configuration.addMapper(this.mapperInterface);
方法,这将最终导致ProviderSqlSource
的构造(关于这个在通用Mapper起到关键作用的类这里就不赘述了,作者的文章做过专门讲解)。
b. 接口InitializingBean
中完成的另外一个关键逻辑正是通用Mapper实现的关键——通过替换掉上面生成MappedStatement
实例中的所有ProviderSqlSource
实例,来完成关键性的准备工作(mapperHelper.processConfiguration(getSqlSession().getConfiguration(), this.mapperInterface);
)。
c. 关于FactoryBean<T>
接口,其对Spring容器的意义不用多说了,在这里的目的也是一样的。 - 以上替换SqlSource的操作,正是给予通用Mapper大展拳脚空间的关键。通用Mapper正是在此替换操作之前,回调一系列的约定下的实现者来动态获取相应的作为替换者的真正SqlSource(
MapperTemplate.setSqlSource(MappedStatement ms)
方法)。
相关时序图如下:
- 替换为
MapperFactoryBean<T>
类型 - 构建出ProviderSqlSource实例,并替换为我们自定义的SqlSource实例。
3. 实例
上面扯完了这么多源码相关的内容,对于初次接触到的读者难免会觉得晦涩难懂。所以接下来我将以一个实际的例子来说明一些使用者关心的细节。
下面这个OracleProvider
类是笔者从通用Mapper4.1.5版本中随便挑出来的。
/**
* @description: Oracle实现类
* @author: qrqhuangcy
* @date: 2018-11-15
**/
// ====== 直接继承自MapperTemplate, 这也满足MapperTemplate类上的注释说明
public class OracleProvider extends MapperTemplate {
// 构造函数参数的参数声明, 可以参考框架中的 MapperHelper.fromMapperClass()中的逻辑
// 正是在 MapperHelper.fromMapperClass() 中实例化了本类, 以填充自身内部的methodMap 字段(该methodMap 字段将在替换sqlSource时候生效) , 例如对于本类将注册insertList方法
public OracleProvider(Class<?> mapperClass, MapperHelper mapperHelper) {
super(mapperClass, mapperHelper);
}
// 该方法将被框架使用反射方式进行回调, 参见MapperTemplate.setSqlSource(MappedStatement ms)
// 允许的方法参数只有MappedStatement
// 允许的返回值有Void, SqlNode, String, 而这些返回值正是真正执行的SQL相关内容
public String insertList(MappedStatement ms){
......
}
}
看完了OracleProvider
类的定义,接下来我们再看看其应用,按照通用Mapper的设计思路,我们还需要一个接口承载该类:
@tk.mybatis.mapper.annotation.RegisterMapper
public interface InsertListMapper<T> {
/**
* <p>生成如下批量SQL:
* <p>INSERT ALL
* <p>INTO demo_country ( country_id,country_name,country_code ) VALUES ( ?,?,? )
* <p>INTO demo_country ( country_id,country_name,country_code ) VALUES ( ?,?,? )
* <p>INTO demo_country ( country_id,country_name,country_code ) VALUES ( ?,?,? )
* <p>SELECT 1 FROM DUAL
*
* @param recordList
* @return
*/
// 该注解也就是导致ProviderSqlSource实例生成的关键
@InsertProvider(type = OracleProvider.class, method = "dynamicSQL")
int insertList(List<? extends T> recordList);
}
以上,所有的扩展准备工作就算是完成了,接下来要做的就是按照自己的需求继承上面的InsertListMapper<T>
接口,然后你就自动拥有了InsertList的能力。
本小节最后,总结下自定义扩展时候的注意事项吧:
- 自定义的扩展接口(例如上面的
InsertListMapper<T>
),方法签名的定义没有注意事项,只需要注意方法上注解的method
参数值必须为dynamicSQL
。 - 上述注解中,其另外一个
type
参数所指示的自定义类型,该类型必须有这样一个方法:
a. 与该接口方法同名的,
b. 方法参数为MappedStatement
类型,
c. 返回值为void
,String
,SqlNode
中的一种。 - 更多的细节还是参见作者本人的扩展通用接口 文档吧。
4. 总结
-
MapperScannerConfigurer
和ClassPathMapperScanner
配合将可能满足条件的全部接口全部扫描入容器,这些接口在Spring容器内部将以自定义MapperFactoryBean<T>
的形式存在。 - 自定义
MapperFactoryBean<T>
因为间接实现的Spring中的InitializingBean
接口,使得有机会在系统初始化过程中完成底层MappedStatement
实例中的SqlSource
字段值的替换。 - 在
MapperScannerConfigurer
构建的MapperHelper
实例是全局唯一的。这也符合重量级的服务域应该是全局唯一的最佳实践。 - 上面提到的
MapperHelper
实例在通用Mapper的关键类MapperFactoryBean<T>
,MapperTemplate
类中都相应的实例字段,这和Mybatis内部的Configuration
实例地位相当类似,类似《程序员修炼之道–从小工到专家》中"黑板"的概念。
5. 结尾
其实最让笔者感慨的是,任何一件事,哪怕再小,只要肯钻研,其中必然有着无数可以完善的细节,而正是这些细节的追求,让人变得优秀。通用Mapper的作者从2014年的最初版本坚持演化到现在,这份坚持的精神,以及精益求精的态度真的很让人动容。