面官問你Mybatis用了哪些設計模式你應該知道這些

前語:

不要爲了讀文章而讀文章,必定要帶着問題來讀文章,勤思考。 雖然咱們都知道有26個設計模式,可是大多停留在概念層面,真實開發中不多遇到,Mybatis源碼中使用了大量的設計模式,閱讀源碼並觀察設計模式在其中的應用,可以更深刻的理解設計模式。java

Mybatis至少遇到了如下的設計模式的使用:算法

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

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

單例模式,例如ErrorContext和LogFactory;設計模式

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

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

模板方法模式,例如BaseExecutor和SimpleExecutor,還有BaseTypeHandler和全部的子類例如IntegerTypeHandler;session

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

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

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

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

一、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這個工廠對象。

二、工廠模式

在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 class LogFactory {
  private static Constructor<? extends Log> logConstructor;
 
  private LogFactory() {
    // disable construction
  }
 
  public static Log getLog(Class<?> aClass) {
    return getLog(aClass.getName());
  }
複製代碼

這裏有個特別的地方,是Log變量的的類型是Constructor<? extends Log>,也就是說該工廠生產的不僅是一個產品,而是具備Log公共接口的一系列產品,好比Log4jImpl、Slf4jImpl等不少具體的Log。

三、單例模式

單例模式(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。

四、代理模式

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

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

代理模式包含以下角色:

Subject: 抽象主題角色

Proxy: 代理主題角色

RealSubject: 真實主題角色

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

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

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 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的執行和返回。

五、組合模式

組合模式組合多個對象造成樹形結構以表示「總體-部分」的結構層次。

組合模式對單個對象(葉子對象)和組合對象(組合對象)具備一致性,它將對象組織到樹結構中,能夠用來描述總體與部分的關係。同時它也模糊了簡單元素(葉子對象)和複雜元素(容器對象)的概念,使得客戶可以像處理簡單元素同樣來處理複雜元素,從而使客戶程序可以與複雜元素的內部結構解耦。

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

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接口的全部節點,就是整個組合模式樹的各個節點:

組合模式的簡單之處在於,全部的子節點都是同一類節點,能夠遞歸的向下執行,好比對於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;
}
複製代碼

六、模板方法模式

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

模板方法模式須要開發抽象類和具體子類的設計師之間的協做。一個設計師負責給出一個算法的輪廓和骨架,另外一些設計師則負責給出這個算法的各個邏輯步驟。表明這些具體邏輯步驟的方法稱作基本方法(primitive method);而將這些基本方法彙總起來的方法叫作模板方法(template method),這個設計模式的名字就是今後而來。

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

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

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

protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;

protected abstract 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<String, Statement>內,供下一次使用。(能夠是Statement或PrepareStatement對象)

批量BatchExecutor:執行update(沒有select,JDBC批處理不支持select),將全部sql都添加到批處理中(addBatch()),等待統一執行(executeBatch()),它緩存了多個Statement對象,每一個Statement對象都是addBatch()完畢後,等待逐一執行executeBatch()批處理的;BatchExecutor至關於維護了多個桶,每一個桶裏都裝了不少屬於本身的SQL,就像蘋果藍裏裝了不少蘋果,番茄藍裏裝了不少番茄,最後,再統一倒進倉庫。(能夠是Statement或PrepareStatement對象)

好比在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);
    }
  }
複製代碼

七、適配器模式

適配器模式(Adapter Pattern) :將一個接口轉換成客戶但願的另外一個接口,適配器模式使接口不兼容的那些類能夠一塊兒工做,其別名爲包裝器(Wrapper)。適配器模式既能夠做爲類結構型模式,也能夠做爲對象結構型模式。

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

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);
  }
複製代碼

八、裝飾者模式

裝飾模式(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

九、迭代器模式

迭代器(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方法對解析後的子串進行遍歷,是一個很經常使用的方法類。

相關文章
相關標籤/搜索