mybatis-binding 綁定包解析

概述

爲了實現直接調用Mapper接口類的方法,便達到調用sql的目標,mybatis-binding包提供了Mapper接口的代理類和其方法的代理類。主要起到連接 Mapper.java 和 Mapper.xml的作用。

方法映射綁定

爲了連接Mapper接口的方法 和 Mapper.xml的statement,於是就有了類 MapperMethod

public class MapperMethod {
 
  // 代表一條sql命令的屬性
  private final SqlCommand command;
  // 方法屬性
  private final MethodSignature method;

  // 構造函數
  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }
}

MapperMethod只有兩個屬性,一個是代表sql的,一個是代表java方法的。

這兩個類都是MapperMethod的內部類,mybatis是爲了聚合並隔離,所以用了內部類來承載。

SqlCommand描述和定位sql的基本信息,因爲對於Statement的具體信息由MappedStatement承載,這裏只保存

public static class SqlCommand {

  /**
   * statementId,其實就是方法的全限定名
   */
  private final String name;

  /**
   * sql的類型
   */
  private final SqlCommandType type;
}
public static class MethodSignature {

  /**
   * 返回類型是否是集合
   */
  private final boolean returnsMany;
  /**
   * 返回類型是否是map
   */
  private final boolean returnsMap;
  /**
   * 返回是否是void
   */
  private final boolean returnsVoid;
  /**
   * 返回是否是遊標
   */
  private final boolean returnsCursor;
  /**
   * 返回是否是Optional
   */
  private final boolean returnsOptional;
  /**
   * 返回類型
   */
  private final Class<?> returnType;
  /**
   * 返回map類型是,指定的key
   */
  private final String mapKey;
  /**
   * resultHandler在參數中的索引
   * 可能爲null,基本不會傳這個參數
   */
  private final Integer resultHandlerIndex;
  /**
   * 分頁參數在參數中的索引
   * 可能爲null,一般也很少使用
   */
  private final Integer rowBoundsIndex;
  /**
   * 參數名解析器
   */
  private final ParamNameResolver paramNameResolver;
}

這兩個類的屬性是如何得到的比較簡單也不是重點,這裏不介紹。

焦點繼續放在MapperMethod上。前面講了MapperMethod的屬性,現在看依託屬性,MapperMethod提供的能力。

MapperMethod就提供了一個public方法,即execute執行,方法的執行直接代表了sql的執行,看下實現。

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  //根據sql的類型,路由到sqlSession的具體方法,對結果進行簡單處理
  switch (command.getType()) {
    case INSERT: {
      //處理參數,將參數轉換成paramMap
      Object param = method.convertArgsToSqlCommandParam(args);
      //結果轉換,轉成方法返回值的類型
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: {
      //處理參數,將參數轉換成paramMap
      Object param = method.convertArgsToSqlCommandParam(args);
      //結果轉換,轉成方法返回值的類型
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {
      //處理參數,將參數轉換成paramMap
      Object param = method.convertArgsToSqlCommandParam(args);
      //結果轉換,轉成方法返回值的類型
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT:
      //查詢的方法路由,根據返回值的類型,調用sqlSession不同的方法
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {
        result = executeForCursor(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
        if (method.returnsOptional()
            && (result == null || !method.getReturnType().equals(result.getClass()))) {
          result = Optional.ofNullable(result);
        }
      }
      break;
    case FLUSH:
      result = sqlSession.flushStatements();
      break;
    default:
      throw new BindingException("Unknown execution method for: " + command.getName());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName()
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}

原來解析好sql、method的屬性是爲了路由到SqlSession上,雖然情況較多,但還好單個情況的處理並不複雜,只是簡單處理下入參、返回值。

這裏傳給SqlSession的入參都會被轉成ParamMap,key是參數的name,value是參數的值,這裏參數的name我們在xml中編寫sql中就是根據其引用到參數。

這裏name的規則可以看下org.apache.ibatis.reflection.ParamNameResolver#getNamedParams,這裏不多介紹。

如果單單使用SqlSession操作,那麼就沒有MapperMethod什麼事了,就是因爲直接調用SqlSession很麻煩,爲了讓使用者可以像調用java方法一樣調用sql,MapperMethod才應運而生。

Mapper代理

Mapper是接口,用戶無需自己實現,mybatis使用 MapperProxy僞造其實現類,完成sql調用。

public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }
}

MapperProxy實現了InvocationHandler,就是通過jdk動態代理的方法 代理Mapper接口。

保存SqlSession,和該類所有的MapperMethod,攔截方法調用,最終委託給MapperMethod執行。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    //Object的方法直接執行
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    }
    //default方法直接執行
    else if (method.isDefault()) {
      if (privateLookupInMethod == null) {
        return invokeDefaultMethodJava8(proxy, method, args);
      } else {
        return invokeDefaultMethodJava9(proxy, method, args);
      }
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }

  //其餘方法需要執行sql,從緩存中獲取 MapperMethod
  final MapperMethod mapperMethod = cachedMapperMethod(method);

  // 委託給MapperMethod執行
  return mapperMethod.execute(sqlSession, args);
}

解析過MapperMethod,MapperProxy so easy。

MapperProxyFactory

Mapper代理類工廠,就是爲了創建MapperProxy。

public class MapperProxyFactory<T> {

  /**
   * mapper類型
   */
  private final Class<T> mapperInterface;

  /**
   * java方法和 映射方法的緩存
   */
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  /**
   * 爲指定接口創建代理類
   */
  protected T newInstance(MapperProxy<T> mapperProxy) {
    //創建代理對象
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  /**
   *  爲指定接口創建代理類
   */ 
  public T newInstance(SqlSession sqlSession) {
    //創建 InvocationHandler
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);

    //創建代理對象
    return newInstance(mapperProxy);
  }

}

這裏可以看到MapperProxy和是根據SqlSession創建的,生命週期與SqlSession相同。

Map<Method, MapperMethod> methodCache是工廠共享的,其實就是因爲MapperProxy的生命週期原因,不同的SqlSession但是Method和MapperMethod其實是沒有改變的,所以可以緩存起來。

Mapper註冊器

MapperRegistry存放mybatis全量Mapper,提供了註冊、獲取的功能。邏輯也不復雜,看下代碼。

public class MapperRegistry {
  // mybatis配置,存儲而已,和Mapper註冊無關
  private final Configuration config;
  /**
   * key:Mapper接口
   * value:MapperProxyFactory
   */
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
}

這裏Value不是MapperProxy而是MapperProxyFactory的問題在上面解釋過了,就是因爲MapperProxy的生命週期是跟着SqlSession的,不適合做緩存。

mapper註冊

public <T> void addMapper(Class<T> type) {
  if (type.isInterface()) {
    // 重複添加報錯
    if (hasMapper(type)) {
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }
    boolean loadCompleted = false;
    try {
      // 添加到hashmap中
      knownMappers.put(type, new MapperProxyFactory<>(type));
      //解析mapper接口上的註解,包括(Cache、Select、Insert等)
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

註冊邏輯簡單粗暴

獲取Mapper

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  if (mapperProxyFactory == null) {
    throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  }
  try {
    //創建代理對象
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception e) {
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}

根據type獲取MapperProxyFactory,再根據SqlSession創建代理對象。easy。

總結

binding包類不多,邏輯不復雜,但還是蠻重要的。解析binding包主要給我的收穫還是 mybatis對代理模式的運用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章