首先推薦一個簡單的Mybatis原理視頻教程,可以作為入門教程進行學習:點我 (該教程講解的是如何手寫簡易版Mybatis)
執(zhí)行流程的理解
理解Mybatis的簡單流程后自己手寫一個,可以解決百分之70的面試問題和開發(fā)中遇到的困惑严卖,此乃重中之重
假如我們要自己設計一個半自動的仿Mybatis框架楣黍,有哪些環(huán)節(jié)是必不可少的呢胖秒?思考再三馆匿,必然有以下環(huán)節(jié):
- 相關配置文件加載(XML類型滞欠,接口類型則可以省略)
- 接口代理(JDK 動態(tài)代理)
- 針對XML或者接口進行解析 ==》即把
不可直接執(zhí)行的SQL
處理為攜帶參數(shù),返回值明確的數(shù)據(jù)結(jié)構(gòu) - JDBC模塊執(zhí)行何鸡,并返回對應的返回值類型
如果僅考慮這三點的話纺弊,其實實現(xiàn)一個簡單的ORM框架就很容易了,再附加一些反射和正則表達式等等就可以搞定了.
那如果去參考Mybatis骡男,我們來看看它的幾個環(huán)節(jié)是如何設計的:
其實大致思路一樣淆游,需要一個數(shù)據(jù)結(jié)構(gòu)去存儲全部的變量,通過接口代理的方式調(diào)用Sqlsession里面內(nèi)置的方法隔盛,不同的是真正的執(zhí)行者又加了一層犹菱,是 Executor
,再通過原始JDBC返回數(shù)據(jù)給調(diào)用者吮炕,當然腊脱,真正的Mybatis包含了眾多的設計模式以及數(shù)據(jù)源,緩存龙亲,動態(tài)SQL陕凹,數(shù)據(jù)庫事務,延遲加載處理等等
為了驗證mybatis的執(zhí)行流程鳄炉,采用了兩種方式去調(diào)用接口杜耙,如下所示:
public static void main(String[] args) throws IOException {
// 指定全局配置文件
String resource = "mybatis-config.xml";
// 讀取配置文件
InputStream inputStream = Resources.getResourceAsStream(resource);
// 構(gòu)建sqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
// Mapper 編程方式
ScriptDirDao mapper = sqlSession.getMapper(ScriptDirDao.class);
System.out.println(mapper.selectOne(1));
// ibatis 編程方式 ---> 注意由于沒有顯式設置提交, 因此兩個sql執(zhí)行使用的是同一次sqlsession, 即默認觸發(fā)了一級緩存
Object object = sqlSession.selectOne("com.mycode.mybatis.ScriptDirDao.selectOne", 1);
System.out.println(object);
}
ibatis編程方式實際就是通過 namespace+方法名定位具體的接口方法,然后傳遞參數(shù)并執(zhí)行
正常使用方式就是基于上述的基本流程做了一層自動的返回值映射拂盯,接口方法的匹配
這里有個小點需要強調(diào)下佑女,真正的執(zhí)行者是Executor
,我們每次在使用以下代碼:
// 構(gòu)建sqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
// Mapper 編程方式
ScriptDirDao mapper = sqlSession.getMapper(ScriptDirDao.class);
System.out.println(mapper.selectOne(1));
通過查看源碼也可以看到磕仅,SqlSession接口的默認實現(xiàn)類是DefaultSqlSession
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor; // 執(zhí)行者
private final boolean autoCommit;
private boolean dirty;
private List<Cursor<?>> cursorList;
.......
}
而方法真正的執(zhí)行珊豹,如selectList方法:
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
深度分析文章參考
源碼分析Mybatis MapperProxy初始化之Mapper對象的掃描與構(gòu)建 (作者:掘金- 中間件興趣圈)
源碼分析Mybatis MappedStatement的創(chuàng)建流程(作者:掘金- 中間件興趣圈)
Mybatis執(zhí)行SQL的4大基礎組件詳解(作者:掘金- 中間件興趣圈)
源碼解析MyBatis Sharding-Jdbc SQL語句執(zhí)行流程詳解(作者:掘金- 中間件興趣圈)
mybatis 一級二級緩存原理及使用建議(美團技術團隊-官方博客)
面試題集錦
Myabtis的細節(jié)使用和執(zhí)行原理其實都很好理解簸呈,對于源碼感興趣的可以深挖榕订,但大多時候建議點到為止即可
還是著眼當下 面向面試要點進行針對性學習(不包括一些簡單的使用問題)
#{}和${}的區(qū)別是什么?
#{} 是預編譯處理蜕便,${}是字符串替換劫恒。Mybatis 在處理#{}時,會將 sql 中的#{}替換為?號轿腺,調(diào)用 PreparedStatement 的
set 方法來賦值两嘴;
Mybatis 在處理時,就是把時族壳,就是把{}替換成變量的值憔辫。
使用#{}可以有效的防止 SQL 注入,提高系統(tǒng)安全性
PS:mybatis執(zhí)行的本質(zhì)還是SQL仿荆,因此回歸本質(zhì)可以簡單理解為一個對于PreparedStatement 贰您,一個對應 Statement
通常一個 Xml 映射文件坏平,都會寫一個 Dao 接口與之對應,請問锦亦,這個 Dao 接口的工作原理是什么舶替?Dao 接口里的方法,參數(shù)不同時杠园,方法能重載嗎顾瞪?(id是否可以相同)
Dao 接口即 Mapper 接口,接口的全限名,就是映射文件中的 namespace 的值;接口的方法名朴恳,就是映射文件中 Mapper 的 Statement 的 id 值吩谦;接口方法內(nèi)的參數(shù),就是傳遞給 sql 的參數(shù)
實現(xiàn)原理:
Mapper接口的工作原理是JDK動態(tài)代理贷腕,mybatis會對每一個mapper代理生成一個mapperProxy對象,代理對象會攔截接口方法,轉(zhuǎn)而自動對應到sqlsession上尘应,最終由Executor
執(zhí)行
參數(shù)不同,方法不可重載
吼虎,為什么犬钢?
上文說到mybatis有一個環(huán)節(jié)是解析XML文件或者解析接口,它會去構(gòu)建一個叫做 MapperStatement 對象去存儲mapper的相關信息思灰,每一個dao接口方法在執(zhí)行的時候到底是如何定位找到對應的MapperStatement 的呢玷犹?
源碼邏輯圖:
// 這個 mappedStatements 即
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
// Key即 XML文件中配置的
<mapper namespace="com.mycode.mybatis.ScriptDirDao">
<select id="selectOne" resultMap="BaseResultMap">
namespace + id ===》 全限名(NameSpace)+方法名
Mybatis是如何將sql執(zhí)行結(jié)果封裝為目標對象并返回的?都有哪些映射形式洒疚?
第一種是使用 標簽歹颓,逐一定義數(shù)據(jù)庫列名和對象屬性名之間的映射關系
第二種是使用 sql 列的別名功能,將列的別名書寫為對象屬性名油湖,有了列名與屬性名的映射關系后巍扛,Mybatis 通過反射創(chuàng)建對象,同時使用反射給對象的屬性逐一賦值并返回乏德,那些找不到映射關系的屬性撤奸,是無法完成賦值的
Mybatis 是否支持延遲加載?如果支持喊括,它的實現(xiàn)原理是什么胧瓜?
Mybatis 僅支持 association 關聯(lián)對象和 collection 關聯(lián)集合對象的延遲加載,association 指的就是一對一郑什,collection 指的就是一對多查詢府喳,在 Mybatis配置文件中,可以配置是否啟用延遲加載 lazyLoadingEnabled=true|false
它的原理是蘑拯,使用 CGLIB 創(chuàng)建目標對象的代理對象钝满,當調(diào)用目標方法時肉津,進入攔截器方法,比如調(diào)用 a.getB().getName()舱沧,攔截器 invoke()方法發(fā)現(xiàn) a.getB()是null 值妹沙,那么就會單獨發(fā)送事先保存好的查詢關聯(lián) B 對象的 sql,把 B 查詢上來熟吏,然后調(diào)用 a.setB(b)距糖,于是 a 的對象 b 屬性就有值了,接著完成 a.getB().getName()法的調(diào)用牵寺,這就是延遲加載的基本原理
Mybatis 的一級悍引、二級緩存
一級緩存: 基于 PerpetualCache 的 HashMap 本地緩存,其存儲作用域為Session帽氓,當 Session flush 或 close 之后趣斤,該 Session 中的所有 Cache 就將清空,默認打開一級緩存黎休,無法關閉
二級緩存與一級緩存其機制相同浓领,默認也是采用 PerpetualCache,HashMap存儲势腮,不同在于其存儲作用域為 Mapper(Namespace)联贩,并且可自定義存儲源,如 Ehcache捎拯。默認不打開二級緩存泪幌,要開啟二級緩存,使用二級緩存屬性類需要實現(xiàn) Serializable 序列化接口(可用來保存對象的狀態(tài)),可在它的映射文件中配置
對于緩存數(shù)據(jù)更新機制署照,當某一個作用域(一級緩存 Session/二級緩存Namespaces)的進行了 C/U/D 操作后祸泪,默認該作用域下所有 select 中的緩存將被 clear
簡述 Mybatis 的插件運行原理,以及如何編寫一個插件
Mybatis 僅可以編寫針對 ParameterHandler建芙、ResultSetHandler没隘、StatementHandler、Executor 這 4 種接口的插件岁钓,Mybatis 使用 JDK 的動態(tài)代理升略,為需要攔截的接口生成代理對象以實現(xiàn)接口方法攔截功能微王,每當執(zhí)行這 4 種接口對象的方法時屡限,就會進入攔截方法,具體就是 InvocationHandler 的 invoke()方法炕倘,當然钧大,只會攔截那些你指定需要攔截的方法
編寫插件:實現(xiàn) Mybatis 的 Interceptor 接口并復寫 intercept()方法,然后在給插件編寫注解罩旋,指定要攔截哪一個接口的哪些方法即可啊央,需要在配置文件中配置你編寫的插件
Mybatis 的插件實際在執(zhí)行的流程留下了一些固定的環(huán)節(jié)眶诈,允許你自行做一些處理,我們自己利用接口掃描瓜饥,在代碼執(zhí)行的某個階段去調(diào)用目標方法逝撬,也可以實現(xiàn)這種類似插件的做法
Demo:SpringBoot代碼生成器 包含自定義接口攔截,實現(xiàn)了類似的插件效果
以下問題來源于掘金文章【面試官之你說我聽】-MyBatis常見面試題(作者:Ccww)
數(shù)據(jù)庫鏈接中斷如何處理
數(shù)據(jù)庫的訪問底層是通過tcp實現(xiàn)的乓土,當鏈接中斷是程序是無法得知宪潮,導致程序一直會停頓一段時間在這,最終會導致用戶體驗不好趣苏,因此面對數(shù)據(jù)庫連接中斷的異常狡相,該怎么設置mybatis呢?
connection操作底層是一個循環(huán)處理操作食磕,因此可以進行時間有關的參數(shù):
- max_idle_time : 表明最大的空閑時間尽棕,超過這個時間socket就會關閉
- connect_timeout : 表明鏈接的超時時間
在開發(fā)過程中,經(jīng)常遇到插入重復的現(xiàn)象彬伦,這種情況該如何解決呢滔悉?
插入的過程一般都是分兩步的:先判斷是否存在記錄,沒有存在則插入否則不插入单绑。如果存在并發(fā)操作氧敢,那么同時進行了第一步,然后大家都發(fā)現(xiàn)沒有記錄询张,然后都插入了數(shù)據(jù)從而造成數(shù)據(jù)的重復
分布式環(huán)境中通過Redis分布式鎖解決即可孙乖,多線程環(huán)節(jié)下用普通的Lock鎖解決即可
事務執(zhí)行過程中宕機的應對處理方式
數(shù)據(jù)庫插入百萬級數(shù)據(jù)的時候,還沒操作完份氧,但是把服務器重啟了唯袄,數(shù)據(jù)庫會繼續(xù)執(zhí)行嗎? 還是直接回滾了蜗帜?
不會自動繼續(xù)執(zhí)行恋拷,不會自動直接回滾 ,但可以依據(jù)事務日志進行回滾或者進行執(zhí)行。
事務開啟時厅缺,事務中的操作蔬顾,都會先寫入存儲引擎的日志緩沖中,在事務提交之前湘捎,這些緩沖的日志都需要提前刷新到磁盤上持久化 诀豁,兩種類型:
在事務執(zhí)行的過程中,除了記錄redo log窥妇,還會記錄一定量的undo log舷胜。
- redo log :按語句的執(zhí)行順序,依次交替的記錄在一起
- undo log: 主要為事務的回滾服務活翩。undo log記錄了數(shù)據(jù)在每個操作前的狀態(tài)烹骨,如果事務執(zhí)行過程中需要回滾翻伺,就可以根據(jù)undo log進行回滾操作
Mybatis都有哪些Executor執(zhí)行器?它們之間的區(qū)別是什么沮焕?
Mybatis有三種基本的Executor執(zhí)行器吨岭,SimpleExecutor、ReuseExecutor峦树、BatchExecutor未妹。
- SimpleExecutor:每執(zhí)行一次update或select,就開啟一個Statement對象空入,用完立刻關閉Statement對象络它。
- ReuseExecutor:執(zhí)行update或select,以sql作為key查找Statement對象歪赢,存在就使用化戳,不存在就創(chuàng)建,用完后埋凯,不關閉Statement對象点楼,而是放置于Map<String, Statement>內(nèi),供下一次使用白对。簡言之掠廓,就是重復使用Statement對象。
- BatchExecutor:執(zhí)行update(沒有select甩恼,JDBC批處理不支持select)蟀瞧,將所有sql都添加到批處理中(addBatch()),等待統(tǒng)一執(zhí)行(executeBatch())条摸,它緩存了多個Statement對象悦污,每個Statement對象都是addBatch()完畢后,等待逐一執(zhí)行executeBatch()批處理钉蒲。與JDBC批處理相同切端。
作用范圍:Executor的這些特點,都嚴格限制在SqlSession生命周期范圍內(nèi)
在Mybatis配置文件中顷啼,可以指定默認的ExecutorType執(zhí)行器類型踏枣,也可以手動給DefaultSqlSessionFactory的創(chuàng)建SqlSession的方法傳遞ExecutorType類型參數(shù)
最后
Myabtis也有其缺點,重復代碼實在太多钙蒙,在這里推薦我的另一篇文章茵瀑,SpringBoot & Mybatis代碼生成器,解決百分之90的重復代碼仪搔,數(shù)據(jù)庫建好即可CRUD瘾婿,所有XML都是手寫蜻牢,歡迎嘗試一下
點擊 ===》 SpringBoot代碼生成器
集中感謝幾位作者:
掘金-Ccww
掘金- 中間件興趣圈
美團技術團隊-官方博客