mybatis 中的動態(tài)代理

mbatis 中的 mapper 類谭羔,在代碼層級都是接口類华糖,使用框架的時候,也沒有要求我們給出這些接口聲明的實現(xiàn)瘟裸,而是應用開發(fā)者編寫對應的 xml 文件用于映射客叉。

看到這一步,我想有經(jīng)驗的程序員應該能聯(lián)想到代理话告。框架應該是通過動態(tài)代理的方式給上述的接口聲明方法創(chuàng)建的實現(xiàn)類和方法兼搏,并且這個代理創(chuàng)建的方法內(nèi)容還關(guān)聯(lián)到了 mapper.xml 文件中的 sql 語句和參數(shù)注入。

這個具體流程是怎么執(zhí)行的呢沙郭?接下來通過測試類來 debug 一下佛呻。

一 測試類 debug

    @Test
    public void testProxy() throws Exception {
        String resource = "db_config/mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        Configuration configuration = sqlSessionFactory.getConfiguration();

        SqlSession sqlSession = sqlSessionFactory.openSession();
        WaterMapper waterMapper = sqlSession.getMapper(WaterMapper.class);
        System.out.println(waterMapper.getById(1));
    }

代碼是基礎的 junit 測試流程,通過 sqlSession 獲取對應的 mapper病线。 需要注意的是吓著,這里的 mapper 實際上是 mybatis 幫我們創(chuàng)建的代理類。

這里檢索的入口代碼應該是

WaterMapper waterMapper = sqlSession.getMapper(WaterMapper.class);

定義在 SqlSession 接口上的 getMapper 方法送挑,有兩個類有對應的實現(xiàn)绑莺,debug 的時候?qū)嶋H進入的是 SqlSessionManager.getMapper(Class<T> type)

  @Override
  public <T> T getMapper(Class<T> type) {
    return getConfiguration().getMapper(type, this);
  }

然后進入到了 org.apache.ibatis.session.Configuration#getMapper,傳入的參數(shù)是類型參數(shù) class 和 sqlSession惕耕。

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

繼續(xù)遞進到 org.apache.ibatis.binding.MapperRegistry#getMapper

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 獲取用于創(chuàng)建代理類的工廠類纺裁,這個工廠類在 mapper 注冊的時候初始化
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    // 如果沒有找到對應類代理工廠的話,拋出異常
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      // 通過代理工廠類創(chuàng)建代理對象赡突,入?yún)?sqlSession区赵,上面綁定了類信息和 sql 信息
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

mapperRegistry 即 mapper 的注冊器類惭缰,這個類持有了為每一個 mapper 創(chuàng)建的代理工廠類及其映射關(guān)系浪南,key 為對應的 mapper 類型 class 信息漱受。

  protected T newInstance(MapperProxy<T> mapperProxy) {
    // 3 mapperProxy 中包含了被代理對象,sqlSession 對象和方法緩存,此處直接使用了 jdk 的動態(tài)代理
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    // 1 創(chuàng)建一個 mapper 代理對象類
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    // 2 以創(chuàng)建的 mapper 代理對象類為參數(shù),創(chuàng)建最終供客戶端調(diào)用的代理對象
    return newInstance(mapperProxy);
  }

由此可見 mybatis 在此處直接使用了 jdk 的動態(tài)代理撰洗。不過最后傳入的 invocationHandler 類是 mybatis 自己封裝的。

public class MapperProxy<T> implements InvocationHandler

這里需要關(guān)注一下 mapperProxy 類中復寫的 invoke 方法腐芍,這里是生成的代理類在方法被調(diào)用的時候的核心實現(xiàn)邏輯猪勇。

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

org.apache.ibatis.binding.MapperProxy#cachedInvoker

這個方法會返回一個 org.apache.ibatis.binding.MapperProxy.MapperMethodInvoker 對象设褐。這個對象是 MapperProxy 的一個內(nèi)部接口。實際調(diào)用的實現(xiàn)類是

org.apache.ibatis.binding.MapperProxy.PlainMethodInvoker

最后這個代理流程泣刹,實際執(zhí)行到的方法是

org.apache.ibatis.binding.MapperMethod#execute

會根據(jù)傳入 sql 的類型助析,執(zhí)行不同都方法 case。里面實際用到的椅您,還是 sqlSession 對象實體貌笨。


  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      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:
        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;
  }

二 流程總結(jié)

調(diào)用流程
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市襟沮,隨后出現(xiàn)的幾起案子锥惋,更是在濱河造成了極大的恐慌,老刑警劉巖开伏,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件膀跌,死亡現(xiàn)場離奇詭異,居然都是意外死亡固灵,警方通過查閱死者的電腦和手機捅伤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來巫玻,“玉大人丛忆,你說我怎么就攤上這事祠汇。” “怎么了熄诡?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵可很,是天一觀的道長。 經(jīng)常有香客問我凰浮,道長我抠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任袜茧,我火速辦了婚禮菜拓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘笛厦。我一直安慰自己纳鼎,他們只是感情好,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布裳凸。 她就那樣靜靜地躺著贱鄙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪登舞。 梳的紋絲不亂的頭發(fā)上贰逾,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天,我揣著相機與錄音菠秒,去河邊找鬼疙剑。 笑死,一個胖子當著我的面吹牛践叠,可吹牛的內(nèi)容都是我干的言缤。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼禁灼,長吁一口氣:“原來是場噩夢啊……” “哼管挟!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起弄捕,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤僻孝,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后守谓,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體穿铆,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年斋荞,在試婚紗的時候發(fā)現(xiàn)自己被綠了荞雏。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖凤优,靈堂內(nèi)的尸體忽然破棺而出悦陋,到底是詐尸還是另有隱情,我是刑警寧澤筑辨,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布俺驶,位于F島的核電站,受9級特大地震影響挖垛,放射性物質(zhì)發(fā)生泄漏痒钝。R本人自食惡果不足惜秉颗,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一痢毒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蚕甥,春花似錦哪替、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至爱沟,卻和暖如春帅霜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背呼伸。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工身冀, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人括享。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓搂根,卻偏偏與公主長得像,于是被迫代替她去往敵國和親铃辖。 傳聞我的和親對象是個殘疾皇子剩愧,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容