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

小傅哥發表於2022-07-18

作者:小傅哥
<br/>部落格: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章

相關文章