天天看點

硬核!五千字帶你從設計模式去解讀Mybatis源碼思維導圖概述工廠模式單例模式建造者模式模闆模式代理模式裝飾器模式總結

思維導圖

硬核!五千字帶你從設計模式去解讀Mybatis源碼思維導圖概述工廠模式單例模式建造者模式模闆模式代理模式裝飾器模式總結
文章已收錄Github精選,歡迎Star: https://github.com/yehongzhi/learningSummary

概述

Mybatis是一個比較主流的ORM架構,是以在日常工作中接觸得很多。我比較喜歡看優秀架構的源碼,因為能寫出這種架構的作者肯定有其獨特之處。如果能看懂源碼的一些巧妙構思,一定是受益匪淺的。

所謂萬事開頭難,看源碼也要找到切入的點。設計模式無疑是源碼分析一個很好的切入點,廢話不多說,那麼我們就開始吧。

工廠模式

工廠模式屬于建立型模式。工廠模式的作用是把建立對象的邏輯封裝起來,提供一個接口給外部建立對象,降低類與類之間的耦合。

在Mybatis中,用到工廠模式主要在DataSourceFactory。這是一個負責建立DataSource資料源的工廠。DataSourceFactory是一個接口,有不同的子類實作,根據不同的配置,生成不同的DataSourceFactory實作類。類圖如下:

硬核!五千字帶你從設計模式去解讀Mybatis源碼思維導圖概述工廠模式單例模式建造者模式模闆模式代理模式裝飾器模式總結

接着我們看一下DataSourceFactory的源碼:

public interface DataSourceFactory {
  
  void setProperties(Properties props);

  DataSource getDataSource();

}           

DataSourceFactory接口定義了兩個抽象方法,怎麼工作的呢,其實是跟dataSource标簽的屬性type有關。

<environment id="development">
    <transactionManager type="JDBC"/>
    <!-- type屬性是關鍵屬性 -->
    <dataSource type="POOLED">
        <property name="driver" value="${dataSource.driver}"/>
        <property name="url" value="${dataSource.url}"/>
        <property name="username" value="${dataSource.username}"/>
        <property name="password" value="${dataSource.password}"/>
    </dataSource>
</environment>           

Mybatis内置的type有三種配置,分别是UNPOOLED,POOLED,JNDI。

UNPOOLED,這個資料源的實作隻是每次被請求時打開和關閉連接配接。

POOLED,這種資料源的使用“池“的思想,避免了建立新的連接配接執行個體時所必需的初始化和認證時間。

JNDI,這個資料源的實作是為了能在如 EJB 或應用伺服器這類容器中使用,容器可以集中或在外部配置資料源,然後放置一個 JNDI 上下文的引用。

接着看XMLConfigBuilder的dataSourceElement()方法。

private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    if (context != null) {
        //讀取配置檔案中dataSource标簽的屬性type
        String type = context.getStringAttribute("type");
        Properties props = context.getChildrenAsProperties();
        //根據type屬性的值,傳回不同的子類,使用接口DataSourceFactory接收,展現了面向接口程式設計的思想
        DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
        //設定屬性值,比如資料庫的url,username,password等等
        factory.setProperties(props);
        //傳回factory
        return factory;
    }
    throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}           

獲得DataSourceFactory之後,就通過getDataSource()方法擷取資料源,完事了。

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            environment = context.getStringAttribute("default");
        }
        //這裡for循環主要是配置檔案可以配置多個資料源
        for (XNode child : context.getChildren()) {
            String id = child.getStringAttribute("id");
            if (isSpecifiedEnvironment(id)) {
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                //dsFactory調動getDataSource()方法,建立dataSource對象
                DataSource dataSource = dsFactory.getDataSource();
                Environment.Builder environmentBuilder = new Environment.Builder(id)
                    .transactionFactory(txFactory)
                    .dataSource(dataSource);
                configuration.setEnvironment(environmentBuilder.build());
            }
        }
    }
}           

Mybatis使用工廠模式來建立DataSourceFactory,可以做到通過配置去使用不同的DataSourceFactory建立DataSource,非常靈活。在Mybatis中使用到工廠模式還有很多地方,比如SqlSessionFactory,這裡就不再展開了,有興趣的可以自己探索一下。

單例模式

單例模式屬于建立型設計模式,這個類提供了一種通路其唯一的對象的方式,可以直接通路,不需要執行個體化該類的對象。保證在應用中隻有單一對象被建立。

Mybatis中用到單例模式的地方有很多,這裡舉個例子是ErrorContext類。這是一個用于記錄該線程的執行環境錯誤資訊,是以是線上程範圍内的單例。

public class ErrorContext {
    //使用ThreadLocal儲存每個線程中的單例對象
    private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
    //私有化構造器
    private ErrorContext() {
    }
    //向外提供唯一的接口擷取單例對象
    public static ErrorContext instance() {
        //從LOCAL中取出context對象
        ErrorContext context = LOCAL.get();
        if (context == null) {
            //如果為null,new一個
            context = new ErrorContext();
            //放入到LOCAL中儲存
            LOCAL.set(context);
        }
        //如果不為null,直接傳回
        return context;
    }
}           

建造者模式

建造者模式也是屬于建立型模式,主要是在建立一個複雜對象時使用,通過一步一步構造最終的對象,将一個複雜對象的建構與它的表示分離。

在Mybatis中,使用到建造者模式的地方展現在ParameterMapping類,這是用于參數映射的一個類。

public class ParameterMapping {
  private Configuration configuration;
  private String property;
  private ParameterMode mode;
  private Class<?> javaType = Object.class;
  private JdbcType jdbcType;
  private Integer numericScale;
  private TypeHandler<?> typeHandler;
  private String resultMapId;
  private String jdbcTypeName;
  private String expression;
  
  //私有化構造器
  private ParameterMapping() {
  }
  
  //通過内部類Builder建立對象
  public static class Builder {
      //初始化ParameterMapping執行個體
      private ParameterMapping parameterMapping = new ParameterMapping();
      //通過構造器初始化ParameterMapping的一些成員變量
      public Builder(Configuration configuration, String property, TypeHandler<?> typeHandler) {
          parameterMapping.configuration = configuration;
          parameterMapping.property = property;
          parameterMapping.typeHandler = typeHandler;
          parameterMapping.mode = ParameterMode.IN;
      }

      public Builder(Configuration configuration, String property, Class<?> javaType) {
          parameterMapping.configuration = configuration;
          parameterMapping.property = property;
          parameterMapping.javaType = javaType;
          parameterMapping.mode = ParameterMode.IN;
      }
      //設定parameterMapping的mode
      public Builder mode(ParameterMode mode) {
          parameterMapping.mode = mode;
          return this;
      }
      //設定parameterMapping的javaType
      public Builder javaType(Class<?> javaType) {
          parameterMapping.javaType = javaType;
          return this;
      }
      //設定parameterMapping的jdbcType
      public Builder jdbcType(JdbcType jdbcType) {
          parameterMapping.jdbcType = jdbcType;
          return this;
      }
      //設定parameterMapping的numericScale
      public Builder numericScale(Integer numericScale) {
          parameterMapping.numericScale = numericScale;
          return this;
      }
      //設定parameterMapping的resultMapId
      public Builder resultMapId(String resultMapId) {
          parameterMapping.resultMapId = resultMapId;
          return this;
      }
      //設定parameterMapping的typeHandler
      public Builder typeHandler(TypeHandler<?> typeHandler) {
          parameterMapping.typeHandler = typeHandler;
          return this;
      }
      //設定parameterMapping的jdbcTypeName
      public Builder jdbcTypeName(String jdbcTypeName) {
          parameterMapping.jdbcTypeName = jdbcTypeName;
          return this;
      }
      //設定parameterMapping的expression
      public Builder expression(String expression) {
          parameterMapping.expression = expression;
          return this;
      }
      //通過build()方法建立對象,傳回
      public ParameterMapping build() {
          resolveTypeHandler();
          validate();
          return parameterMapping;
      }
      }
}           

在SqlSourceBuilder類的buildParameterMapping()方法中可以看到建造者模式的實戰應用:

//根據參數content,建構parameterMapping執行個體
private ParameterMapping buildParameterMapping(String content) {
    //屬性值的Map集合
    Map<String, String> propertiesMap = parseParameterMapping(content);
    String property = propertiesMap.get("property");
    Class<?> propertyType;
    //省略...
    //建立一個ParameterMapping.Builder對象
    ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
    //省略...
    //這裡周遊Map集合,把屬性值設定到ParameterMapping對象中,并建立
    for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
        String name = entry.getKey();
        String value = entry.getValue();
        if ("javaType".equals(name)) {
            javaType = resolveClass(value);
            builder.javaType(javaType);
        } else if ("jdbcType".equals(name)) {
            builder.jdbcType(resolveJdbcType(value));
        } else if ("mode".equals(name)) {
            builder.mode(resolveParameterMode(value));
        } else if ("numericScale".equals(name)) {
            builder.numericScale(Integer.valueOf(value));
        } else if ("resultMap".equals(name)) {
            builder.resultMapId(value);
        } else if ("typeHandler".equals(name)) {
            typeHandlerAlias = value;
        } else if ("jdbcTypeName".equals(name)) {
            builder.jdbcTypeName(value);
        } else if ("property".equals(name)) {
            // Do Nothing
        } else if ("expression".equals(name)) {
            //抛出異常Expression based parameters are not supported yet
        } else {
            //抛出異常
        }
    }
    //省略...
    //建立ParameterMapping對象,并傳回
    return builder.build();
}           

模闆模式

模闆模式是一種行為型模式,一般用在一些比較通用的方法中,定義一個抽象類,編寫一個算法的骨架,将一些步驟延遲到子類。就像是請假條一樣,開頭和結尾都是寫好的模闆,中間的請假的原因(内容)由請假人(子類)去補充完整,這樣可以提高代碼的複用。

在Mybatis中,模闆模式展現在Executor和BaseExecutor這兩個類中。首先看張類圖:

硬核!五千字帶你從設計模式去解讀Mybatis源碼思維導圖概述工廠模式單例模式建造者模式模闆模式代理模式裝飾器模式總結

Executor是一個接口,從命名上可以看出是用來執行SQL語句的對象。下面有一個BaseExecutor的抽象類,這就是用來定義模闆方法的。再下面有三個實作類,SimpleExecutor(簡單執行器),ReuseExecutor(重用執行器),BatchExecutor(批量執行器)。實作類就是用來填充模闆中間的内容的。

執行器在執行JDBC操作的前後往往有很多需要處理的工作都是相同的,比如查詢的時候使用緩存,更新時需要清除緩存等等,是以就很适合使用模闆模式。

接着我們看BaseExecutor抽象類的源碼,一看就明白了,其實就定義了一個骨架。

public abstract class BaseExecutor implements Executor {
    //查詢操作
    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        localCache.putObject(key, EXECUTION_PLACEHOLDER);
        //上面的代碼是固定的
        try {
            //這段代碼不同的子類有不同的實作,是以是調用抽象方法doQuery()
            list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        //下面的代碼也是固定的
        } finally {
            //清除緩存
            localCache.removeObject(key);
        }
        //添加到緩存中
        localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
            localOutputParameterCache.putObject(key, parameter);
        }
        //傳回結果
        return list;
    }
    
    //抽象方法,由子類去實作
    protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
        throws SQLException;
}           

那麼就有疑問了,一開始如果都不設定的話,預設使用哪個子類的實作。很簡單,直接跟着源碼去順藤摸瓜,我們就看到了。

public class Configuration {
    //預設是SIMPLE,也就是SimpleExecutor
    protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
}           

其實模闆模式很簡單就能“認出來”,我的了解就是,抽象類裡定義具(具體的方法),具體方法再調抽(抽象方法)。那十有八九就是模闆模式了。

代理模式

代理模式屬于結構型模式,代理模式的定義說的很抽象,為其他對象提供一種代理以控制對這個對象的通路。在某些情況下,一個對象不适合或者不能直接引用另一個對象,而代理對象可以在用戶端和目标對象之間起到中介的作用。

其實代理模式可以簡單了解為中介的作用,比如一手房東隻關心收租,其他的水電費結算,帶人看房這些雜七雜八的東西他不想關心,就交給中介(二手房東),租客要租房就給錢中介,一手房東收錢就找中介,這個中介就是所謂的代理者。

回到Mybatis架構中,SqlSession類就用到代理模式,SqlSession是操作資料庫一個會話對象,我們使用者一般通過SqlSession做增删改查,但是如果每次做增删改都開啟事務,關閉事務,顯然是很麻煩,是以就可以交給代理類來完成這個工作,如果沒有開啟事務,由代理類自動開啟事務。

Mybatis在這裡是使用JDK動态代理,是以SqlSession是一個接口。

public interface SqlSession extends Closeable {
    
    <T> T selectOne(String statement);

    <E> List<E> selectList(String statement);

    <E> List<E> selectList(String statement, Object parameter);

    <K, V> Map<K, V> selectMap(String statement, String mapKey);

    <T> Cursor<T> selectCursor(String statement);

    void select(String statement, Object parameter, ResultHandler handler);

    int insert(String statement);

    int update(String statement);

    int delete(String statement);

    void commit();

    void rollback();
    //省略...
}           

我們知道動态代理要有一個實作InvocationHandler接口的類,這個類在SqlSessionManager裡,是一個内部類,叫做SqlSessionInterceptor,在這個類裡做相關的處理。

private class SqlSessionInterceptor implements InvocationHandler {
    public SqlSessionInterceptor() {
        // Prevent Synthetic Access
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //擷取SqlSession對象,localSqlSession是一個ThreadLocal,是以是每個線程有自己的sqlSession
        final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();
        //如果不為null
        if (sqlSession != null) {
            try {
                //執行方法,傳回結果  
                return method.invoke(sqlSession, args);
            } catch (Throwable t) {
                throw ExceptionUtil.unwrapThrowable(t);
            }
            //如果sqlsession為null
        } else {
            //打開session  
            final SqlSession autoSqlSession = openSession();
            try {
                //執行Sqlsession的方法,獲得結果  
                final Object result = method.invoke(autoSqlSession, args);
                //commit送出事務
                autoSqlSession.commit();
                //傳回結果
                return result;
            } catch (Throwable t) {
                //rollback復原事務
                autoSqlSession.rollback();
                //抛出異常
                throw ExceptionUtil.unwrapThrowable(t);
            } finally {
                //關閉sqlsession
                autoSqlSession.close();
            }
        }
    }
}           

然後通過構造器去初始化,提供靜态方法傳回這個SqlSessionManager執行個體,請看源碼。

//實作了SqlSessionFactory,SqlSession接口
public class SqlSessionManager implements SqlSessionFactory, SqlSession {

    private final SqlSessionFactory sqlSessionFactory;
    //代理類sqlSessionProxy
    private final SqlSession sqlSessionProxy;

    private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
        //初始化代理類
        this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
            SqlSessionFactory.class.getClassLoader(),
            new Class[]{SqlSession.class},
            new SqlSessionInterceptor());
    }
    //提供一個靜态方法給外部擷取SqlSessionManager類,可以用SqlSession接收
    public static SqlSessionManager newInstance(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionManager(sqlSessionFactory);
    }
    
    //重寫selectOne方法,使用代理類去執行
    @Override
    public <T> T selectOne(String statement) {
        return sqlSessionProxy.<T> selectOne(statement);
    }
    
    //重寫insert方法,使用代理類去執行
    @Override
    public int insert(String statement) {
        return sqlSessionProxy.insert(statement);
    }
    //省略...
}           

這就是Mybatis使用代理模式的一個例子,其實也不是很複雜,還是能看懂的。

但是上面這種方式一般很少用,我們一般都是使用Mapper接口的方式,其實Mapper接口的方式也是使用了代理模式,接下來再繼續看。直接看MapperProxy類。

//實作了InvocationHandler接口
public class MapperProxy<T> implements InvocationHandler, Serializable {

    private static final long serialVersionUID = -6424540398559729838L;
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;
    
    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }
    //代理類做的什麼事情,看這個方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //省略...
        //擷取mapperMethod對象
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        //執行方法。根據Mapper.xml配置檔案的配置進行執行,傳回結果
        return mapperMethod.execute(sqlSession, args);
    }

    private MapperMethod cachedMapperMethod(Method method) {
        //從methodCache取出mapperMethod
        MapperMethod mapperMethod = methodCache.get(method);
        //為null
        if (mapperMethod == null) {
            //new一個。這裡已經把Mapper.xml的一些配置都封裝好了
            mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
            //然後put進methodCache
            methodCache.put(method, mapperMethod);
        }
        //傳回mapperMethod
        return mapperMethod;
    }
}           

mapperMethod的execute()方法的作用就是根據Mapper接口的方法名找到Mapper.xml檔案的sql的Id,然後執行相應的操作,傳回結果。内部的代碼比較長,但是思路就是這樣,這裡就不展開了。

是以我們平時用的時候,TbCommodityInfoMapper接口假設是這樣定義了一個list()方法。

public interface TbCommodityInfoMapper {
    //查詢TbCommodityInfo清單
    List<TbCommodityInfo> list();
}           

那個在TbCommodityInfoMapper.xml就要對應有一個id為list的配置。

<!-- 命名空間也要和接口的全限定名一緻 -->
<mapper namespace="io.github.yehongzhi.commodity.mapper.TbCommodityInfoMapper">
    <!-- 必須要有一個對應的屬性id為list的sql配置 -->
    <select id="list" resultType="tbCommodityInfo">
        select `id`, `commodity_name`, `commodity_price`, `description`, `number` from tb_commodity_info
    </select>
</mapper>           

然後在MapperProxyFactory類,使用工廠模式提供擷取代理類。

public class MapperProxyFactory<T> {
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
    //通過構造器初始化mapperInterface
    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }
    
    //通過這個方法,擷取Mapper接口的代理類
    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }

}           

那個這個getMapper的方法就在SqlSession的子類中被調用。

@Override
public <T> T getMapper(Class<T> type) {
    return getConfiguration().getMapper(type, this);
}           

最終使用者就是這樣使用,這裡是純Mybatis,沒有內建Spring的寫法。

public class Test {
    public static void main(String[] args) {
        //擷取SqlSession對象
        SqlSession sqlSession = SqlSessionFactoryUtils.openSqlSession();
        //再擷取Mapper接口的代理類
        TbCommodityInfoMapper tbCommodityInfoMapper = sqlSession.getMapper(TbCommodityInfoMapper.class);
        //通過代理類去執行相應的方法
        List<TbCommodityInfo> commodityInfoList = tbCommodityInfoMapper.list();
    }
}           

是以代理模式可以說是Mybatis核心的設計模式,用的是非常巧妙。

裝飾器模式

裝飾器模式屬于結構型模式,使用裝飾類包裝對象,動态地給對象添加一些額外的職責。那麼在Mybatis中,哪個地方會使用到裝飾器模式呢?

沒錯了,就是緩存。Mybatis有一級緩存和二級緩存的功能,一級緩存預設是打開的,範圍在SqlSession中生效,二級緩存需要手動配置打開,範圍在全局Configuration,在每個namespace中配置。二級緩存的類型有以下幾種,請看配置。

<mapper namespace="io.github.yehongzhi.commodity.mapper.TbCommodityInfoMapper">
    <!--
        eviction:代表的是緩存回收政策,目前MyBatis提供以下政策。
        (1) LRU,最近最少使用的,一處最長時間不用的對象
        (2) FIFO,先進先出,按對象進入緩存的順序來移除他們
        (3) SOFT,軟引用,移除基于垃圾回收器狀态和軟引用規則的對象
        (4) WEAK,弱引用,更積極的移除基于垃圾收集器狀态和弱引用規則的對象。這裡采用的是LRU,移除最長時間不用的對形象
        flushInterval:重新整理間隔時間,機關為毫秒,這裡配置的是100秒重新整理,如果你不配置它,那麼當SQL被執行的時候才會去重新整理緩存。
        size:引用數目,一個正整數,代表緩存最多可以存儲多少個對象,不宜設定過大。設定過大會導緻記憶體溢出。這裡配置的是1024個對象
        readOnly:隻讀,意味着緩存資料隻能讀取而不能修改,這樣設定的好處是我們可以快速讀取緩存,缺點是我們沒有辦法修改緩存
    -->
    <cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>
    
    <select id="list" resultMap="base_column">
        select `id`, `commodity_name`, `commodity_price`, `description`, `number` from tb_commodity_info
    </select>
</mapper>           

是以這個場景已經很清晰了,也就是在一級緩存的基礎上,再添加二級緩存,也就符合裝飾器模式的那句話,動态給對象添加職責功能。怎麼做呢,我們不妨先看Cache類的類圖。

硬核!五千字帶你從設計模式去解讀Mybatis源碼思維導圖概述工廠模式單例模式建造者模式模闆模式代理模式裝飾器模式總結

先看Cache接口,其實就定義了一些接口方法。

public interface Cache {

    String getId();

    void putObject(Object key, Object value);

    Object getObject(Object key);

    Object removeObject(Object key);

    void clear();

    int getSize();

    ReadWriteLock getReadWriteLock();
}           

然後再看PerpetualCache類,這是Cache接口最基本的實作,二級緩存要擴充就在這個類上面再去包裝來實作擴充。其實就是一個HashMap,再簡單包裝一下。

public class PerpetualCache implements Cache {

    private final String id;
    //成員變量cache,建立一個HashMap
    private Map<Object, Object> cache = new HashMap<Object, Object>();

    public PerpetualCache(String id) {
        this.id = id;
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public int getSize() {
        return cache.size();
    }

    @Override
    public void putObject(Object key, Object value) {
        cache.put(key, value);
    }

    @Override
    public Object getObject(Object key) {
        return cache.get(key);
    }

    @Override
    public Object removeObject(Object key) {
        return cache.remove(key);
    }

    @Override
    public void clear() {
        cache.clear();
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return null;
    }
    //省略...
}           

那麼我們再看二級緩存的一個代表性的類LruCache。

public class LruCache implements Cache {
    //一級緩存,儲存在這個成員變量中
    private final Cache delegate;
    //實際上這是一個LinkedHashMap,利用LinkedHashMap的LRU算法實作緩存的LRU
    private Map<Object, Object> keyMap;
    private Object eldestKey;
    //構造器緩存對象,初始化
    public LruCache(Cache delegate) {
        this.delegate = delegate;
        setSize(1024);
    }

    //初始化keyMap,重寫removeEldestEntry方法,實作LUR算法
    public void setSize(final int size) {
        keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
            private static final long serialVersionUID = 4267176411845948333L;

            @Override
            protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
                boolean tooBig = size() > size;
                //每次put進來時,eldestKey都是最老的key
                if (tooBig) {
                    eldestKey = eldest.getKey();
                }
                return tooBig;
            }
        };
    }

    @Override
    public void putObject(Object key, Object value) {
        //儲存進緩存
        delegate.putObject(key, value);
        //這裡就是删除掉不常用的值
        cycleKeyList(key);
    }

    @Override
    public Object getObject(Object key) {
        keyMap.get(key); //touch
        return delegate.getObject(key);
    }
    
    private void cycleKeyList(Object key) {
        keyMap.put(key, key);
        if (eldestKey != null) {
            //删除掉Map中最老的key
            delegate.removeObject(eldestKey);
            eldestKey = null;
        }
    }

}           

關鍵在于成員變量delegate,這在其他的二級緩存裝飾類中都定義了。這個是為了儲存PerpetualCache這個基礎緩存類的。是以這也就是說二級緩存是在PerpetualCache為基礎擴充的。再繼續看就更加明白了。

直接看建立SqlSessionFactory的builder方法,一直追蹤下去,就可以找到MapperBuilderAssistant類的useNewCache方法。

public class MapperBuilderAssistant extends BaseBuilder {
    //目前Mapper.xml的命名空間
    private String currentNamespace;

    public Cache useNewCache(Class<? extends Cache> typeClass,
                             Class<? extends Cache> evictionClass,
                             Long flushInterval,
                             Integer size,
                             boolean readWrite,
                             boolean blocking,
                             Properties props) {
        Cache cache = new CacheBuilder(currentNamespace)
            //設定基礎緩存
            .implementation(valueOrDefault(typeClass, PerpetualCache.class))
            //添加緩存裝飾類
            .addDecorator(valueOrDefault(evictionClass, LruCache.class))
            .clearInterval(flushInterval)
            .size(size)
            .readWrite(readWrite)
            .blocking(blocking)
            .properties(props)
            //建立緩存
            .build();
        //把緩存添加到configuration,是以二級緩存是configuration範圍的
        configuration.addCache(cache);
        currentCache = cache;
        return cache;
    }
}           

再看CacheBuilder的build方法,更加清晰了。

public class CacheBuilder {
    
    private Class<? extends Cache> implementation;
    private final List<Class<? extends Cache>> decorators;
    
    public Cache build() {
        setDefaultImplementations();
        Cache cache = newBaseCacheInstance(implementation, id);
        setCacheProperties(cache);
        // issue #352, do not apply decorators to custom caches
        if (PerpetualCache.class.equals(cache.getClass())) {
            //周遊裝飾器類
            for (Class<? extends Cache> decorator : decorators) {
                //在cache上添加二級緩存
                cache = newCacheDecoratorInstance(decorator, cache);
                setCacheProperties(cache);
            }
            cache = setStandardDecorators(cache);
        } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
            cache = new LoggingCache(cache);
        }
        //傳回緩存
        return cache;
    }

    private void setDefaultImplementations() {
        if (implementation == null) {
            //如果為空,初始化為PerpetualCache
            implementation = PerpetualCache.class;
            //如果裝飾器類為空,預設用LruCache
            if (decorators.isEmpty()) {
                decorators.add(LruCache.class);
            }
        }
    }
}           

是以我們大概可以想到二級緩存就像千層餅一樣,一層一層地包裝起來。最後debug模式驗證一下。

硬核!五千字帶你從設計模式去解讀Mybatis源碼思維導圖概述工廠模式單例模式建造者模式模闆模式代理模式裝飾器模式總結

在執行查詢的時候你就會看到真的是千層餅一樣,一層一層的。看CachingExecutor類的query方法。

硬核!五千字帶你從設計模式去解讀Mybatis源碼思維導圖概述工廠模式單例模式建造者模式模闆模式代理模式裝飾器模式總結

最後在調用Cache的putObject方法時就會一層一層從外到内地調用,實作為對象動态擴充功能的裝飾器模式。

硬核!五千字帶你從設計模式去解讀Mybatis源碼思維導圖概述工廠模式單例模式建造者模式模闆模式代理模式裝飾器模式總結

裝飾器模式在Mybatis中的應用就講到這裡了,有什麼不懂的,可以關注公衆号

java技術愛好者

,加我微信提問。

總結

這篇文章就介紹了Mybatis中用到的6種設計模式,分别是工廠模式,單例模式,模闆模式,建造者模式,代理模式,還有裝飾器模式。實際上Mybatis除了我講的這些之外,還有很多我沒有提到的,比如組合模式,擴充卡模式等等,有興趣自己去研究一下吧。

因為現在很多面試動不動就問有看過什麼架構的源碼,實際上看源碼是好的,但是不能盲目入手,因為很多架構是運用了大量的設計模式,如果對設計模式沒有一定的認識,很容易看不懂,看懵。是以對于還沒有設計模式基礎的同學,建議先看設計模式,然後再去學習源碼,這樣才能循序漸進地提升自身實力。

這篇文章就講到這裡了,感謝大家的閱讀。

覺得有用就點個贊吧,你的點贊是我創作的最大動力~

我是一個努力讓大家記住的程式員。我們下期再見!!!

硬核!五千字帶你從設計模式去解讀Mybatis源碼思維導圖概述工廠模式單例模式建造者模式模闆模式代理模式裝飾器模式總結
能力有限,如果有什麼錯誤或者不當之處,請大家批評指正,一起學習交流!