浅谈MyBatis通用Mapper实现原理

本文会先介绍通用 Mapper 的简单原理,然后使用最简单的代码来实现这个过程。

基本原理

通用 Mapper 提供了一些通用的方法,这些通用方法是以接口的形式提供的,例如。

public interface SelectMapper {
  /**
   * 根据实体中的属性值进行查询,查询条件使用等号
   */
  @SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
  List select(T record);
}

接口和方法都使用了泛型,使用该通用方法的接口需要指定泛型的类型。通过 Java 反射可以很容易得到接口泛型的类型信息,代码如下。

Type[] types = mapperClass.getGenericInterfaces();
Class entityClass = null;
for (Type type : types) {
  if (type instanceof ParameterizedType) {
    ParameterizedType t = (ParameterizedType) type;
    //判断父接口是否为 SelectMapper.class
    if (t.getRawType() == SelectMapper.class) {
      //得到泛型类型
      entityClass = (Class) t.getActualTypeArguments()[0];
      break;
    }
  }
}

实体类中添加的 JPA 注解只是一种映射实体和数据库表关系的手段,通过一些默认规则或者自定义注解也很容易设置这种关系,获取实体和表的对应关系后,就可以根据通用接口方法定义的功能来生成和 XML 中一样的 SQL 代码。动态生成 XML 样式代码的方式有很多,最简单的方式就是纯 Java 代码拼字符串,通用 Mapper 为了尽可能的少的依赖选择了这种方式。如果使用模板(如 FreeMarker,Velocity 和 beetl 等模板引擎)实现,自由度会更高,也能方便开发人员调整。

在 MyBatis 中,每一个方法(注解或 XML 方式)经过处理后,最终会构造成 MappedStatement 实例,这个对象包含了方法id(namespace+id)、结果映射、缓存配置、SqlSource 等信息,和 SQL 关系最紧密的是其中的 SqlSource,MyBatis 最终执行的 SQL 时就是通过这个接口的 getBoundSql 方法获取的。

在 MyBatis 中,使用@SelectProvider 这种方式定义的方法,最终会构造成 ProviderSqlSource,ProviderSqlSource 是一种处于中间的 SqlSource,它本身不能作为最终执行时使用的 SqlSource,但是他会根据指定方法返回的 SQL 去构造一个可用于最后执行的 StaticSqlSource,StaticSqlSource的特点就是静态 SQL,支持在 SQL 中使用#{param} 方式的参数,但是不支持 等标签。

为了能根据实体类动态生成支持动态 SQL 的方法,通用 Mapper 从这里入手,利用ProviderSqlSource 可以生成正常的 MappedStatement,可以直接利用 MyBatis 各种配置和命名空间的特点(这是通用 Mapper 选择这种方式的主要原因)。在生成 MappedStatement 后,“过河拆桥” 般的利用完就把 ProviderSqlSource 替换掉了,正常情况下,ProviderSqlSource 根本就没有执行的机会。在通用 Mapper 定义的实现方法中,提供了 MappedStatement 作为参数,有了这个参数,我们就可以根据 ms 的 id(规范情况下是 接口名.方法名)得到接口,通过接口的泛型可以获取实体类(entityClass),根据实体和表的关系我们可以拼出 XML 方式的动态 SQL,一个简单的方法如下。

/**
 * 查询全部结果
 *
 * @param ms
 * @return
 */
public String selectAll(MappedStatement ms) {
  final Class entityClass = getEntityClass(ms);
  //修改返回值类型为实体类型
  setResultType(ms, entityClass);
  StringBuilder sql = new StringBuilder();
  sql.append(SqlHelper.selectAllColumns(entityClass));
  sql.append(SqlHelper.fromTable(entityClass, tableName(entityClass)));
  sql.append(SqlHelper.orderByDefault(entityClass));
  return sql.toString();
}

拼出的 XML 形式的动态 SQL,使用 MyBatis 的 XMLLanguageDriver 中的 createSqlSource 方法可以生成 SqlSource。然后使用反射用新的 SqlSource 替换ProviderSqlSource 即可,如下代码。

/**
 * 重新设置SqlSource
 *
 * @param ms
 * @param sqlSource
 */
protected void setSqlSource(MappedStatement ms, SqlSource sqlSource) {
  MetaObject msObject = SystemMetaObject.forObject(ms);
  msObject.setValue("sqlSource", sqlSource);
}

MetaObject 是MyBatis 中很有用的工具类,MyBatis 的结果映射就是靠这种方式实现的。反射信息使用的 DefaultReflectorFactory,这个类会缓存反射信息,因此 MyBatis 的结果映射的效率很高。

到这里核心的内容都已经说完了,虽然知道怎么去替换 SqlSource了,但是!什么时候去替换呢?

这一直都是一个难题,如果不大量重写 MyBatis 的代码很难万无一失的完成这个任务。通用 Mapper 并没有去大量重写,主要是考虑到以后的升级,也因此在某些特殊情况下,通用 Mapper 的方法会在没有被替换的情况下被调用,这个问题在将来的 MyBatis 3.5.x 版本中会以更友好的方式解决(目前的 ProviderSqlSource 已经比以前能实现更多的东西,后面会讲)。

针对不同的运行环境,需要用不同的方式去替换。当使用纯 MyBatis (没有Spring)方式运行时,替换很简单,因为会在系统中初始化 SqlSessionFactory,可以初始化的时候进行替换,这个时候也不会出现前面提到的问题。替换的方式也很简单,通过 SqlSessionFactory 可以得到 SqlSession,然后就能得到 Configuration,通过 configuration.getMappedStatements() 就能得到所有的 MappedStatement,循环判断其中的方法是否为通用接口提供的方法,如果是就按照前面的方式替换就可以了。

在使用 Spring 的情况下,以继承的方式重写了 MapperScannerConfigurer 和 MapperFactoryBean,在 Spring 调用 checkDaoConfig 的时候对 SqlSource 进行替换。在使用 Spring Boot 时,提供的 mapper-starter 中,直接注入 List sqlSessionFactoryList 进行替换。

下面我们按照这个思路,以最简练的代码,实现一个通用方法。

实现一个简单的通用 Mapper

1. 定义通用接口方法

public interface BaseMapper {
  @SelectProvider(type = SelectMethodProvider.class, method = "select")
  List select(T entity);
}

这里定义了一个简单的 select 方法,这个方法判断参数中的属性是否为空,不为空的字段会作为查询条件进行查询,下面是对应的 Provider。

public class SelectMethodProvider {
  public String select(Object params) {
    return "什么都不是!";
  }
}

这里的 Provider 不会最终执行,只是为了在初始化时可以生成对应的 MappedStatement。

2. 替换 SqlSource

下面代码为了简单,都指定的 BaseMapper 接口,并且没有特别的校验。

public class SimpleMapperHelper {
  public static final XMLLanguageDriver XML_LANGUAGE_DRIVER
      = new XMLLanguageDriver();
  /**
   * 获取泛型类型
   */
  public static Class getEntityClass(Class mapperClass){
    Type[] types = mapperClass.getGenericInterfaces();
    Class entityClass = null;
    for (Type type : types) {
      if (type instanceof ParameterizedType) {
        ParameterizedType t = (ParameterizedType) type;
        //判断父接口是否为 BaseMapper.class
        if (t.getRawType() == BaseMapper.class) {
          //得到泛型类型
          entityClass = (Class) t.getActualTypeArguments()[0];
          break;
        }
      }
    }
    return entityClass;
  }

  /**
   * 替换 SqlSource
   */
  public static void changeMs(MappedStatement ms) throws Exception {
    String msId = ms.getId();
    //标准msId为 包名.接口名.方法名
    int lastIndex = msId.lastIndexOf(".");
    String methodName = msId.substring(lastIndex + 1);
    String interfaceName = msId.substring(0, lastIndex);
    Class mapperClass = Class.forName(interfaceName);
    //判断是否继承了通用接口
    if(BaseMapper.class.isAssignableFrom(mapperClass)){
      //判断当前方法是否为通用 select 方法
      if (methodName.equals("select")) {
        Class entityClass = getEntityClass(mapperClass);
        //必须使用");
        //解析 sqlSource
        SqlSource sqlSource = XML_LANGUAGE_DRIVER.createSqlSource(
            ms.getConfiguration(), sqlBuilder.toString(), entityClass);
        //替换
        MetaObject msObject = SystemMetaObject.forObject(ms);
        msObject.setValue("sqlSource", sqlSource);
      }
    }
  }

}

changeMs 方法简单的从 msId 开始,获取接口和实体信息,通过反射回去字段信息,使用 标签动态判断属性值,这里的写法和 XML 中一样,使用 XMLLanguageDriver 处理时需要在外面包上

你可能感兴趣的:(浅谈MyBatis通用Mapper实现原理)