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. 解读

  1. 通观以上的配置,毫无疑问入口就是通用Mapper自定义的那个MapperScannerConfigurer了。
  2. 而观察自定义MapperScannerConfigurer的实现我们可以发现,逻辑主要位于其所实现的接口BeanDefinitionRegistryPostProcessor中定义的postProcessBeanDefinitionRegistry()方法内——通过自定义的ClassPathMapperScanner类将用户配置的basePackage属性指示的package中的指定类型扫描进行Spring容器中。
  3. 就像自定义MapperScannerConfigurer的直接借鉴自mybatis-spring中的同名类一样 ,自定义的ClassPathMapperScanner类同样是直接借鉴自mybatis-spring中的同名类;并选择覆写了基类的doScan(String... basePackages)方法。
  4. 对于自定义的ClassPathMapperScanner类覆写的doScan(String... basePackages)方法,核心逻辑就是将扫描到的用户指定的basePackage下的每个接口的Bean定义,将其beanClass属性更换为自定义的MapperFactoryBean<T>类型(注意这个类型的由来和上面的ClassPathMapperScannerMapperScannerConfigurer类似),以及为该MapperFactoryBean<T>实例注入相应的依赖(例如关键性的MapperHelper实例字段等等)。
  5. 关于上一步的扫描逻辑,还有一个细节就是自定义的ClassPathMapperScanner类还覆写了基类的isCandidateComponent(AnnotatedBeanDefinition beanDefinition)方法,声明只有接口才可能满足条件。所以其实这里声明一个空的标志性接口也是可以被扫描进去的。
  6. 接下来的关注点就是MapperFactoryBean<T>,通观其继承链我们可以发现两个关键性接口InitializingBeanFactoryBean<T>
    a. 接口InitializingBean,这个间接实现的Spring接口,相关的逻辑位于覆写的checkDaoConfig()方法,在此覆写方法中,将回调Mybatis中的configuration.addMapper(this.mapperInterface);方法,这将最终导致ProviderSqlSource的构造(关于这个在通用Mapper起到关键作用的类这里就不赘述了,作者的文章做过专门讲解)。
    回调InitializingBean
    b. 接口InitializingBean中完成的另外一个关键逻辑正是通用Mapper实现的关键——通过替换掉上面生成MappedStatement实例中的所有ProviderSqlSource实例,来完成关键性的准备工作(mapperHelper.processConfiguration(getSqlSession().getConfiguration(), this.mapperInterface);)。
    偷天换日
    c. 关于FactoryBean<T>接口,其对Spring容器的意义不用多说了,在这里的目的也是一样的。
  7. 以上替换SqlSource的操作,正是给予通用Mapper大展拳脚空间的关键。通用Mapper正是在此替换操作之前,回调一系列的约定下的实现者来动态获取相应的作为替换者的真正SqlSource(MapperTemplate.setSqlSource(MappedStatement ms)方法)。

相关时序图如下:

  1. 替换为MapperFactoryBean<T>类型
    替换为MapperFactoryBean类型
  2. 构建出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的能力。

本小节最后,总结下自定义扩展时候的注意事项吧:

  1. 自定义的扩展接口(例如上面的InsertListMapper<T>),方法签名的定义没有注意事项,只需要注意方法上注解的method参数值必须为dynamicSQL
  2. 上述注解中,其另外一个type 参数所指示的自定义类型,该类型必须有这样一个方法:
    a. 与该接口方法同名的,
    b. 方法参数为MappedStatement类型,
    c. 返回值为voidString, SqlNode中的一种。
  3. 更多的细节还是参见作者本人的扩展通用接口 文档吧。

4. 总结

  1. MapperScannerConfigurerClassPathMapperScanner配合将可能满足条件的全部接口全部扫描入容器,这些接口在Spring容器内部将以自定义MapperFactoryBean<T>的形式存在。
  2. 自定义MapperFactoryBean<T>因为间接实现的Spring中的InitializingBean接口,使得有机会在系统初始化过程中完成底层MappedStatement实例中的SqlSource字段值的替换。
  3. MapperScannerConfigurer构建的MapperHelper实例是全局唯一的。这也符合重量级的服务域应该是全局唯一的最佳实践。
  4. 上面提到的MapperHelper实例在通用Mapper的关键类MapperFactoryBean<T>MapperTemplate类中都相应的实例字段,这和Mybatis内部的Configuration实例地位相当类似,类似《程序员修炼之道–从小工到专家》中"黑板"的概念。

5. 结尾

其实最让笔者感慨的是,任何一件事,哪怕再小,只要肯钻研,其中必然有着无数可以完善的细节,而正是这些细节的追求,让人变得优秀。通用Mapper的作者从2014年的最初版本坚持演化到现在,这份坚持的精神,以及精益求精的态度真的很让人动容。

6. 相关链接

  1. MyBatis 为什么需要通用 Mapper ?
  2. 通用Mapper - WIKI
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值