前段时间在优化部门的codegen项目的时候,要将jdbc全部替换成mybatis去执行,有一些个性化的需求单纯的mybatis generator不能满足,于是特意研究了下mybatis,解决在不改造源码的情况下去另类的”扩展“mybatis generator,由于扩展实际上是根据mybatis的套路去进行扩展,所以这里先在第一段介绍一下mybatis-spring的执行原理,第二段会放出例子表明如何进行扩展
一.mybatis-spring执行原理
(1)扫描basePackage,用于将mapper接口扫描成MapperFactoryBean注册到spring
mybatis-spring里面,我们通过MapperScannerConfigurer设置basePackage路径,确定要扫描的Mapper接口,实际上当我们配置了这个basePackage之后,mybatis会扫描这个路径下的所有Mapper接口,并为每个Mapper接口初始化成一个MapperFactoryBean对象,在执行的时候,会通过这个MapperFactoryBean对象的getObject()方法为每个Mapper接口生成一个proxy对象,通过jdk的反射完成,下面来探究一下源码。
从第一张图片可以看到,MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,在正常的bean注册完之后,可以进一步做一些自定义的bean操作,我们可以看到第二张图哪里,MapperScannerConfigurer会执行一个scanner.scan方法去读取basePackage下面的mapper接口,scanner里面会调用doScan方法去扫描basePackage下面的Mapper接口成MapperFactoryBean并注册到spring容器里面。
在doScan方法里面,首先会调用findCandidateComponents()方法去将basePackage下面的Mapper类扫面成一个BeanDefinition的集合,然后对这些beanDefinition进行解析成BeanDefinitionHolder,最后通过注册到spring容器里面。获取到这些BeanDefinition之后,再去调用processBeanDefinitions方法将beanDefinition设置beanClass为MapperFactoryBean,在第一次需要使用这个bean的时候spring就会根据beanClass通过反射将对应的bean对象生成出来保存在map里面。
(2)扫描mapperLocation,扫描xml将xml里面节点跟namespace对应的mapper结合起来
SqlSessionFactoryBean会在spring初始化的时候调用它的afterPropertiesSet方法,然后再里面调用buildSqlSessionFactory方法,扫描mapperLocation路径下面的xml,根据namespace去找到对应的mapper接口,并调用bindMapperForNamespace()方法将xml和对应的mapper绑定
绑定实际上是通过反射的Class.forName方法根据namespace找到对应的class对象,根据这个class对象创建成MapperProxyFactory对象保存在knownMappers这个map里面。
值得注意的是,mybatis在parsePendingStatements这个方法里面会将每个xml里面的节点(select,delete,update,insert)封装成一个MappedStatement对象,最后保存在一个mappedStatements的map里面(保存的key是获取到xml的namespace+节点id)在执行的时候,mybatis会根据namesapce+id的方式去这个map里面找对应的MappedStatement对象,然后再去执行。
在parseStatementNode方法里面,会解析每个xml节点,最后调用一个builderAssitant.addMappedStatement方法去生成一个MappedStatement对象
builderAssitant.addMappedStatement方法会调用configuration.addMappedStatement方法将创建好的MappedStatement对象put进去mappedStatements的map里面
在put进这个map的时候,mybatis是根据namespace+id(selectByPrimaryKey)作为key来put进去mapper,这样在以后代理类执行的时候就是根据mapper的全路径+方法名就可以找到对应的mappedStatement对象
(3)mybatis执行
mybatis在执行的时候,首先通过MapperFactory.getObject()方法去调用getMapper方法,getMapper会根据type(即根据namespace反射生成的class对象)去knowMapper里面找到对应的MapperProxyFactory对象,然后通过mapperProxyFactory.newInstance(sqlSession)方法为每个mapper生成一个MapperProxy代理类(通过java的jdk代理),然后再通过这个代理类去执行mybatis
MapperProxy类执行的时候,会首先调用invoke方法,除了是Object类的方法之外,其他的都会调用cachedMapperMethod这个方法去获取缓存在methodCache里面的MapperMethod,当在map里面获取不到这个对象时,通过new MapperMethod方法去重新put进去这个map。在创建MapperMethod对象的时候,会调用一个new SqlCommand()方法,我们可以看到经常遇见到的"invalid bound statement"错误也是在这里抛出,statementName实际上就是nameSpace+方法名字(也就是xml里面节点的id),通过这个在mappedStatement的map里面去获取到对应的MappedStatement对象,然后再根据这个MappedStatement对象去执行。
二.扩展mybatis generator
既然我们已经知道mybatis的执行原理,那么去扩展mybatis generator就简单了,因为mybatis是通过namespace去绑定xml和对应的Mapper接口,那么在需要个性化需求的时候,我们可以将一些基础不变的方法放到BaseMapper里面,然后用个性化的CustomMapper去继承那个BaseMapper,然后将基础的xml放到一个base.xml,个性化的xml放到custom.xml,只要两个xml的namespace都是对应于CustomMapper,那么mybatis在初始化的时候就都会将xx.xx.CustomMapper.xx这样作为一个key,对应的MappedStatement对象保存到map里面,在执行的时候就能按照正常的mybatis执行方式去执行sql。
按照这个结构,我们可以将mybatis generator的生成在baseMapper和base.Xml里面,在将个性化的写在CustomMapper和Custom.Xml里面,但是最终我们要使用的时候只需要CustomMapper就可以使用全部的方法。
例子:
基础的baseXml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yue.dao.custom.mybatis.CustomMapper">
<delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
<!--
WARNING - @mbg.generated
This element is automatically generated by MyBatis Generator, do not modify.
-->
delete from demo_table
where `id` = #{id,jdbcType=BIGINT}
</delete>
</mapper>
自定义的customXml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yue.dao.custom.mybatis.CustomMapper">
<update id="updateByPrimaryKeySelective" parameterType="com.yue.domain.DemoTable">
update demo_table
<set>
<if test="dbNo != null">
`db_no` = #{dbNo},
</if>
<if test="globalId != null">
`global_id` = #{globalId},
</if>
<if test="updatedBy != null">
`updated_by` = #{updatedBy},
</if>
</set>
<where>
`id` = #{id,jdbcType=BIGINT}
<if test="versionNumber != null">
and `version_number` = #{versionNumber}
</if>
</where>
</update>
</mapper>
BaseMapper代码:
public interface BaseMapper {
int deleteByPrimaryKey(Long id);
/**
* This method was generated by MyBatis Generator. This method corresponds to the database table auth_menu
*
* @mbg.generated
*/
int updateByPrimaryKey(AuthMenu record);
}
CustomMapper代码:
package com.yue.dao.custom.mybatis;
import com.vip.fcs.app.ar.intfc.dao.mybatis.base.BaseMapper;
/**
* 对应表名:菜单 个性化处理
*/
public interface CustomMapper extends BaseMapper{
}