一起來看看 Mybatis 中使用的 9 種設計模式!

Mybatis 源碼中使用了大量的設計模式,閱讀源碼並觀察設計模式在其中的應用,能夠更深入的理解設計模式。

Mybatis 至少遇到了以下的設計模式的使用:

  1. Builder 模式,例如 SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder;

  2. 工廠模式,例如 SqlSessionFactory、ObjectFactory、MapperProxyFactory;

  3. 單例模式,例如 ErrorContext 和 LogFactory;

  4. 代理模式,Mybatis 實現的核心,比如 MapperProxy、ConnectionLogger,用的 jdk 的動態代理;還有 executor.loader 包使用了 cglib 或者 javassist 達到延遲加載的效果;

  5. 組合模式,例如 SqlNode 和各個子類 ChooseSqlNode 等;

  6. 模板方法模式,例如 BaseExecutor 和 SimpleExecutor,還有 BaseTypeHandler 和所有的子類例如 IntegerTypeHandler;

  7. 適配器模式,例如 Log 的 Mybatis 接口和它對 jdbc、log4j 等各種日誌框架的適配實現;

  8. 裝飾者模式,例如 Cache 包中的 cache.decorators 子包中等各個裝飾者的實現;

  9. 迭代器模式,例如迭代器模式 PropertyTokenizer;

接下來挨個模式進行解讀,先介紹模式自身的知識,然後解讀在 Mybatis 中怎樣應用了該模式。

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 的默認工廠實現裏,有一個方法可以看出工廠怎麼產出一個產品:

 1private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,
 2			boolean autoCommit) {
 3		Transaction tx = null;
 4		try {
 5			final Environment environment = configuration.getEnvironment();
 6			final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
 7			tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
 8			final Executor executor = configuration.newExecutor(tx, execType);
 9			returnnew DefaultSqlSession(configuration, executor, autoCommit);
10		} catch (Exception e) {
11			closeTransaction(tx); // may have fetched a connection so lets call
12									// close()
13			throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
14		} finally {
15			ErrorContext.instance().reset();
16		}
17	}
18
19

這是一個 openSession 調用的底層方法,該方法先從 configuration 讀取對應的環境配置,然後初始化 TransactionFactory 獲得一個 Transaction 對象

然後通過 Transaction 獲取一個 Executor 對象,最後通過 configuration、Executor、是否 autoCommit 三個參數構建了 SqlSession。

在這裏其實也可以看到端倪,SqlSession 的執行,其實是委託給對應的 Executor 來進行的。

而對於 LogFactory,它的實現代碼:

 1publicfinalclass LogFactory {
 2	privatestatic Constructor logConstructor;
 3
 4	private LogFactory() {
 5		// disable construction
 6	}
 7
 8	public static Log getLog(Class aClass) {
 9		return getLog(aClass.getName());
10	}
11}
12
13

這裏有個特別的地方,Log 變量的的類型是 Constructorextends Log>

也就是說該工廠生產的不只是一個產品,而是具有 Log 公共接口的一系列產品,比如 Log4jImpl、Slf4jImpl 等很多具體的 Log。

3、單例模式

單例模式 (Singleton Pattern):單例模式確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例,這個類稱爲單例類,它提供全局訪問的方法。

單例模式的要點有三個:一是某個類只能有一個實例;二是它必須自行創建這個實例;三是它必須自行向整個系統提供這個實例。

單例模式是一種對象創建型模式,單例模式又名單件模式或單態模式。

在 Mybatis 中有兩個地方用到單例模式,ErrorContext 和 LogFactory,其中 ErrorContext 是用在每個線程範圍內的單例,用於記錄該線程的執行環境錯誤信息

而 LogFactory 則是提供給整個 Mybatis 使用的日誌工廠,用於獲得針對項目配置好的日誌對象。

ErrorContext 的單例實現代碼:

 1publicclass ErrorContext {
 2
 3	privatestaticfinal ThreadLocalLOCAL= new ThreadLocal();
 4
 5	private ErrorContext() {
 6	}
 7
 8	public static ErrorContext instance() {
 9		ErrorContext context = LOCAL.get();
10		if (context == null) {
11			context = new ErrorContext();
12			LOCAL.set(context);
13		}
14		return context;
15	}
16}
17
18

構造函數是 private 修飾,具有一個 static 的局部 instance 變量和一個獲取 instance 變量的方法,在獲取實例的方法中,先判斷是否爲空如果是的話就先創建,然後返回構造好的對象。

只是這裏有個有趣的地方是,LOCAL 的靜態實例變量使用了 ThreadLocal 修飾,也就是說它屬於每個線程各自的數據,而在 instance() 方法中,先獲取本線程的該實例,如果沒有就創建該線程獨有的 ErrorContext。

4、代理模式

代理模式可以認爲是 Mybatis 的核心使用的模式,正是由於這個模式,我們只需要編寫 Mapper.java 接口,不需要實現,由 Mybatis 後臺幫我們完成具體 SQL 的執行。

代理模式 (Proxy Pattern) :給某一個對象提供一個代 理,並由代理對象控制對原對象的引用。代理模式的英 文叫做 Proxy 或 Surrogate,它是一種對象結構型模式。

代理模式包含如下角色:

這裏有兩個步驟,第一個是提前創建一個 Proxy,第二個是使用的時候會自動請求 Proxy,然後由 Proxy 來執行具體事務;

當我們使用 Configuration 的 getMapper 方法時,會調用 mapperRegistry.getMapper 方法,而該方法又會調用 mapperProxyFactory.newInstance(sqlSession) 來生成一個具體的代理:

 1/**
 2 * @author Lasse Voss
 3 */
 4publicclass MapperProxyFactory<T> {
 5
 6	privatefinal ClassmapperInterface;
 7	private final MapmethodCache = new ConcurrentHashMap();
 8
 9	public MapperProxyFactory(ClassmapperInterface) {
10		this.mapperInterface = mapperInterface;
11	}
12
13	public ClassgetMapperInterface() {
14		return mapperInterface;
15	}
16
17	public MapgetMethodCache() {
18		return methodCache;
19	}
20
21	@SuppressWarnings("unchecked")
22	protected T newInstance(MapperProxymapperProxy) {
23		return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface },
24				mapperProxy);
25	}
26
27	public T newInstance(SqlSession sqlSession) {
28		final MapperProxymapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache);
29		return newInstance(mapperProxy);
30	}
31
32}
33
34

在這裏,先通過 T newInstance(SqlSession sqlSession) 方法會得到一個 MapperProxy 對象,然後調用 T newInstance(MapperProxymapperProxy) 生成代理對象然後返回。

而查看 MapperProxy 的代碼,可以看到如下內容:

 1publicclass MapperProxy<T> implements InvocationHandler, Serializable {
 2
 3	@Override
 4	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 5		try {
 6			if (Object.class.equals(method.getDeclaringClass())) {
 7				return method.invoke(this, args);
 8			} elseif (isDefaultMethod(method)) {
 9				return invokeDefaultMethod(proxy, method, args);
10			}
11		} catch (Throwable t) {
12			throw ExceptionUtil.unwrapThrowable(t);
13		}
14		final MapperMethod mapperMethod = cachedMapperMethod(method);
15		return mapperMethod.execute(sqlSession, args);
16	}
17}
18
19

非常典型的,該 MapperProxy 類實現了 InvocationHandler 接口,並且實現了該接口的 invoke 方法。

通過這種方式,我們只需要編寫 Mapper.java 接口類,當真正執行一個 Mapper 接口的時候,就會轉發給 MapperProxy.invoke 方法

而該方法則會調用後續的 sqlSession.cud>executor.execute>prepareStatement 等一系列方法,完成 SQL 的執行和返回。

5、組合模式

組合模式組合多個對象形成樹形結構以表示 “整體 - 部分” 的結構層次。

組合模式對單個對象 (葉子對象) 和組合對象 (組合對象) 具有一致性,它將對象組織到樹結構中,可以用來描述整體與部分的關係。

同時它也模糊了簡單元素 (葉子對象) 和複雜元素 (容器對象) 的概念,使得客戶能夠像處理簡單元素一樣來處理複雜元素,從而使客戶程序能夠與複雜元素的內部結構解耦。

在使用組合模式中需要注意一點也是組合模式最關鍵的地方:葉子對象和組合對象實現相同的接口。這就是組合模式能夠將葉子節點和對象節點進行一致處理的原因。

Mybatis 支持動態 SQL 的強大功能,比如下面的這個 SQL:

在這裏面使用到了 trim、if 等動態元素,可以根據條件來生成不同情況下的 SQL;

在 DynamicSqlSource.getBoundSql 方法裏,調用了 rootSqlNode.apply(context) 方法,apply 方法是所有的動態節點都實現的接口:

1publicinterface SqlNode {
2	boolean apply(DynamicContext context);
3}
4
5

對於實現該 SqlSource 接口的所有節點,就是整個組合模式樹的各個節點:

組合模式的簡單之處在於,所有的子節點都是同一類節點,可以遞歸的向下執行,比如對於 TextSqlNode,因爲它是最底層的葉子節點,所以直接將對應的內容 append 到 SQL 語句中:

1@Override
2	public boolean apply(DynamicContext context) {
3		GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
4		context.appendSql(parser.parse(text));
5		returntrue;
6	}
7
8

但是對於 IfSqlNode,就需要先做判斷,如果判斷通過,仍然會調用子元素的 SqlNode,即 contents.apply 方法,實現遞歸的解析。

 1@Override
 2	public boolean apply(DynamicContext context) {
 3		if (evaluator.evaluateBoolean(test, context.getBindings())) {
 4			contents.apply(context);
 5			returntrue;
 6		}
 7		returnfalse;
 8	}
 9
10

6、模板方法模式

模板方法模式是所有模式中最爲常見的幾個模式之一,是基於繼承的代碼複用的基本技術。

模板方法模式需要開發抽象類和具體子類的設計師之間的協作。一個設計師負責給出一個算法的輪廓和骨架,另一些設計師則負責給出這個算法的各個邏輯步驟。

代表這些具體邏輯步驟的方法稱做基本方法 (primitive method);而將這些基本方法彙總起來的方法叫做模板方法 (template method),這個設計模式的名字就是從此而來。

模板類定義一個操作中的算法的骨架,而將一些步驟延遲到子類中。使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。

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

其中的 BaseExecutor 就採用了模板方法模式,它實現了大部分的 SQL 執行邏輯,然後把以下幾個方法交給子類定製化完成:

1protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
2
3	protected abstract ListdoFlushStatements(boolean isRollback) throws SQLException;
4
5	protectedabstractListdoQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,
6			ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
7
8

該模板方法類有幾個子類的具體實現,使用了不同的策略:

比如在 SimpleExecutor 中這樣實現 update 方法:

 1@Override
 2	public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
 3		Statement stmt = null;
 4		try {
 5			Configuration configuration = ms.getConfiguration();
 6			StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null,
 7					null);
 8			stmt = prepareStatement(handler, ms.getStatementLog());
 9			return handler.update(stmt);
10		} finally {
11			closeStatement(stmt);
12		}
13	}
14
15

7、適配器模式

適配器模式 (Adapter Pattern) :將一個接口轉換成客戶希望的另一個接口,適配器模式使接口不兼容的那些類可以一起工作,其別名爲包裝器 (Wrapper)。

適配器模式既可以作爲類結構型模式,也可以作爲對象結構型模式。

在 Mybatsi 的 logging 包中,有一個 Log 接口:

 1/**
 2 * @author Clinton Begin
 3 */
 4publicinterface Log {
 5
 6	boolean isDebugEnabled();
 7
 8	boolean isTraceEnabled();
 9
10	void error(String s, Throwable e);
11
12	void error(String s);
13
14	void debug(String s);
15
16	void trace(String s);
17
18	void warn(String s);
19
20}
21
22

該接口定義了 Mybatis 直接使用的日誌方法,而 Log 接口具體由誰來實現呢?

Mybatis 提供了多種日誌框架的實現,這些實現都匹配這個 Log 接口所定義的接口方法,最終實現了所有外部日誌框架到 Mybatis 日誌包的適配:

比如對於 Log4jImpl 的實現來說,該實現持有了 org.apache.log4j.Logger 的實例,然後所有的日誌方法,均委託該實例來實現。

 1publicclass Log4jImpl implements Log {
 2
 3	privatestaticfinal String FQCN = Log4jImpl.class.getName();
 4
 5	private Logger log;
 6
 7	public Log4jImpl(String clazz) {
 8		log = Logger.getLogger(clazz);
 9	}
10
11	@Override
12	public boolean isDebugEnabled() {
13		return log.isDebugEnabled();
14	}
15
16	@Override
17	public boolean isTraceEnabled() {
18		return log.isTraceEnabled();
19	}
20
21	@Override
22	public void error(String s, Throwable e) {
23		log.log(FQCN, Level.ERROR, s, e);
24	}
25
26	@Override
27	public void error(String s) {
28		log.log(FQCN, Level.ERROR, s, null);
29	}
30
31	@Override
32	public void debug(String s) {
33		log.log(FQCN, Level.DEBUG, s, null);
34	}
35
36	@Override
37	public void trace(String s) {
38		log.log(FQCN, Level.TRACE, s, null);
39	}
40
41	@Override
42	public void warn(String s) {
43		log.log(FQCN, Level.WARN, s, null);
44	}
45
46}
47
48

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 包中):

  1. FifoCache:先進先出算法,緩存回收策略

  2. LoggingCache:輸出緩存命中的日誌信息

  3. LruCache:最近最少使用算法,緩存回收策略

  4. ScheduledCache:調度緩存,負責定時清空緩存

  5. SerializedCache:緩存序列化和反序列化存儲

  6. SoftCache:基於軟引用實現的緩存管理策略

  7. SynchronizedCache:同步的緩存裝飾器,用於防止多線程併發訪問

  8. WeakCache:基於弱引用實現的緩存管理策略

另外,還有一個特殊的裝飾器 TransactionalCache:事務性的緩存

正如大多數持久層框架一樣,mybatis 緩存同樣分爲一級緩存和二級緩存

二級緩存對象的默認類型爲 PerpetualCache,如果配置的緩存是默認類型,則 mybatis 會根據配置自動追加一系列裝飾器。

Cache 對象之間的引用順序爲:

SynchronizedCache–>LoggingCache–>SerializedCache–>ScheduledCache–>LruCache–>PerpetualCache

9、迭代器模式

迭代器(Iterator)模式,又叫做遊標(Cursor)模式。GOF 給出的定義爲:提供一種方法訪問一個容器(container)對象中各個元素,而又不需暴露該對象的內部細節。

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

比如 Mybatis 的 PropertyTokenizer 是 property 包中的重量級類,該類會被 reflection 包中其他的類頻繁的引用到。這個類實現了 Iterator 接口,在使用時經常被用到的是 Iterator 接口中的 hasNext 這個函數。

 1publicclass PropertyTokenizer implements Iterator<PropertyTokenizer> {
 2	private String name;
 3	private String indexedName;
 4	private String index;
 5	private String children;
 6
 7	public PropertyTokenizer(String fullname) {
 8		int delim = fullname.indexOf('.');
 9		if (delim > -1) {
10			name = fullname.substring(0, delim);
11			children = fullname.substring(delim + 1);
12		} else {
13			name = fullname;
14			children = null;
15		}
16		indexedName = name;
17		delim = name.indexOf('[');
18		if (delim > -1) {
19			index = name.substring(delim + 1, name.length() - 1);
20			name = name.substring(0, delim);
21		}
22	}
23
24	public String getName() {
25		return name;
26	}
27
28	public String getIndex() {
29		return index;
30	}
31
32	public String getIndexedName() {
33		return indexedName;
34	}
35
36	public String getChildren() {
37		return children;
38	}
39
40	@Override
41	public boolean hasNext() {
42		return children != null;
43	}
44
45	@Override
46	public PropertyTokenizer next() {
47		returnnew PropertyTokenizer(children);
48	}
49
50	@Override
51	public void remove() {
52		thrownew UnsupportedOperationException(
53				"Remove is not supported, as it has no meaning in the context of properties.");
54	}
55}
56
57

可以看到,這個類傳入一個字符串到構造函數,然後提供了 iterator 方法對解析後的子串進行遍歷,是一個很常用的方法類。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/aH7gGmuXe50R9koYUZPM-g