MyBatis 中的九種設計模式!
- 2020 年 3 月 3 日
- 筆記
點擊上方「掌上編程」,選擇「置頂或者星標」
優質文章第一時間送達!
雖然我們都知道有26個設計模式,但是大多停留在概念層面,真實開發中很少遇到,Mybatis源碼中使用了大量的設計模式,閱讀源碼並觀察設計模式在其中的應用,能夠更深入的理解設計模式。
Mybatis至少遇到了以下的設計模式的使用:
- Builder模式,例如SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder;
- 工廠模式,例如SqlSessionFactory、ObjectFactory、MapperProxyFactory;
- 單例模式,例如ErrorContext和LogFactory;
- 代理模式,Mybatis實現的核心,比如MapperProxy、ConnectionLogger,用的jdk的動態代理;還有executor.loader包使用了cglib或者javassist達到延遲載入的效果;
- 組合模式,例如SqlNode和各個子類ChooseSqlNode等;
- 模板方法模式,例如BaseExecutor和SimpleExecutor,還有BaseTypeHandler和所有的子類例如IntegerTypeHandler;
- 適配器模式,例如Log的Mybatis介面和它對jdbc、log4j等各種日誌框架的適配實現;
- 裝飾者模式,例如Cache包中的cache.decorators子包中等各個裝飾者的實現;
- 迭代器模式,例如迭代器模式PropertyTokenizer;
1、Builder模式
Builder模式的定義是「將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。」,它屬於創建類模式,一般來說,如果一個對象的構建比較複雜,超出了構造函數所能包含的範圍,就可以使用工廠模式和Builder模式,相對於工廠模式會產出一個完整的產品,Builder應用於更加複雜的對象的構建,甚至只會構建產品的一個部分。

在Mybatis環境的初始化過程中,SqlSessionFactoryBuilder會調用XMLConfigBuilder讀取所有的MybatisMapConfig.xml和所有的*Mapper.xml文件,構建Mybatis運行的核心對象Configuration對象,然後將該Configuration對象作為參數構建一個SqlSessionFactory對象。
其中XMLConfigBuilder在構建Configuration對象時,也會調用XMLMapperBuilder用於讀取*Mapper
文件,而XMLMapperBuilder會使用XMLStatementBuilder來讀取和build所有的SQL語句。
在這個過程中,有一個相似的特點,就是這些Builder會讀取文件或者配置,然後做大量的XpathParser解析、配置或語法的解析、反射生成對象、存入結果快取等步驟,這麼多的工作都不是一個構造函數所能包括的,因此大量採用了Builder模式來解決。
對於builder的具體類,方法都大都用build*
開頭,比如SqlSessionFactoryBuilder為例,它包含以下方法:

即根據不同的輸入參數來構建SqlSessionFactory這個工廠對象。
2、工廠模式
在Mybatis中比如SqlSessionFactory使用的是工廠模式,該工廠沒有那麼複雜的邏輯,是一個簡單工廠模式。
簡單工廠模式(Simple Factory Pattern):又稱為靜態工廠方法(Static Factory Method)模式,它屬於類創建型模式。在簡單工廠模式中,可以根據參數的不同返回不同類的實例。簡單工廠模式專門定義一個類來負責創建其他類的實例,被創建的實例通常都具有共同的父類。

SqlSession可以認為是一個Mybatis工作的核心的介面,通過這個介面可以執行執行SQL語句、獲取Mappers、管理事務。類似於連接MySQL的Connection對象。

可以看到,該Factory的openSession方法重載了很多個,分別支援autoCommit、Executor、Transaction等參數的輸入,來構建核心的SqlSession對象。
在DefaultSqlSessionFactory的默認工廠實現里,有一個方法可以看出工廠怎麼產出一個產品:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call // close() throw ExceptionFactory.wrapException(Error opening session. Cause: + e, e); } finally { ErrorContext.instance().reset(); } }
這是一個openSession調用的底層方法,該方法先從configuration讀取對應的環境配置,然後初始化TransactionFactory獲得一個Transaction對象,然後通過Transaction獲取一個Executor對象,最後通過configuration、Executor、是否autoCommit三個參數構建了SqlSession。
在這裡其實也可以看到端倪,SqlSession的執行,其實是委託給對應的Executor來進行的。
而對於LogFactory,它的實現程式碼:
public final classLogFactory{ private static Constructor<? extends Log> logConstructor; private LogFactory() { // disable construction } publicstatic Log getLog(Class<?> aClass) { return getLog(aClass.getName()); }
這裡有個特別的地方,是Log變數的的類型是Constructor,也就是說該工廠生產的不只是一個產品,而是具有Log公共介面的一系列產品,比如Log4jImpl、Slf4jImpl等很多具體的Log。
3、單例模式
單例模式(Singleton Pattern):單例模式確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例,這個類稱為單例類,它提供全局訪問的方法。
單例模式的要點有三個:一是某個類只能有一個實例;二是它必須自行創建這個實例;三是它必須自行向整個系統提供這個實例。單例模式是一種對象創建型模式。單例模式又名單件模式或單態模式。

在Mybatis中有兩個地方用到單例模式,ErrorContext和LogFactory,其中ErrorContext是用在每個執行緒範圍內的單例,用於記錄該執行緒的執行環境錯誤資訊,而LogFactory則是提供給整個Mybatis使用的日誌工廠,用於獲得針對項目配置好的日誌對象。
ErrorContext的單例實現程式碼:
public classErrorContext{ private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>(); privateErrorContext() { } publicstatic ErrorContext instance() { ErrorContext context = LOCAL.get(); if (context == null) { context = new ErrorContext(); LOCAL.set(context); } return context; }
構造函數是private修飾,具有一個static的局部instance變數和一個獲取instance變數的方法,在獲取實例的方法中,先判斷是否為空如果是的話就先創建,然後返回構造好的對象。
只是這裡有個有趣的地方是,LOCAL的靜態實例變數使用了ThreadLocal修飾,也就是說它屬於每個執行緒各自的數據,而在instance()方法中,先獲取本執行緒的該實例,如果沒有就創建該執行緒獨有的ErrorContext。
4、代理模式
代理模式可以認為是Mybatis的核心使用的模式,正是由於這個模式,我們只需要編寫Mapper.java介面,不需要實現,由Mybatis後台幫我們完成具體SQL的執行。
代理模式(Proxy Pattern) :給某一個對象提供一個代 理,並由代理對象控制對原對象的引用。代理模式的英 文叫做Proxy或Surrogate,它是一種對象結構型模式。
代理模式包含如下角色:
- Subject: 抽象主題角色
- Proxy: 代理主題角色
- RealSubject: 真實主題角色


這裡有兩個步驟,第一個是提前創建一個Proxy,第二個是使用的時候會自動請求Proxy,然後由Proxy來執行具體事務;
當我們使用Configuration的getMapper方法時,會調用mapperRegistry.getMapper方法,而該方法又會調用mapperProxyFactory.newInstance(sqlSession)來生成一個具體的代理:
/** * @author Lasse Voss */ public classMapperProxyFactory<T> { private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>(); publicMapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } public Class<T> getMapperInterface() { return mapperInterface; } public Map<Method, MapperMethod> getMethodCache() { return methodCache; } @SuppressWarnings(unchecked) protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } }
在這裡,先通過T newInstance(SqlSession sqlSession)方法會得到一個MapperProxy對象,然後調用T newInstance(MapperProxy mapperProxy)生成代理對象然後返回。
而查看MapperProxy的程式碼,可以看到如下內容:
public classMapperProxy<T> implementsInvocationHandler, 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 if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }
非常典型的,該MapperProxy類實現了InvocationHandler介面,並且實現了該介面的invoke方法。
通過這種方式,我們只需要編寫Mapper.java介面類,當真正執行一個Mapper介面的時候,就會轉發給MapperProxy.invoke方法,而該方法則會調用後續的sqlSession.cud>executor.execute>prepareStatement等一系列方法,完成SQL的執行和返回。
5、組合模式
組合模式組合多個對象形成樹形結構以表示「整體-部分」的結構層次。
組合模式對單個對象(葉子對象)和組合對象(組合對象)具有一致性,它將對象組織到樹結構中,可以用來描述整體與部分的關係。同時它也模糊了簡單元素(葉子對象)和複雜元素(容器對象)的概念,使得客戶能夠像處理簡單元素一樣來處理複雜元素,從而使客戶程式能夠與複雜元素的內部結構解耦。
在使用組合模式中需要注意一點也是組合模式最關鍵的地方:葉子對象和組合對象實現相同的介面。這就是組合模式能夠將葉子節點和對象節點進行一致處理的原因。

Mybatis支援動態SQL的強大功能,比如下面的這個SQL:
<update id=update parameterType=org.format.dynamicproxy.mybatis.bean.User> UPDATE users <trim prefix=SET prefixOverrides=,> <if test=name != null and name != ''> name = #{name} </if> <if test=age != null and age != ''> , age = #{age} </if> <if test=birthday != null and birthday != ''> , birthday = #{birthday} </if> </trim> where id = ${id} </update>
在這裡面使用到了trim、if等動態元素,可以根據條件來生成不同情況下的SQL;
在DynamicSqlSource.getBoundSql方法里,調用了rootSqlNode.apply(context)方法,apply方法是所有的動態節點都實現的介面:
public interfaceSqlNode{ booleanapply(DynamicContext context); }
對於實現該SqlSource介面的所有節點,就是整個組合模式樹的各個節點:

組合模式的簡單之處在於,所有的子節點都是同一類節點,可以遞歸的向下執行,比如對於TextSqlNode,因為它是最底層的葉子節點,所以直接將對應的內容append到SQL語句中:
@Override publicbooleanapply(DynamicContext context) { GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter)); context.appendSql(parser.parse(text)); return true; }
但是對於IfSqlNode,就需要先做判斷,如果判斷通過,仍然會調用子元素的SqlNode,即contents.apply方法,實現遞歸的解析。
@Override publicbooleanapply(DynamicContext context) { if (evaluator.evaluateBoolean(test, context.getBindings())) { contents.apply(context); return true; } return false; }
6、模板方法模式
模板方法模式是所有模式中最為常見的幾個模式之一,是基於繼承的程式碼復用的基本技術。
模板方法模式需要開發抽象類和具體子類的設計師之間的協作。一個設計師負責給出一個演算法的輪廓和骨架,另一些設計師則負責給出這個演算法的各個邏輯步驟。代表這些具體邏輯步驟的方法稱做基本方法(primitive method);而將這些基本方法匯總起來的方法叫做模板方法(template method),這個設計模式的名字就是從此而來。
模板類定義一個操作中的演算法的骨架,而將一些步驟延遲到子類中。使得子類可以不改變一個演算法的結構即可重定義該演算法的某些特定步驟。

在Mybatis中,sqlSession的SQL執行,都是委託給Executor實現的,Executor包含以下結構:

其中的BaseExecutor就採用了模板方法模式,它實現了大部分的SQL執行邏輯,然後把以下幾個方法交給子類訂製化完成:
protectedabstractintdoUpdate(MappedStatement ms, Object parameter) throws SQLException; protectedabstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException; protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
該模板方法類有幾個子類的具體實現,使用了不同的策略:
- 簡單SimpleExecutor:每執行一次update或select,就開啟一個Statement對象,用完立刻關閉Statement對象。(可以是Statement或PrepareStatement對象)
- 重用ReuseExecutor:執行update或select,以sql作為key查找Statement對象,存在就使用,不存在就創建,用完後,不關閉Statement對象,而是放置於Map內,供下一次使用。(可以是Statement或PrepareStatement對象)
- 批量BatchExecutor:執行update(沒有select,JDBC批處理不支援select),將所有sql都添加到批處理中(addBatch()),等待統一執行(executeBatch()),它快取了多個Statement對象,每個Statement對象都是addBatch()完畢後,等待逐一執行executeBatch()批處理的;BatchExecutor相當於維護了多個桶,每個桶里都裝了很多屬於自己的SQL,就像蘋果藍里裝了很多蘋果,番茄藍里裝了很多番茄,最後,再統一倒進倉庫。(可以是Statement或PrepareStatement對象)
比如在SimpleExecutor中這樣實現update方法:
@Override publicintdoUpdate(MappedStatement ms, Object parameter) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.update(stmt); } finally { closeStatement(stmt); } }
7、適配器模式
適配器模式(Adapter Pattern) :將一個介面轉換成客戶希望的另一個介面,適配器模式使介面不兼容的那些類可以一起工作,其別名為包裝器(Wrapper)。適配器模式既可以作為類結構型模式,也可以作為對象結構型模式。

在Mybatsi的logging包中,有一個Log介面:
/** * @author Clinton Begin */ public interfaceLog{ booleanisDebugEnabled(); booleanisTraceEnabled(); voiderror(String s, Throwable e); voiderror(String s); voiddebug(String s); voidtrace(String s); voidwarn(String s); }
該介面定義了Mybatis直接使用的日誌方法,而Log介面具體由誰來實現呢?Mybatis提供了多種日誌框架的實現,這些實現都匹配這個Log介面所定義的介面方法,最終實現了所有外部日誌框架到Mybatis日誌包的適配:

比如對於Log4jImpl的實現來說,該實現持有了org.apache.log4j.Logger的實例,然後所有的日誌方法,均委託該實例來實現。
public classLog4jImplimplementsLog{ private static final String FQCN = Log4jImpl.class.getName(); private Logger log; publicLog4jImpl(String clazz) { log = Logger.getLogger(clazz); } @Override publicbooleanisDebugEnabled() { return log.isDebugEnabled(); } @Override publicbooleanisTraceEnabled() { return log.isTraceEnabled(); } @Override publicvoiderror(String s, Throwable e) { log.log(FQCN, Level.ERROR, s, e); } @Override publicvoiderror(String s) { log.log(FQCN, Level.ERROR, s, null); } @Override publicvoiddebug(String s) { log.log(FQCN, Level.DEBUG, s, null); } @Override publicvoidtrace(String s) { log.log(FQCN, Level.TRACE, s, null); } @Override publicvoidwarn(String s) { log.log(FQCN, Level.WARN, s, null); } }
8、裝飾者模式
裝飾模式(Decorator Pattern) :動態地給一個對象增加一些額外的職責(Responsibility),就增加對象功能來說,裝飾模式比生成子類實現更為靈活。其別名也可以稱為包裝器(Wrapper),與適配器模式的別名相同,但它們適用於不同的場合。根據翻譯的不同,裝飾模式也有人稱之為「油漆工模式」,它是一種對象結構型模式。

在mybatis中,快取的功能由根介面Cache(org.apache.ibatis.cache.Cache)定義。整個體系採用裝飾器設計模式,數據存儲和快取的基本功能由PerpetualCache(org.apache.ibatis.cache.impl.PerpetualCache)永久快取實現,然後通過一系列的裝飾器來對PerpetualCache永久快取進行快取策略等方便的控制。如下圖:

用於裝飾PerpetualCache的標準裝飾器共有8個(全部在org.apache.ibatis.cache.decorators包中):
- FifoCache:先進先出演算法,快取回收策略
- LoggingCache:輸出快取命中的日誌資訊
- LruCache:最近最少使用演算法,快取回收策略
- ScheduledCache:調度快取,負責定時清空快取
- SerializedCache:快取序列化和反序列化存儲
- SoftCache:基於軟引用實現的快取管理策略
- SynchronizedCache:同步的快取裝飾器,用於防止多執行緒並發訪問
- WeakCache:基於弱引用實現的快取管理策略
另外,還有一個特殊的裝飾器TransactionalCache:事務性的快取
正如大多數持久層框架一樣,mybatis快取同樣分為一級快取和二級快取
- 一級快取,又叫本地快取,是PerpetualCache類型的永久快取,保存在執行器中(BaseExecutor),而執行器又在SqlSession(DefaultSqlSession)中,所以一級快取的生命周期與SqlSession是相同的。
- 二級快取,又叫自定義快取,實現了Cache介面的類都可以作為二級快取,所以可配置如encache等的第三方快取。二級快取以namespace名稱空間為其唯一標識,被保存在Configuration核心配置對象中。
二級快取對象的默認類型為PerpetualCache,如果配置的快取是默認類型,則mybatis會根據配置自動追加一系列裝飾器。
Cache對象之間的引用順序為:
SynchronizedCache–>LoggingCache–>SerializedCache–>ScheduledCache–>LruCache–>PerpetualCache
9、迭代器模式
迭代器(Iterator)模式,又叫做游標(Cursor)模式。GOF給出的定義為:提供一種方法訪問一個容器(container)對象中各個元素,而又不需暴露該對象的內部細節。

Java的Iterator就是迭代器模式的介面,只要實現了該介面,就相當於應用了迭代器模式:

比如Mybatis的PropertyTokenizer是property包中的重量級類,該類會被reflection包中其他的類頻繁的引用到。這個類實現了Iterator介面,在使用時經常被用到的是Iterator介面中的hasNext這個函數。
public classPropertyTokenizerimplementsIterator<PropertyTokenizer> { private String name; private String indexedName; private String index; private String children; publicPropertyTokenizer(String fullname) { int delim = fullname.indexOf('.'); if (delim > -1) { name = fullname.substring(0, delim); children = fullname.substring(delim + 1); } else { name = fullname; children = null; } indexedName = name; delim = name.indexOf('['); if (delim > -1) { index = name.substring(delim + 1, name.length() - 1); name = name.substring(0, delim); } } public String getName() { return name; } public String getIndex() { return index; } public String getIndexedName() { return indexedName; } public String getChildren() { return children; } @Override publicbooleanhasNext() { return children != null; } @Override public PropertyTokenizer next() { return new PropertyTokenizer(children); } @Override publicvoidremove() { throw new UnsupportedOperationException( Remove is not supported, as it has no meaning in the context of properties.); } }
可以看到,這個類傳入一個字元串到構造函數,然後提供了iterator方法對解析後的子串進行遍歷,是一個很常用的方法類。
來源 | http://www.crazyant.net/2022.html
參考資料:
- 圖說設計模式
- http://design-patterns.readthedocs.io/zh_CN/latest/index.html
- 深入淺出Mybatis系列(十)—SQL執行流程分析(源碼篇)
- http://www.cnblogs.com/dongying/p/4142476.html
- 設計模式讀書筆記—–組合模式
- http://www.cnblogs.com/chenssy/p/3299719.html
- Mybatis3.3.x技術內幕(四):五鼠鬧東京之執行器Executor設計原本
- http://blog.csdn.net/wagcy/article/details/32963235
- mybatis快取機制詳解(一)——Cache
- https://my.oschina.net/lixin91/blog/620068
正文結束
掌上編程往期精彩文章 架構師必備最全SQL優化方案如何在 GitHub 上面精準搜索開源項目?掌握中台系統,需要了解哪些技術? 對於 Ping 的過程,你真的了解嗎? 6 個接私活的網站,你有技術就有錢!推薦給大家! 一個吊打百度網盤的開源神器,還是99年妹子開發的! 黑客入侵你Linux伺服器的一萬種玩法... 一個基於 RabbitMQ 的可復用的分散式事務消息架構方案! 一份tcp、http面試指南,常考點都給你了 美國公布長達35頁的《2016-2045年新興科技趨勢報告》掌上部落格給個[在看],是對掌上編程最大的支援