MyBatis與設計模式的激情碰撞

最近一直在研究MyBatis的源碼,MyBatis作爲國內最爲經常使用的持久層框架,其內部代碼的設計也是極其優秀的!我們學習源碼的目的是什麼呢?

  • 一方面是對該框架有一個很深入的認識,以便在開發過程中有能力對框架進行深度的定製化開發或者在解決BUG的時候更加得心應手!
  • 一方面是學習代碼裏面優秀的設計,看看這些成名多年的框架,他們的開發者是如何設計出一個高擴展性、低耦合性的代碼呢?然後在自己的開發場景中應用。

今天我們就來討論一下,在MyBatis內部,爲了提高代碼的可讀性究竟做了哪些設計呢?當然,如果你對MyBatis的代碼特別熟悉,作者在文中有錯誤的地方歡迎指出來,因爲作者還沒有完整的通讀MyBatis的源碼,大概看了70%左右,後續看完之後,作者會考慮出一期關於MyBatis源碼的解讀,一方面是加強作者對於MyBatis源碼的理解,一方面是讓大家更好的學習MyBatis,話不多說,進入正題吧!

一、外觀模式

外觀模式,有些開發者也會把它叫做門面模式,他多用於接口的設計防,面,目的是封裝系統的底層實現,隱藏系統的複雜性,提供一組更加簡單易用、更高層的接口。我們將多個接口的Api替換爲一個接口,以減少程序調用的複雜性,增加程序的易用性!

我們先看一段代碼:

@Test
public void SqlSessionTest(){
    //構建會話對象
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    System.out.println(mapper.findUserByName("張三"));
}

熟悉Mybatis代碼的同學應該對這個代碼無比熟悉,利用會話工廠構建會話對象SqlSession,基於會話對象調用Mapper方法,但是憑什麼我們只需要構建一個SqlSession對象就能夠完全操作咱們的MyBatis呢?這裏MyBatis的開發者使用了外觀設計模式,將所有的操作Api都封裝進了SqlSession內部,讓使用者無需關心內部的底層實現就能夠使用是不是很完美,那麼內部他是如何操作的呢?由於本章內容的目的並不是爲了分析源碼,所以我們只需要知道如何實現的就行!我們進入到SqlSession內部

public class DefaultSqlSession implements SqlSession {
  //忽略不必要代碼
  private final Executor executor;
  //忽略不必要代碼
  public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
    //...
    this.executor = executor;
    //...
  }
}

我們可以裏看到SqlSession內部封裝了一個Executor對象,也就是MyBatis的執行器,然後通過構造方法傳遞過來!後續所有的查詢邏輯都是調用Executor內的方法來完成的實現,而SqlSession本身不做任何操作,所以就能僅僅通過一個對象,來構建起整個Mybatis框架的使用!

二、裝飾者模式

裝飾者模式:動態地給一個對象增加一些額外的職責,增加對象功能來說,裝飾模式比生成子類實現更爲靈活。裝飾模式是一種對象結構型模式。裝飾者設計模式的目的是爲了給某一些沒有辦法或者不方便變動的方法動態的增加一些額外的功能!

鳥語說完了,轉換成大白話就是,有些類沒有辦法經常改代碼,但是有要求他在不同的場景下展示不同的功能,又想要女朋友,又想和其他美女撩騷!典型的渣男,但是裝飾者模式真正爲這一操作提供了可能!比如將美女裝飾成自己的姐姐妹妹,阿姨大媽,那不就能痛快的撩騷了!開個玩笑,我對我家的絕對忠貞不二!那麼MyBatis是如何使用這一設計模式呢?

衆所周知,MyBatis存在二級緩存,但是我們有時候需要二級緩存,有時候又不需要,這個時候怎麼辦呢?因此MyBatis單獨抽象出來了一個Executor的實現類CachingExecutor專門來做緩存相關的操作,它本身不做任何的查詢邏輯,只實現自己的混村邏輯,從而可以動態的插拔MyBatis的緩存邏輯!具體的實現思路如下:

public class CachingExecutor implements Executor {

  private final Executor delegate;
  //.....忽略多餘代碼

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    //.....忽略多餘代碼
  }
    
  //我們以二級緩存下的查詢爲例
   @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, 
                           	ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        //查詢緩存是否存在
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
           //不存在就調用其他執行器的 query 方法
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //將查出來的對象放置到緩存中
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    //調用其他執行器的 query 方法
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
  //.....忽略多餘代碼
}

我們可以看到,CachingExecutor通過構造方法傳入一個真正的執行器,也就是一個真正能夠查詢的執行器,然後處理完緩存操作後,調用能夠真正執行查詢的執行器進行數據的查詢,待數據查詢到之後,再將數據放置到緩存內部,從而完成整個緩存的邏輯!這就是裝飾者模式!

三、責任鏈模式

責任鏈設計模式:責任鏈模式(Chain of Responsibility)是多個對象都有機會處理請求,從而避免請求的發送者和接受者之間的耦合關係.將這些對象連成一條鏈,並沿着這條鏈傳遞該請求,直到有對象能夠處理。

但是接下來介紹的這種事實上並沒有完全遵守以上的概念,獲取我們可以將這種設計模式叫做 責任鏈的變種:功能鏈,它的設計思想是分而治之,符合七大設計原則的 迪米特法則 合成複用原則 單一職責原則 ,它通過實現組合一種功能實現,鏈條上的每一個節點都能夠處理一部分特有的操作,一直向下傳遞,最終完成整個操作!

我們還是基於MyBatis的二級緩存來說話,先看一張圖:

圖片來源於源碼閱讀網http://www.coderead.cn/

如果不懂責任鏈設計模式,就會懵逼,僅僅是一個緩存而已,弄這麼多getObject()幹嘛?事實上即使我們進入到源碼中也會發現好多類似這樣的邏輯:

我們先獲取一個緩存對象,然後設置緩存:

@Test
public void CacheTest (){
    Cache cache = configuration.getCache(UserMapper.class.getName());
    cache.putObject("666","你好");
    cache.getObject("666");
}

按照常規理解,他應該會把這個k-v值放置到 Map中或者執行一些邏輯操作在放到Map中,但是我們卻發現下面這一段:

org.apache.ibatis.cache.decorators.SynchronizedCache#putObject

@Override
public synchronized void putObject(Object key, Object object) {
    delegate.putObject(key, object);
}

你心態崩不崩,行繼續往下跟:

org.apache.ibatis.cache.decorators.LoggingCache#putObject

@Override
public void putObject(Object key, Object object) {
    delegate.putObject(key, object);
}

你又會發現這樣一段邏輯,繼續往下也一樣,那麼MyBatis爲什麼會搞這麼多空方法呢?顯得代碼牛逼?當然不是,他這麼設計肯定是有一定的用意的,什麼用意呢?

事實上我們發現org.apache.ibatis.cache.decorators.SynchronizedCache#putObject這個方法上增加了synchronized屬性,他是爲了解決多線程的併發問題的,org.apache.ibatis.cache.decorators.LoggingCache#putObject這個方法本身沒做什麼,但是我們看getObject方法:

@Override
public Object getObject(Object key) {
    requests++;
    final Object value = delegate.getObject(key);
    if (value != null) {
        hits++;
    }
    if (log.isDebugEnabled()) {
        log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
    }
    return value;
}

這個類是爲了統計二級緩存的命中率的,諸如此類,往下還有org.apache.ibatis.cache.decorators.SerializedCache#putObject做二級緩存序列化的、org.apache.ibatis.cache.decorators.LruCache#putObject最少使用緩存淘汰策略的以及org.apache.ibatis.cache.impl.PerpetualCache#putObject真正的緩存方法,這是一個功能鏈條,其實這個例子與使用了一定的裝飾模式,通過構造函數:

public SynchronizedCache(Cache delegate) {
    this.delegate = delegate;
}

設置本次處理完成後的下一個處理節點,從而完成整個鏈條的調用,那麼在哪裏構建鏈條的呢?我們看一段代碼,這裏由於篇幅原因,作者不做太多的講解,大家看一下就行:

private Cache setStandardDecorators(Cache cache) {
    try {
        MetaObject metaCache = SystemMetaObject.forObject(cache);
        if (size != null && metaCache.hasSetter("size")) {
            metaCache.setValue("size", size);
        }
        if (clearInterval != null) {
            cache = new ScheduledCache(cache);
            ((ScheduledCache) cache).setClearInterval(clearInterval);
        }
        if (readWrite) {
            cache = new SerializedCache(cache);
        }
        cache = new LoggingCache(cache);
        cache = new SynchronizedCache(cache);
        if (blocking) {
            cache = new BlockingCache(cache);
        }
        return cache;
    } catch (Exception e) {
        throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
}

可以看到,以上代碼通過各種條件的判斷往裏面放置調用鏈節點,從而構建出一整個鏈條,但是事實上,Mybatis中對鏈條的構建遠不止那麼簡單,這個我們以後再議!

四、動態代理模式

代理模式:爲其它對象提供一種代理以控制對這個對象的訪問。當無法或不想直接訪問某個對象存在困難時可以通過一個代理對象來間接訪問,爲了保證客戶端使用的透明性,委託對象與代理對象需要實現相同的接口。

MyBatis中是在哪裏使用的動態代理的設計模式呢?衆所周知,我們在使用MyBatis的時候,只需要將對應的Dao層抽象出一個接口,後續的調用邏輯就能夠完整的調用數據庫實現各種邏輯,但是你是否疑惑過,MyBatis的Mapper我們明明沒有設置實現類啊,他是如何操作數據庫的呢?這裏就使用了動態代理設計模式!

我們先看一段代碼:

@Test
public void SqlSessionTest(){
    //構建會話對象
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    System.out.println(mapper.findUserByName("張三"));
}

UserMapper對象是一個接口,只需要將他交給Mybatis就能夠自己完成對應的邏輯,我們通過斷點一步步跟下去,會發現這樣一段邏輯:

protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

看到這裏,熟悉jdk動態代理的同學可能會恍然大悟,原來它使用的是動態代理來實現的對應實現類,mapperInterface.getClassLoader()是類加載器,mapperInterface是要代理的接口,mapperProxy是真正的實現操作,他是InvocationHandler的子類,目的就是完成代理類的自定義的代碼操作!它事實上會構建這麼個東西:

public class MapperProxy<T> implements InvocationHandler, Serializable {  
  //........
  @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);
    }
  }
  //........
}

最終會調用mapperMethod.execute(sqlSession, args)方法來構建與底層數據庫的交互操作,在使用中,你獲取的事實上不是接口的實現類,而是接口的代理對象,由生成的代理對象,完成了後續的所有操作!

五、總結

本篇文章事實上很多的代碼細節都是一概而過,並沒有深入講解,當然這也不是我寫本篇文章的一個目的,本篇文章的目的僅僅是想要讓使用者能夠了解一些MyBatis的大致細節,從而對MyBatis有一個整體的認知,方便再自己調試源碼的時候,不至於那麼懵逼!


才疏學淺,如果文章中理解有誤,歡迎大佬們私聊指正!歡迎關注作者的公衆號,一起進步,一起學習!

歡迎關注作者,一起學習一起進步

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