MyBatis基礎(二) 代理對象的生成--源碼分析

系列內容回顧:

整體思路

這個部分的源碼比較多, 先談思路. 假設我們自己要實現一個和MyBatis相似的功能, 給一個接口和一個方法-sql語句映射關係的xml, 生成一個代理類完成dao的工作, 應該怎麼做?

首先實現工具上, 有兩種能用的工具, 一個是JDK動態代理, 另一個是CGLib動態代理, 兩者的區別是JDK動態代理通過反射實現, 只能生成接口中聲明的方法; CGLib通過操縱字節碼實現, 其本質是繼承了原有類的子類, 並進行加強, 所以不需要提供接口, 但是生成代理類的速度會比JDK的方式慢一些.

既然給定了接口, 這裏可以採用JDK的動態代理來做, 首先回想一下動態代理類的生成, 我們通常採用java.lang.reflect.Proxy類的靜態工廠方法來生成:

// 生成動態代理類的靜態方法
Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
// invocationHandler需要實現的方法
invoke(Object proxy, Method method, Object[] args)

loader通常和接口或實現類的classLoader保持一致, interface就是我們自己定義的dao接口, 最重要的是InvocationHandler, 因爲我們事實上只有方法聲明沒有方法實現, 所以當調用例如findAll之類的方法時, 我們需要在InvocationHandler中給出具體的實現.

<mapper namespace="org.org.example.a_xml.dao.IUserDao">
    <!--配置查詢所有-->
<!--    <select id="方法名稱">-->
<!--        sql語句-->
<!--    </select>-->
    <select id="findAll" resultType="org.org.example.a_xml.domain.User">/*resultType 指定拿到的數據封裝類*/
        select * from user;
    </select>
</mapper>

上面給出了一個非常簡單的mapper.xml, InvocationHandler中需要完成的工作的整體思路如下:

  • 我們在mapper.xml中已經定義好了namespace="org.org.example.a_xml.dao.IUserDao", 這個是接口和xml之間的一一對應
  • 之後在<mapper>標籤中定義了方法名和sql語句的一一映射, 我們可以構造這樣一個類: 它持有一對接口名.方法名 - sql命令的映射
  • 在InvocationHandler中根據接口名+方法名找到對應的上述類的對象, 有對象執行以下工作:
  • 根據sql語句執行, 將sql語句返回的結果映射成實體類的對象, 這個映射關係由select標籤中的resultType="org.org.example.a_xml.domain.User"定義.
  • 將映射的實體類對象從InvacationHandler的方法調用返回.

整體思路理完了我們來看源碼, 首先看一下整體流程:

在這裏插入圖片描述

左邊是我們在使用中的調用流程, 右邊是batis幫我們做的事情,方括號內的是類實現的接口. 可以看到MapperProxy其實是一個InvocationHandler類, 在這裏面定義的invoke方法,正是MyBatis幫我們生成的dao的各種方法的實現, 這些方法的類稱爲MapperMethod, 其中的excute方法返回的就是我們需要的dao方法返回的實體類結果.

源碼分析

MapperRegistry.java

這個類裏面重點關注getMapper方法

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // knownMappers 是個Map<Class<?>, MapperProxyFactory<?>>實例, 存着Dao接口類和mapperProxyFactory的映射
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); 
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
        // 如果knownMapper中沒有的話報錯,  否則嘗試返回一個新的MapperProxy
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

getMapper方法事實上是通過工廠方法返回一個MapperProxy<T>實例, 這個實例顧名思義就是Dao的代理類, 由它負責Dao的各個方法的實現. 代理類在執行每個方法時都會調用invoke方法, 一個傳統的代理類中的invoke方法中大致是這麼寫的:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //在真實的對象執行之前我們可以添加自己的操作
        System.out.println("before invoke。。。");
        Object invoke = method.invoke(obj, args);
        //在真實的對象執行之後我們可以添加自己的操作
        System.out.println("after invoke。。。");
        return invoke;
    }

問題是我們這裏的method可能是接口的default方法, 也可能只聲明瞭沒有實現的, 如果是沒有實現的方法, 就是mybatis需要幫忙實現的, 來看invoke的源碼:

@Override
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()) {
    return invokeDefaultMethod(proxy, method, args);
    }
} catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
}
// 按照Method實例查找一個mapperMethod並執行
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}

所以一個代理類的方法的實現工作, 事實上是交給了一個MapperMethod實例去完成的, 所以我們接着來看MapperMethod裏面幹了什麼?

public class MapperMethod {
  // 兩個成員變量, 一個是sql語句的封裝對象, 一個是調用者所調用的方法的對象
  // 就是我們之前整理思路時說的, 持有**一對接口名.方法名 - sql命令的映射**的類
  private final SqlCommand command;
  private final MethodSignature method;

  //...

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      /*
        所有case的流程:
        - 準備sql語句的參數
        - 執行sqlSession中提供的方法
        - 對插入, 刪除, 更新結果, 返回的是影響了幾行數據, 調用rowCountResult進行封裝
        - 對查詢操作分情況討論
      */
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
      // select裏面的花招比較多, 需要考慮到是否直接用回調函數處理, 返回多個值, 返回單個值, 返回map, 返回cursor的幾種情況
        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;
  }
  //...
}

具體的select怎麼處理, update, delete, insert怎麼處理結果, 這些內容就比較簡單, 可以直接查看下源碼.

結論

Mybatis的動態生成Dao代理對象的原理, 就是用動態代理在InvocationHandler裏面按照sql和方法的映射處理沒有實現的方法,並封裝結果.

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