【分享】從Mybatis源碼中,學習到的10種設計模式

作者:小傅哥
博客:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收穫!😄

一、前言:小鎮卷碼家

總有不少研發夥伴問小傅哥:“爲什麼學設計模式、看框架源碼、補技術知識,就一個普通的業務項目,會造飛機不也是天天寫CRUD嗎?”

你說的沒錯,但你天天寫CRUD,你覺得 煩不? 慌不? 是不是既擔心自己沒有得到技術成長,也害怕將來沒法用這些都是CRUD的項目去參加;述職、晉升、答辯,甚至可能要被迫面試時,自己手裏一點乾貨也沒有的情況。

所以你/我作爲一個小鎮卷碼家,當然要擴充自己的知識儲備,否則架構,架構思維不懂設計,設計模式不會源碼、源碼學習不深,最後就用一堆CRUD寫簡歷嗎?

二、源碼:學設計模式

在 Mybatis 兩萬多行的框架源碼實現中,使用了大量的設計模式來解耦工程架構中面對複雜場景的設計,這些是設計模式的巧妙使用纔是整個框架的精華,這也是小傅哥喜歡卷源碼的重要原因。經過小傅哥的整理有如下10種設計模式的使用,如圖所示

Mybatis 框架源碼10種設計模式

講道理,如果只是把這10種設計模式背下來,等着下次面試的時候拿出來說一說,雖然能有點幫助,不過這種學習方式就真的算是把路走窄了。就像你每說一個設計模式,能聯想到這個設計模式在Mybatis的框架中,體現到哪個流程中的源碼實現上了嗎?這個源碼實現的思路能不能用到你的業務流程開發裏?別總說你的流程簡單,用不上設計模式!難到因爲有錢、富二代,就不考試嗎?🤔

好啦,不扯淡了,接下來小傅哥就以《手寫Mybatis:漸進式源碼實踐》的學習,給大家列舉出這10種設計模式,在Mybatis框架中都體現在哪裏了!

三、類型:創建型模式

1. 工廠模式

源碼詳見cn.bugstack.mybatis.session.SqlSessionFactory

public interface SqlSessionFactory {

   SqlSession openSession();

}

源碼詳見cn.bugstack.mybatis.session.defaults.DefaultSqlSessionFactory

public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private final Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public SqlSession openSession() {
        Transaction tx = null;
        try {
            final Environment environment = configuration.getEnvironment();
            TransactionFactory transactionFactory = environment.getTransactionFactory();
            tx = transactionFactory.newTransaction(configuration.getEnvironment().getDataSource(), TransactionIsolationLevel.READ_COMMITTED, false);
            // 創建執行器
            final Executor executor = configuration.newExecutor(tx);
            // 創建DefaultSqlSession
            return new DefaultSqlSession(configuration, executor);
        } catch (Exception e) {
            try {
                assert tx != null;
                tx.close();
            } catch (SQLException ignore) {
            }
            throw new RuntimeException("Error opening session.  Cause: " + e);
        }
    }

}

Mybatis 工廠模式

  • 工廠模式:簡單工廠,是一種創建型設計模式,其在父類中提供一個創建對象的方法,允許子類決定實例對象的類型。
  • 場景介紹SqlSessionFactory 是獲取會話的工廠,每次我們使用 Mybatis 操作數據庫的時候,都會開啓一個新的會話。在會話工廠的實現中負責獲取數據源環境配置信息、構建事務工廠、創建操作SQL的執行器,並最終返回會話實現類。
  • 同類設計SqlSessionFactoryObjectFactoryMapperProxyFactoryDataSourceFactory

2. 單例模式

源碼詳見cn.bugstack.mybatis.session.Configuration

public class Configuration {

    // 緩存機制,默認不配置的情況是 SESSION
    protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;

    // 映射註冊機
    protected MapperRegistry mapperRegistry = new MapperRegistry(this);

    // 映射的語句,存在Map裏
    protected final Map<String, MappedStatement> mappedStatements = new HashMap<>();
    // 緩存,存在Map裏
    protected final Map<String, Cache> caches = new HashMap<>();
    // 結果映射,存在Map裏
    protected final Map<String, ResultMap> resultMaps = new HashMap<>();
    protected final Map<String, KeyGenerator> keyGenerators = new HashMap<>();

    // 插件攔截器鏈
    protected final InterceptorChain interceptorChain = new InterceptorChain();

    // 類型別名註冊機
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
    protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();

    // 類型處理器註冊機
    protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();

    // 對象工廠和對象包裝器工廠
    protected ObjectFactory objectFactory = new DefaultObjectFactory();
    protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();

    protected final Set<String> loadedResources = new HashSet<>();
 
    //...
}

Mybatis 單例模式

  • 單例模式:是一種創建型模式,讓你能夠保證一個類只有一個實例,並提供一個訪問該實例的全局節點。
  • 場景介紹:Configuration 就像狗皮膏藥一樣大單例,貫穿整個會話的生命週期,所以的配置對象;映射、緩存、入參、出參、攔截器、註冊機、對象工廠等,都在 Configuration 配置項中初始化。並隨着 SqlSessionFactoryBuilder 構建階段完成實例化操作。
  • 同類場景ErrorContextLogFactoryConfiguration

3. 建造者模式

源碼詳見cn.bugstack.mybatis.mapping.ResultMap#Builder

public class ResultMap {

    private String id;
    private Class<?> type;
    private List<ResultMapping> resultMappings;
    private Set<String> mappedColumns;

    private ResultMap() {
    }

    public static class Builder {
        private ResultMap resultMap = new ResultMap();

        public Builder(Configuration configuration, String id, Class<?> type, List<ResultMapping> resultMappings) {
            resultMap.id = id;
            resultMap.type = type;
            resultMap.resultMappings = resultMappings;
        }

        public ResultMap build() {
            resultMap.mappedColumns = new HashSet<>();
            // step-13 新增加,添加 mappedColumns 字段
            for (ResultMapping resultMapping : resultMap.resultMappings) {
                final String column = resultMapping.getColumn();
                if (column != null) {
                    resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));
                }
            }
            return resultMap;
        }

    }
    
    // ... get
}

Mybatis 建造者模式

  • 建造者模式:使用多個簡單的對象一步一步構建成一個複雜的對象,這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。
  • 場景介紹:關於建造者模式在 Mybatis 框架裏的使用,那真是紗窗擦屁股,給你漏了一手。到處都是 XxxxBuilder,所有關於 XML 文件的解析到各類對象的封裝,都使用建造者以及建造者助手來完成對象的封裝。它的核心目的就是不希望把過多的關於對象的屬性設置,寫到其他業務流程中,而是用建造者的方式提供最佳的邊界隔離。
  • 同類場景SqlSessionFactoryBuilderXMLConfigBuilderXMLMapperBuilderXMLStatementBuilderCacheBuilder

四、類型:結構型模式

1. 適配器模式

源碼詳見cn.bugstack.mybatis.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);

}

源碼詳見cn.bugstack.mybatis.logging.slf4j.Slf4jImpl

public class Slf4jImpl implements Log {

  private Log log;

  public Slf4jImpl(String clazz) {
    Logger logger = LoggerFactory.getLogger(clazz);

    if (logger instanceof LocationAwareLogger) {
      try {
        // check for slf4j >= 1.6 method signature
        logger.getClass().getMethod("log", Marker.class, String.class, int.class, String.class, Object[].class, Throwable.class);
        log = new Slf4jLocationAwareLoggerImpl((LocationAwareLogger) logger);
        return;
      } catch (SecurityException e) {
        // fail-back to Slf4jLoggerImpl
      } catch (NoSuchMethodException e) {
        // fail-back to Slf4jLoggerImpl
      }
    }

    // Logger is not LocationAwareLogger or slf4j version < 1.6
    log = new Slf4jLoggerImpl(logger);
  }

  @Override
  public boolean isDebugEnabled() {
    return log.isDebugEnabled();
  }

  @Override
  public boolean isTraceEnabled() {
    return log.isTraceEnabled();
  }

  @Override
  public void error(String s, Throwable e) {
    log.error(s, e);
  }

  @Override
  public void error(String s) {
    log.error(s);
  }

  @Override
  public void debug(String s) {
    log.debug(s);
  }

  @Override
  public void trace(String s) {
    log.trace(s);
  }

  @Override
  public void warn(String s) {
    log.warn(s);
  }

}

Mybatis 適配器模式

  • 適配器模式:是一種結構型設計模式,它能使接口不兼容的對象能夠相互合作。
  • 場景介紹:正是因爲有太多的日誌框架,包括:Log4j、Log4j2、Slf4J等等,而這些日誌框架的使用接口又都各有差異,爲了統一這些日誌工具的接口,Mybatis 定義了一套統一的日誌接口,爲所有的其他日誌工具接口做相應的適配操作。
  • 同類場景:主要集中在對日誌的適配上,Log 和 對應的實現類,以及在 LogFactory 工廠方法中進行使用。

2. 代理模式

源碼詳見cn.bugstack.mybatis.binding.MapperProxy

public class MapperProxy<T> implements InvocationHandler, Serializable {

    private static final long serialVersionUID = -6424540398559729838L;

    private 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 {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else {
            final MapperMethod mapperMethod = cachedMapperMethod(method);
            return mapperMethod.execute(sqlSession, args);
        }
    }
    
    // ...

}

Mybatis 代理模式

  • 代理模式:是一種結構型模式,讓你能夠提供對象的替代品或其佔位符。代理控制着對原對象的訪問,並允許在將請求提交給對象前進行一些處理。
  • 場景介紹:不吹牛的講,沒有代理模式,就不會有各類的框架存在。就像 Mybatis 中的 MapperProxy 映射器代理實現類,它所實現的功能就是幫助我們完成 DAO 接口的具體實現類的方法操作,你的任何一個配置的 DAO 接口所調用的 CRUD 方法,都會被 MapperProxy 接管,調用到方法執行器等一系列操作,並返回最終的數據庫執行結果。
  • 同類場景DriverProxyPluginInvokerMapperProxy

3. 組合模式

源碼詳見cn.bugstack.mybatis.scripting.xmltags.SqlNode

public interface SqlNode {

    boolean apply(DynamicContext context);

}

源碼詳見cn.bugstack.mybatis.scripting.xmltags.IfSqlNode

public class IfSqlNode implements SqlNode{

    private ExpressionEvaluator evaluator;
    private String test;
    private SqlNode contents;

    public IfSqlNode(SqlNode contents, String test) {
        this.test = test;
        this.contents = contents;
        this.evaluator = new ExpressionEvaluator();
    }

    @Override
    public boolean apply(DynamicContext context) {
        // 如果滿足條件,則apply,並返回true
        if (evaluator.evaluateBoolean(test, context.getBindings())) {
            contents.apply(context);
            return true;
        }
        return false;
    }

}

源碼詳見cn.bugstack.mybatis.scripting.xmltags.XMLScriptBuilder

public class XMLScriptBuilder extends BaseBuilder {

    private void initNodeHandlerMap() {
        // 9種,實現其中2種 trim/where/set/foreach/if/choose/when/otherwise/bind
        nodeHandlerMap.put("trim", new TrimHandler());
        nodeHandlerMap.put("if", new IfHandler());
    }
 
    List<SqlNode> parseDynamicTags(Element element) {
        List<SqlNode> contents = new ArrayList<>();
        List<Node> children = element.content();
        for (Node child : children) {
            if (child.getNodeType() == Node.TEXT_NODE || child.getNodeType() == Node.CDATA_SECTION_NODE) {

            } else if (child.getNodeType() == Node.ELEMENT_NODE) {
                String nodeName = child.getName();
                NodeHandler handler = nodeHandlerMap.get(nodeName);
                if (handler == null) {
                    throw new RuntimeException("Unknown element " + nodeName + " in SQL statement.");
                }
                handler.handleNode(element.element(child.getName()), contents);
                isDynamic = true;
            }
        }
        return contents;
    }
    
    // ...
}

配置詳見resources/mapper/Activity_Mapper.xml

<select id="queryActivityById" parameterType="cn.bugstack.mybatis.test.po.Activity" resultMap="activityMap" flushCache="false" useCache="true">
    SELECT activity_id, activity_name, activity_desc, create_time, update_time
    FROM activity
    <trim prefix="where" prefixOverrides="AND | OR" suffixOverrides="and">
        <if test="null != activityId">
            activity_id = #{activityId}
        </if>
    </trim>
</select>

Mybatis 組合模式

  • 組合模式:是一種結構型設計模式,你可以使用它將對象組合成樹狀結構,並且能獨立使用對象一樣使用它們。
  • 場景介紹:在 Mybatis XML 動態的 SQL 配置中,共提供了9種(trim/where/set/foreach/if/choose/when/otherwise/bind)標籤的使用,讓使用者可以組合出各類場景的 SQL 語句。而 SqlNode 接口的實現就是每一個組合結構中的規則節點,通過規則節點的組裝完成一顆規則樹組合模式的使用。具體使用源碼可以閱讀《手寫Mybatis:漸進式源碼實踐》
  • 同類場景:主要體現在對各類SQL標籤的解析上,以實現 SqlNode 接口的各個子類爲主。

4. 裝飾器模式

源碼詳見cn.bugstack.mybatis.session.Configuration

public Executor newExecutor(Transaction transaction) {
    Executor executor = new SimpleExecutor(this, transaction);
    // 配置開啓緩存,創建 CachingExecutor(默認就是有緩存)裝飾者模式
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    return executor;
}

Mybatis 裝飾器模式

  • 裝飾器模式:是一種結構型設計模式,允許你通過將對象放入包含行爲的特殊封裝對象中來爲原對象綁定新的行爲。
  • 場景介紹:Mybatis 的所有 SQL 操作,都是經過 SqlSession 會話調用 SimpleExecutor 簡單實現的執行器完成的,而一級緩存的操作也是在簡單執行器中處理。那麼這裏二級緩存因爲是基於一級緩存刷新操作的,所以在實現上,通過創建一個緩存執行器,包裝簡單執行器的處理邏輯,實現二級緩存操作。那麼這裏用到的就是裝飾器模式,也叫俄羅斯套娃模式。
  • 同類場景:主要提前在 Cache 緩存接口的實現和 CachingExecutor 執行器中。

五、類型:行爲型模式

1. 模板模式

源碼詳見cn.bugstack.mybatis.executor.BaseExecutor

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    if (closed) {
        throw new RuntimeException("Executor was closed.");
    }
    // 清理局部緩存,查詢堆棧爲0則清理。queryStack 避免遞歸調用清理
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    try {
        queryStack++;
        // 根據cacheKey從localCache中查詢數據
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list == null) {
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {
        queryStack--;
    }
    if (queryStack == 0) {
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            clearLocalCache();
        }
    }
    return list;
}

源碼詳見cn.bugstack.mybatis.executor.SimpleExecutor

protected int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
        Configuration configuration = ms.getConfiguration();
        // 新建一個 StatementHandler
        StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
        // 準備語句
        stmt = prepareStatement(handler);
        // StatementHandler.update
        return handler.update(stmt);
    } finally {
        closeStatement(stmt);
    }
}

Mybatis 模板模式

  • 模板模式:是一種行爲設計模式,它在超類中定義了一個算法的框架,允許子類在不修改結構的情況下重寫算法的特定步驟。
  • 場景介紹:只要存在一系列可被標準定義的流程,在流程的步驟大部分是通用邏輯,只有一少部分是需要子類實現的,那麼通常會採用模板模式來定義出這個標準的流程。就像 Mybatis 的 BaseExecutor 就是一個用於定義模板模式的抽象類,在這個類中把查詢、修改的操作都定義出了一套標準的流程。
  • 同類場景BaseExecutorSimpleExecutorBaseTypeHandler

2. 策略模式

源碼詳見cn.bugstack.mybatis.type.TypeHandler

public interface TypeHandler<T> {

    /**
     * 設置參數
     */
    void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

    /**
     * 獲取結果
     */
    T getResult(ResultSet rs, String columnName) throws SQLException;

    /**
     * 取得結果
     */
    T getResult(ResultSet rs, int columnIndex) throws SQLException;

}

源碼詳見cn.bugstack.mybatis.type.LongTypeHandler

public class LongTypeHandler extends BaseTypeHandler<Long> {

    @Override
    protected void setNonNullParameter(PreparedStatement ps, int i, Long parameter, JdbcType jdbcType) throws SQLException {
        ps.setLong(i, parameter);
    }

    @Override
    protected Long getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return rs.getLong(columnName);
    }

    @Override
    public Long getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return rs.getLong(columnIndex);
    }

}

Mybatis 策略模式

  • 策略模式:是一種行爲設計模式,它能定義一系列算法,並將每種算法分別放入獨立的類中,以使算法的對象能夠互相替換。
  • 場景介紹:在 Mybatis 處理 JDBC 執行後返回的結果時,需要按照不同的類型獲取對應的值,這樣就可以避免大量的 if 判斷。所以這裏基於 TypeHandler 接口對每個參數類型分別做了自己的策略實現。
  • 同類場景PooledDataSource\UnpooledDataSourceBatchExecutor\ResuseExecutor\SimpleExector\CachingExecutorLongTypeHandler\StringTypeHandler\DateTypeHandler

3. 迭代器模式

源碼詳見cn.bugstack.mybatis.reflection.property.PropertyTokenizer

public class PropertyTokenizer implements Iterable<PropertyTokenizer>, Iterator<PropertyTokenizer> {

    public PropertyTokenizer(String fullname) {
        // 班級[0].學生.成績
        // 找這個點 .
        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);
        }
    }

		// ...

}

Mybatis 迭代器模式

  • 迭代器模式:是一種行爲設計模式,讓你能在不暴露集合底層表現形式的情況下遍歷集合中所有的元素。
  • 場景介紹:PropertyTokenizer 是用於 Mybatis 框架 MetaObject 反射工具包下,用於解析對象關係的迭代操作。這個類在 Mybatis 框架中使用的非常頻繁,包括解析數據源配置信息並填充到數據源類上,以及參數的解析、對象的設置都會使用到這個類。
  • 同類場景PropertyTokenizer

六、總結:“卷王”的心得

一份源碼的成體系拆解漸進式學習,可能需要1~2個月的時間,相比於爽文和疲於應試要花費更多的經歷。但你總會在一個大塊時間學習完後,會在自己的頭腦中構建出一套完整體系關於此類知識的技術架構,無論從哪裏入口你都能清楚各個分支流程的走向,這也是你成爲技術專家路上的深度學習。

如果你也想有這樣酣暢淋漓的學習,千萬別錯過傅哥爲你編寫的資料《手寫Mybatis:漸進式源碼實踐》目錄如圖所示,共計20章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章