最近在思考一個問題:如何能夠更好的分享主流框架源碼學習筆記(主要是源碼部分)?讓有緣刷到的同學既可以有所收獲,還能保持對相關技術架構探討學習熱情和興趣。以及自己也保持較高的分享熱情和動力。
今天嘗試用一個SQL查詢作為引子,去解構Mybatis的核心原理和關鍵源碼處理流程。這種更加貼合工作實踐方式,相信可以降低探索核心源碼門檻。
一、前言背景
二、Mybatis概述
三、Mybatis的核心原理
3.1 Mybatis核心功能處理流程
3.1.1 解析配置-加載并解析Mapper配置文件
3.1.2 創建連接-創建SqlSessionFactory并獲取sqlSession連接
3.1.3 執行SQL語句-Executor
3.1.4 結果數據封裝 MapperStatement & ResultSetHandler
3.1.5 關閉連接
四、核心功能特性
4.1 支持動態靈活的SQL
4.2 詳解一級、二級緩存機制
4.2.1 二級緩存為什么默認不開啟?
4.3 支持插件擴展
4.4 延遲加載
4.5 SQL注解
【公眾號搜索:拉丁解牛說技術】歡迎一起交流討論。
一、前言背景
在10多年前,那時候剛開始工作,移動互聯網還沒發展起來,Mybatis還沒流行,后端應用開發,主流用的是SSH框架。回想那時候的Hibernate、Spring,配置多又雜,其實對新手并不友好。然而相比手寫JDBC連接管理、繁瑣的結果數據轉換,讓SSH當年也是火了好幾年。
隨著Hibernate和Mybatis的不斷發展,研發人員成功解放手寫JDBC數據庫連接查詢相關研發工作。他們都是優秀的ORM對象關系映射管理框架,也就是持久層框架。但是Hibernate存在對復雜sql關系支持弱、不支持存儲過程、性能差、調優難、全表映射復雜等問題,用的人越來越少。
而后起之秀Mybatis,當今最經典ORM框架,由于其靈活易用、好擴展、支持復雜SQL、出色的性能,較好的平衡對象關系映射管理和SQL編寫支持,稱為半ORM框架,已經成功替代Hibernate。
今天我們梳理Mybatis的核心原理和工作流程,以及重點分析它的一些核心功能特性。
二、Mybatis概述
Mybatis是一個持久層框架,具體就是用來操作數據庫數據,并轉換成目標對象的技術框架。它的核心在于將表數據和對象實例進行關聯映射,也就是ORM(object relation Mapping)。
Mybatis之所以可以替換Hibernate,主要是支持SQL定制、高級的映射管理功能、緩存機制、還有存儲過程(由于大數據技術發展,目前存儲過程用的人也越來越少,但是在那個年代支持存儲過程非常實用)。Mybatis靈活可擴展高性能的特性,讓我們開發讀寫數據,幾乎不需要編程,主要做的工作就是編寫Mapper配置文件,把SQL和對象關系映射管理好,就可以實現CRUD。而多類型數據庫的切換遷移,對系統應用來說,簡單到只需要替換JDBC的驅動。
三、Mybatis的核心原理
如上所述,Mybatis核心工作就是幫助研發人員對數據庫的讀寫操作,簡化成面向對象操作。
對于一個完全不懂Mybatis或者ORM的人來說,如果要實現讀寫數據庫,該怎么實現?這個相信很多人都能回答:通過jdbc連接數據庫、執行SQL、解析sql數據結果,就三個步驟完成。
而Mybatis的核心原理邏輯更加細化,但整體也是類似以上三個步驟。畢竟大家目標一致,只是實現過程不同而已。
接下來我們用一個非常簡單的demo,就是通過Mybatis去讀數據庫數據,demo就只有幾行代碼,然后循序漸進了解Mybatis的核心工作原理,以及核心源碼組件功能。
package com.lading.mybaties;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;import java.io.IOException;
import java.io.InputStream;public class MyBatisDemo {public static void main(String[] args) throws IOException {InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);try (SqlSession sqlSession = sqlSessionFactory.openSession()) {sqlSession.selectOne("com.lading.mapper.UserMapper.getUserById", 1);}}
}
3.1 Mybatis核心功能處理流程
Mybatis整體框架,基于面向對象思想去實現與數據庫表數據交互,它的核心步驟按順序處理有以下五個。
3.1.1 解析配置-加載并解析Mapper配置文件
Mybatis通過SqlSessionFactoryBuilder來加載解析配置文件,并生SqlSessionFactory。
SqlSessionFactoryBuilder是Mybatis的入口類,類似tomcat的org.apache.catalina.startup.Bootstrap 啟動類。
比如我們項目只有Mybatis包、jdbc驅動,想要基于Mybatis去讀寫數據,首先需要通過以下三行代碼去解析你的Mybatis相關配置文件,以及構建一個SqlSessionFactory,為后續創建數據庫連接和讀寫做準備。
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
這三行代碼里最后一行,看源碼會發現,里面做了非常多的工作,細無巨細的把Mybatis管理配置文件、還有對象sql映射Mapper文件一一解析,包括properties、settings、environment、mapper等標簽數據,以及業務相關的mapper文件里的resultMap、CURD 標簽都會轉成configration對象屬性。
最后利用解析結果數據存放的對象configration去實例化構建sql 會話工廠:
new DefaultSqlSessionFactory(config)。
3.1.2 創建連接-創建SqlSessionFactory并獲取sqlSession連接
解析完成配置文件后,Mybatis框架已經清晰知道Mybatis的基礎配置信息、連接數據庫的類型、用戶密碼信息,還有相關表映射關系。
通過SqlSessionFactoryBuilder構建了session工廠實例SqlSessionFactory,看名字可以知道是通過建造者模式去實例化該對象。
SqlSessionFactory是Mybatis的核心接口,它就是用來負責實現管理會話連接。
在應用啟動或者需要用到Mybatis讀寫數據時候,就生成一個實例DefaultSqlSessionFactory(它是session工廠接口唯一實現類)。
SqlSessionFactory,通常在應用里是全局唯一并共享。
然后通過會話工廠實例sqlSessionFactory去開啟一個session連接:
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession就是真正負責執行sql、并且管理事務的核心功能類,它底層是jdbc的連接。
session每次用完就關閉,需要用的時候再次新建。但是Mybatis也有實現對應的連接池,如果配置了連接池就不會關閉。 Mybatis的連接池,后面出一篇文章專門分享Mybatis如何管理連接池。
比如,通過session去查詢用戶ID=1的用戶數據:
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User ladingUser = userMapper.selectUserById(1);
System.out.println(ladingUser);
3.1.3 執行SQL語句-Executor
在3.1.2里,看起來sql session執行了sql查詢,但是DefaultSqlSession里面封裝了一個Executor執行器。
Executor執行器,它是真正負責執行sql的打工人,里面有一個抽象類BaseExecutor,通過模板方法模式去共享自己的模板方法能力。另外三個子類去繼承實現不同的db操作。
這三個執行器的主要區別在于:
SimpleExecutor,是一個最簡單的執行器,每次執行sql都新建一個Statement對象。
比如它里面的query方法源碼:
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {// JDBC中 Statement接口Statement stmt = null;try {// 獲取到Configuration對象Configuration configuration = ms.getConfiguration();StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);// 里面創建Connection代理對象,新增這個Statementstmt = prepareStatement(handler, ms.getStatementLog());// 執行查詢,封裝結果集return handler.query(stmt, resultHandler);} finally {closeStatement(stmt);}
}
ReuseExecutor,顧名思義是可復用執行器。特點是,復用預處器PreparedStatement。
比如獲取預處理Statement的方法源碼:
而BatchExcutor,叫做批量執行器,支持批量處理SQL語句。
3.1.4 結果數據封裝 MapperStatement & ResultSetHandler
結果數據封裝邏輯里面相對繁瑣。實際上在Excutor執行sql之前,也依賴mapperStatement去封裝sql語句、sql的入參。底層通過jdbc connection執行sql后,通過ResultSetHandler和TypeHandler去解析封裝結果數據。
3.1.5 關閉連接
最后是關閉sqlsession,釋放資源。如果是配置應用連接池,就是歸還連接操作。
四、核心功能特性
4.1 支持動態靈活的SQL
直接允許在mapper xml文件配置動態的sql。包括if、where、choose、when、foreach等多種動態條件。這個能力讓Mybatis成功支持復雜關聯sql處理。
4.2 詳解一級、二級緩存機制
Mybatis支持緩存,大幅提升數據庫查詢性能。默認開啟一級緩存,關閉二級緩存。
一級緩存:是基于sqlsession去實現,同一個sql session多次查詢,會復用相同sql 的緩存結果。底層是通過把一個SQL的id、名稱、入參計算得到一個唯一key,并和結果數據存入一個map里。
當同一個sql session后面重復的查詢,就會判斷緩存是否有數據,如果有就直接返回,不再繼續從數據庫里查詢數據,大幅提升單個sql session里的重復查詢效率。
如果在一個sql session里出現了update、delete、insert、或者commit、close session操作,就會自動去清空緩存,確保沒有臟數據在Mybatis一級緩存里。
該緩存默認開啟。如果要關閉,可以通過flushCache=true去關閉。
二級緩存:是基于mapper,也就是命名空間級別的緩存。相當于sqlSessionFactory級別,比一級緩存sqlSession更高一個層級。在同一個mapper下,所有session會話產生的緩存數據統一在mapper里命名空間管理。多個mapper的二級緩存互不干擾。
二級緩存,默認是關閉的。可以在mapper里,新增cache標簽就可以開啟。
<mapper namespace="com.lading.mapper.UserMapper">
<!--啟用二級緩存-->
<cache eviction="FIFO" flushInterval="30000" readOnly="true"/>
</mapper>
4.2.1 二級緩存為什么默認不開啟?
之所以默認關閉,主要因為二級緩存可能有臟讀。
正因為所有session會話產生的緩存數據,統一在mapper里命名空間管理,多個mapper的二級緩存互不干擾。這里就可能導致研發人員如果在另一個mapper新增或者修改了數據,其他地方mapper緩存的數據就沒有被自動更新,就會造成生產故障。
當然這個Mybatis有提供相關配置,支持關聯mapper同步被動去清空二級緩存,避免干擾。
4.3 支持插件擴展
Mybatis允許研發人員在核心組件中插入自定義的邏輯,比如分頁、攔截器、性能監控插件功能 。
插件的開發,可以通過實現 org.apache.ibatis.pluginInterceptor接口,然后實現里面 intercept、plugin 和 setProperties方法來新增插件功能。
比如增加一個執行sql監控功能。
package com.lading.mybaties;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;import java.util.Properties;public class TimeMonitorPlugin implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {long startTime = System.currentTimeMillis();Object result = invocation.proceed(); // 執行目標方法long endTime = System.currentTimeMillis();System.out.println("SQL 執行耗時: " + (endTime - startTime) + "ms");return result;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {// 可以設置攔截器的屬性}
}
在 mybatis-config.xml 文件中注冊自定義攔截器:
<plugins>
<plugin interceptor="lading.mybaties.TimeMonitorPlugin"/>
</plugins>
sql查詢的時候就會打印sql執行時間。
4.4 延遲加載
可以控制關聯對象,在需要用到的時候才去加載。比如1-n的場景,一個學生有多門課程信息。查詢一個學生基本信息的時候,如果沒有用到課程列表,就不需要在關聯查詢里把課程列表頁拉出去,提升了查詢效率。
//fetchType=lazy開啟延遲加載
<association property="xxx" fetchType="lazy">
這個延遲加載功能,和Springboot通過@lazy注解去解決循環依賴問題,有類似異曲同工的作用。
4.5 SQL注解
MyBatis 也支持使用注解來配置 SQL 映射,從而簡化 XML 配置,可以實現零xml配置文件去操作數據庫數據。常用的注解包括常規的CURD:
@Select
@Insert
@Update
@Delete
還有,@Results結果映射注解。
比如通過名稱去查詢用戶:
@Select("SELECT * FROM user WHERE name = #{name}")
@Results({@Result(property = "id", column = "id"),@Result(property = "name", column = "name")
})
User selectUserByName(String name);