面試官:“談談MyBatis中都用到了那些設計模式?”。
瘋狂的螞蟻 JavaGuide
雖然我們都知道有26個設計模式,但是大多停留在概念層面,真實開發中很少遇到,Mybatis源碼中使用了大量的設計模式,閱讀源碼并觀察設計模式在其中的應用,能夠更深入的了解設計模式。
目錄
面試官:“談談MyBatis中都用到了那些設計模式?”。
1、Builder 模式
2、工廠模式
3、單例模式
4、代理模式
5、組合模式
6、模闆方法模式
7、擴充卡模式
8、裝飾者模式
9、疊代器模式
參考資料
Mybatis至少遇到了以下的設計模式的使用:
-
Builder模式 :
例如
、SqlSessionFactoryBuilder
、XMLConfigBuilder
、XMLMapperBuilder
、XMLStatementBuilder
;CacheBuilder
-
工廠模式 :
例如
、SqlSessionFactory
、ObjectFactory
;MapperProxyFactory
- 單例模式 :例如
和ErrorContext
;LogFactory
- 代理模式 :Mybatis實作的核心,比如
、MapperProxy
,用的jdk的動态代理;還有ConnectionLogger
包使用了cglib或者javassist達到延遲加載的效果;executor.loader
- 組合模式 :例如
和各個子類SqlNode
等;ChooseSqlNode
- 模闆方法模式 : 例如
和BaseExecutor
,還有SimpleExecutor
和所有的子類例如BaseTypeHandler
;IntegerTypeHandler
- 擴充卡模式 : 例如Log的Mybatis接口和它對jdbc、log4j等各種日志架構的适配實作;
- 裝飾者模式 : 例如
包中的cache
子包中等各個裝飾者的實作;cache.decorators
- 疊代器模式 : 例如疊代器模式
;PropertyTokenizer
接下來挨個模式進行解讀,先介紹模式自身的知識,然後解讀在Mybatis中怎樣應用了該模式。
1、Builder 模式
Builder模式的定義是“将一個複雜對象的建構與它的表示分離,使得同樣的建構過程可以建立不同的表示。”,它屬于建立類模式,一般來說,如果一個對象的建構比較複雜,超出了構造函數所能包含的範圍,就可以使用工廠模式和Builder模式,相對于工廠模式會産出一個完整的産品,Builder應用于更加複雜的對象的建構,甚至隻會建構産品的一個部分。《effective-java》中第2條也提到:遇到多個構造器參數時,考慮用建構者(Builder)模式。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5yN4ITM3Y2M4QGZxQTY5IDNzYzX5ADN1cTM5AzLcBTMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
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
為例,它包含以下方法:
SqlSessionFactoryBuilder
即根據不同的輸入參數來建構
SqlSessionFactory
這個工廠對象。
2、工廠模式
在Mybatis中比如
SqlSessionFactory
使用的是工廠模式,該工廠沒有那麼複雜的邏輯,是一個簡單工廠模式。
簡單工廠模式(Simple Factory Pattern):又稱為靜态工廠方法(Static Factory Method)模式,它屬于類建立型模式。在簡單工廠模式中,可以根據參數的不同傳回不同類的執行個體。簡單工廠模式專門定義一個類來負責建立其他類的執行個體,被建立的執行個體通常都具有共同的父類。
簡單工廠模式
SqlSession
可以認為是一個Mybatis工作的核心的接口,通過這個接口可以執行執行SQL語句、擷取Mappers、管理事務。類似于連接配接MySQL的
Connection
對象。
SqlSessionFactory
可以看到,該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 class LogFactory {
private static Constructor<? extends Log> logConstructor;
private LogFactory() {
// disable construction
}
public static Log getLog(Class<?> aClass) {
return getLog(aClass.getName());
}
這裡有個特别的地方,是Log變量的的類型是
Constructor<? extendsLog>
,也就是說該工廠生産的不隻是一個産品,而是具有Log公共接口的一系列産品,比如
Log4jImpl
、
Slf4jImpl
等很多具體的Log。
3、單例模式
單例模式(Singleton Pattern):單例模式確定某一個類隻有一個執行個體,而且自行執行個體化并向整個系統提供這個執行個體,這個類稱為單例類,它提供全局通路的方法。
單例模式的要點有三個:一是某個類隻能有一個執行個體;二是它必須自行建立這個執行個體;三是它必須自行向整個系統提供這個執行個體。單例模式是一種對象建立型模式。單例模式又名單件模式或單态模式。
單例模式
在Mybatis中有兩個地方用到單例模式,
ErrorContext
和
LogFactory
,其中
ErrorContext
是用在每個線程範圍内的單例,用于記錄該線程的執行環境錯誤資訊,而
LogFactory
則是提供給整個Mybatis使用的日志工廠,用于獲得針對項目配置好的日志對象。
ErrorContext
的單例實作代碼:
public class ErrorContext {
private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
private ErrorContext() {
}
public static 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 class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
public MapperProxyFactory(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<T> mapperProxy)
生成代理對象然後傳回。
而檢視
MapperProxy
的代碼,可以看到如下内容:
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 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 interface SqlNode {
boolean apply(DynamicContext context);
}
對于實作該
SqlSource
接口的所有節點,就是整個組合模式樹的各個節點:
SqlNode
組合模式的簡單之處在于,所有的子節點都是同一類節點,可以遞歸的向下執行,比如對于TextSqlNode,因為它是最底層的葉子節點,是以直接将對應的内容append到SQL語句中:
@Override
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}
但是對于IfSqlNode,就需要先做判斷,如果判斷通過,仍然會調用子元素的SqlNode,即
contents.apply
方法,實作遞歸的解析。
@Override
public boolean apply(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包含以下結構:
Executor接口
其中的BaseExecutor就采用了模闆方法模式,它實作了大部分的SQL執行邏輯,然後把以下幾個方法交給子類定制化完成:
@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
該模闆方法類有幾個子類的具體實作,使用了不同的政策:
- 簡單
:每執行一次SimpleExecutor
或update
,就開啟一個select
對象,用完立刻關閉Statement
對象。(可以是Statement
或Statement
對象)PrepareStatement
- 重用
:執行ReuseExecutor
或update
,以sql作為key查找select
對象,存在就使用,不存在就建立,用完後,不關閉Statement
對象,而是放置于Statement
内,供下一次使用。(可以是Map
或Statement
對象)PrepareStatement
- 批量
:執行update(沒有select,JDBC批處理不支援select),将所有sql都添加到批進行中(BatchExecutor
),等待統一執行(addBatch()
),它緩存了多個Statement對象,每個Statement對象都是executeBatch()
完畢後,等待逐一執行addBatch()
批處理的;executeBatch()
相當于維護了多個桶,每個桶裡都裝了很多屬于自己的SQL,就像蘋果藍裡裝了很多蘋果,番茄藍裡裝了很多番茄,最後,再統一倒進倉庫。(可以是Statement或PrepareStatement對象)BatchExecutor
比如在SimpleExecutor中這樣實作update方法:
@Override
public int doUpdate(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 interface Log {
boolean isDebugEnabled();
boolean isTraceEnabled();
void error(String s, Throwable e);
void error(String s);
void debug(String s);
void trace(String s);
void warn(String s);
}
該接口定義了Mybatis直接使用的日志方法,而Log接口具體由誰來實作呢?Mybatis提供了多種日志架構的實作,這些實作都比對這個Log接口所定義的接口方法,最終實作了所有外部日志架構到Mybatis日志包的适配:
比如對于
Log4jImpl
的實作來說,該實作持有了
org.apache.log4j.Logger
的執行個體,然後所有的日志方法,均委托該執行個體來實作。
public class Log4jImpl implements Log {
private static final String FQCN = Log4jImpl.class.getName();
private Logger log;
public Log4jImpl(String clazz) {
log = Logger.getLogger(clazz);
}
@Override
public boolean isDebugEnabled() {
return log.isDebugEnabled();
}
@Override
public boolean isTraceEnabled() {
return log.isTraceEnabled();
}
@Override
public void error(String s, Throwable e) {
log.log(FQCN, Level.ERROR, s, e);
}
@Override
public void error(String s) {
log.log(FQCN, Level.ERROR, s, null);
}
@Override
public void debug(String s) {
log.log(FQCN, Level.DEBUG, s, null);
}
@Override
public void trace(String s) {
log.log(FQCN, Level.TRACE, s, null);
}
@Override
public void warn(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
永久緩存進行緩存政策等友善的控制。如下圖:
Cache
用于裝飾PerpetualCache的标準裝飾器共有8個(全部在org.apache.ibatis.cache.decorators包中):
-
:先進先出算法,緩存回收政策FifoCache
-
:輸出緩存命中的日志資訊LoggingCache
-
:最近最少使用算法,緩存回收政策LruCache
-
:排程緩存,負責定時清空緩存ScheduledCache
-
:緩存序列化和反序列化存儲SerializedCache
-
:基于軟引用實作的緩存管理政策SoftCache
-
:同步的緩存裝飾器,用于防止多線程并發通路SynchronizedCache
-
:基于弱引用實作的緩存管理政策WeakCache
另外,還有一個特殊的裝飾器
TransactionalCache
:事務性的緩存
正如大多數持久層架構一樣,mybatis緩存同樣分為一級緩存和二級緩存
- 一級緩存,又叫本地緩存,是
類型的永久緩存,儲存在執行器中(PerpetualCache
),而執行器又在BaseExecutor
(SqlSession
)中,是以一級緩存的生命周期與DefaultSqlSession
是相同的。SqlSession
- 二級緩存,又叫自定義緩存,實作了
接口的類都可以作為二級緩存,是以可配置如encache等的第三方緩存。二級緩存以namespace名稱空間為其唯一辨別,被儲存在Cache
核心配置對象中。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 class PropertyTokenizer implements Iterator<PropertyTokenizer> {
private String name;
private String indexedName;
private String index;
private String children;
public PropertyTokenizer(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
public boolean hasNext() {
return children != null;
}
@Override
public PropertyTokenizer next() {
return new PropertyTokenizer(children);
}
@Override
public void remove() {
throw new UnsupportedOperationException(
"Remove is not supported, as it has no meaning in the context of properties.");
}
}
可以看到,這個類傳入一個字元串到構造函數,然後提供了iterator方法對解析後的子串進行周遊,是一個很常用的方法類。
參考資料
•圖說設計模式:http://design-patterns.readthedocs.io/zh_CN/latest/index.html