MyBatis與設計模式的激情碰撞

MyBatis與設計模式的激情碰撞

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

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

今天咱們就來討論一下,在MyBatis內部,爲了提升代碼的可讀性究竟作了哪些設計呢?固然,若是你對MyBatis的代碼特別熟悉,做者在文中有錯誤的地方歡迎指出來,由於做者尚未完整的通讀MyBatis的源碼,大概看了70%左右,後續看完以後,做者會考慮出一期關於MyBatis源碼的解讀,一方面是增強做者對於MyBatis源碼的理解,一方面是讓你們更好的學習MyBatis,話很少說,進入正題吧!sql

1、外觀模式

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

咱們先看一段代碼:apache

@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框架的使用!緩存

2、裝飾者模式

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

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

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

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經過構造方法傳入一個真正的執行器,也就是一個真正可以查詢的執行器,而後處理完緩存操做後,調用可以真正執行查詢的執行器進行數據的查詢,待數據查詢到以後,再將數據放置到緩存內部,從而完成整個緩存的邏輯!這就是裝飾者模式!框架

3、責任鏈模式

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

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

咱們仍是基於MyBatis的二級緩存來講話,先看一張圖:

圖片來源於源碼閱讀網http://www.coderead.cn/
圖片來源於源碼閱讀網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中對鏈條的構建遠不止那麼簡單,這個咱們之後再議!

4、動態代理模式

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

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)方法來構建與底層數據庫的交互操做,在使用中,你獲取的事實上不是接口的實現類,而是接口的代理對象,由生成的代理對象,完成了後續的全部操做!

5、總結

本篇文章事實上不少的代碼細節都是一律而過,並無深刻講解,固然這也不是我寫本篇文章的一個目的,本篇文章的目的僅僅是想要讓使用者可以瞭解一些MyBatis的大體細節,從而對MyBatis有一個總體的認知,方便再本身調試源碼的時候,不至於那麼懵逼!


才疏學淺,若是文章中理解有誤,歡迎大佬們私聊指正!歡迎關注做者的公衆號,一塊兒進步,一塊兒學習!

相關文章
相關標籤/搜索