Myabtis原始碼分析五-Mybatis配置載入完全圖解,建造者模式的使用
一起養成寫作習慣!這是我參與「掘金日新計劃 · 4 月更文挑戰」的第10天,[點選檢視活動詳情]
一、Mybatis執行流程概述
為了熟悉Mybatis的執行流程,我們先看一段程式碼
``` public class MybatisDemo {
private SqlSessionFactory sqlSessionFactory;
@Before
public void init() throws IOException {
//--------------------第一步:載入配置---------------------------
// 1.讀取mybatis配置檔案創SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 1.讀取mybatis配置檔案創SqlSessionFactory
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
inputStream.close();
}
@Test
// 快速入門
public void quickStart() throws IOException {
//--------------------第二部,建立代理物件---------------------------
// 2.獲取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3.獲取對應mapper
TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
//--------------------第三步:獲取資料---------------------------
// 4.執行查詢語句並返回單條資料
TUser user = mapper.selectByPrimaryKey(2);
System.out.println(user);
System.out.println("----------------------------------");
// 5.執行查詢語句並返回多條資料
// List
以上是我們一個使用mybatis訪問資料的demo,通過對快速入門程式碼的分析,可以把 MyBatis 的執行流程分為三大階段:
- 初始化階段:讀取 XML 配置檔案和註解中的配置資訊,建立配置物件,並完成各個模組的初始化的工作;
- 代理封裝階段:封裝 iBatis 的程式設計模型,使用 mapper 介面開發的初始化工作;
- 資料訪問階段:通過 SqlSession 完成 SQL 的解析,引數的對映、SQL 的執行、結果的解析過程;
今天我們就介紹以下第一個階段中,Mybatis是如何讀取配置的
二、配置載入的核心類
2.1 建造器三個核心類
在 MyBatis 中負責載入配置檔案的核心類有三個,類圖如下:
- BaseBuilder:所有解析器的父類,包含配置檔案例項,為解析檔案提供的一些通用的方法;
- XMLConfigBuilder: 主要負責解析 mybatis-config.xml;
- XMLMapperBuilder: 主要負責解析對映配置 Mapper.xml 檔案;
- XMLStatementBuilder: 主要負責解析對映配置檔案中的 SQL 節點;
XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder 這三個類在配置檔案載入過程中非常重要,具體分工如下圖所示:
這三個類使用了建造者模式對 configuration 物件進行初始化,但是沒有使用建造者模式\ 的“肉體”(流式程式設計風格),只用了靈魂(遮蔽複雜物件的建立過程),把建造者模式演繹\ 成了工廠模式;後面還會對這三個類原始碼進行分析;
居然這三個物件使用的是建造者模式,那麼我們稍後介紹下什麼是建造者模式
三、建造者模式
3.1 什麼是建造者模式
建造者模式(BuilderPattern)使用多個簡單的物件一步一步構建成一個複雜的物件。這種型別的設計模式屬於建立型模式,它提供了一種建立物件的最佳方式。
建造者模式類圖如下:
各要素如下:
- Product:要建立的複雜物件
- Builder:給出一個抽象介面,以規範產品物件的各個組成成分的建造。這個介面規定要實現複雜物件的哪些部分的建立,並不涉及具體的物件部件的建立;
- ConcreteBuilder:實現 Builder 介面,針對不同的商業邏輯,具體化複雜物件的各部分的建立。 在建造過程完成後,提供產品的例項;
- Director:呼叫具體建造者來建立複雜物件的各個部分,在指導者中不涉及具體產品的資訊,只負責保證物件各部分完整建立或按某種順序建立;
應用舉例:紅包的建立是個複雜的過程,可以使用構建者模式進行建立
程式碼示例:
1、紅包物件RedPacket
```
public class RedPacket {
private String publisherName; //發包人
private String acceptName; //收包人
private BigDecimal packetAmount; //紅包金額
private int packetType; //紅包型別
private Date pulishPacketTime; //發包時間
private Date openPacketTime; //搶包時間
public RedPacket(String publisherName, String acceptName, BigDecimal packetAmount, int packetType, Date pulishPacketTime, Date openPacketTime) {
this.publisherName = publisherName;
this.acceptName = acceptName;
this.packetAmount = packetAmount;
this.packetType = packetType;
this.pulishPacketTime = pulishPacketTime;
this.openPacketTime = openPacketTime;
}
public String getPublisherName() {
return publisherName;
}
public void setPublisherName(String publisherName) {
this.publisherName = publisherName;
}
public String getAcceptName() {
return acceptName;
}
public void setAcceptName(String acceptName) {
this.acceptName = acceptName;
}
public BigDecimal getPacketAmount() {
return packetAmount;
}
public void setPacketAmount(BigDecimal packetAmount) {
this.packetAmount = packetAmount;
}
public int getPacketType() {
return packetType;
}
public void setPacketType(int packetType) {
this.packetType = packetType;
}
public Date getPulishPacketTime() {
return pulishPacketTime;
}
public void setPulishPacketTime(Date pulishPacketTime) {
this.pulishPacketTime = pulishPacketTime;
}
public Date getOpenPacketTime() {
return openPacketTime;
}
public void setOpenPacketTime(Date openPacketTime) {
this.openPacketTime = openPacketTime;
}
@Override
public String toString() {
return "RedPacket [publisherName=" + publisherName + ", acceptName="
+ acceptName + ", packetAmount=" + packetAmount
+ ", packetType=" + packetType + ", pulishPacketTime="
+ pulishPacketTime + ", openPacketTime=" + openPacketTime + "]";
}
} ```
2、構建物件
``` public class Director {
public static void main(String[] args) {
RedPacket redPacket = RedPacketBuilderImpl.getBulider().setPublisherName("DK")
.setAcceptName("粉絲")
.setPacketAmount(new BigDecimal("888"))
.setPacketType(1)
.setOpenPacketTime(new Date())
.setPulishPacketTime(new Date()).build();
System.out.println(redPacket);
}
} ```
PS:流式程式設計風格越來越流行,如 zookeeper 的 Curator、JDK8 的流式程式設計等等都是例子。流式程式設計的優點在於程式碼程式設計性更高、可讀性更好,缺點在於對程式設計師編碼要求更高、不太利於除錯。建造者模式是實現流式程式設計風格的一種方式;
3.2 與工廠模式區別
建造者模式應用場景如下:
- 需要生成的物件具有複雜的內部結構,例項化物件時要遮蔽掉物件程式碼與複雜物件的例項化過程解耦,可以使用建造者模式;簡而言之,如果“遇到多個構造器引數時要考慮用構建器”;
- 物件的例項化是依賴各個元件的產生以及裝配順序,關注的是一步一步地組裝出目標對
- 象,可以使用建造器模式;
建造者模式與工程模式的區別在於:
| | | | | | -------- | -------- | ------------------------------------------------- | ----------------------------------------------- | | 設計模式 | 形象比喻 | 物件複雜度 | 客戶端參與程度 | | 工廠模式 | 生產大眾版 | 關注的是一個產品整體,無須關心產品的各部分是如何創建出來的; | 客戶端對產品的建立過程參與度低,物件例項化時屬性值相對比較固定; | | 建造者模式 | 生產定製版 | 建造的物件更加複雜,是一個複合產品,它由各個部件複合而成,部件不同產品物件不同,生成的產品粒度細; | 客戶端參與了產品的建立,決定了產品的型別和內容,參與度高;適合例項化物件時屬性變化頻繁的場景; |
四、Configuration 物件介紹
例項化並初始化 Configuration 物件是第一個階段的最終目的,所以熟悉 configuration 對\ 象是理解第一個階段程式碼的核心;configuration 物件的關鍵屬性解析如下:
- MapperRegistry:mapper 介面動態代理工廠類的註冊中心。在 MyBatis 中,通過mapperProxy 實現 InvocationHandler 介面,MapperProxyFactory 用於生成動態代理的例項物件;
- ResultMap:用於解析 mapper.xml 檔案中的 resultMap 節點,使用 ResultMapping 來封裝id,result 等子元素;
- MappedStatement:用於儲存 mapper.xml 檔案中的 select、insert、update 和 delete 節點,同時還包含了這些節點的很多重要屬性;
- SqlSource:用於建立 BoundSql,mapper.xml 檔案中的 sql 語句會被解析成 BoundSql 物件,經過解析 BoundSql 包含的語句最終僅僅包含?佔位符,可以直接提交給資料庫執行;
Configuration物件圖解:
需要特別注意的是 Configuration 物件在 MyBatis 中是單例的,生命週期是應用級的,換句話說只要 MyBatis 執行 Configuration 物件就會獨一無二的存在;在 MyBatis 中僅在\ org.apache.ibatis.builder.xml.XMLConfigBuilder.XMLConfigBuilder(XPathParser, String, Properties)中有例項化 configuration 物件的程式碼,如下圖:
Configuration 物件的初始化(屬性複製),是在建造 SqlSessionfactory 的過程中進行的,接下\ 來分析第一個階段的內部流程;
五、配置載入流程解析
5.1 配置載入過程
可以把第一個階段配置載入過程分解為四個步驟,四個步驟如下圖:
\
第一步:通過 SqlSessionFactoryBuilder 建造 SqlSessionFactory,並建立 XMLConfigBuilder 對\
象 讀 取 MyBatis 核 心 配 置 文 件 , 見 原始碼方 法 :\
org.apache.ibatis.session.SqlSessionFactoryBuilder.build(Reader, String, Properties):
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
//讀取配置檔案
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());//解析配置檔案得到configuration物件,並返回SqlSessionFactory
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
第二步:進入 XMLConfigBuilder 的 parseConfiguration 方法,對 MyBatis 核心配置檔案的各個\ 元素進行解析,讀取元素資訊後填充到 configuration 物件。在 XMLConfigBuilder 的\ mapperElement()方法中通過 XMLMapperBuilder 讀取所有 mapper.xml 檔案;見方法:\ org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration(XNode);
``` public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; }
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
//解析
第三步:XMLMapperBuilder 的核心方法為 configurationElement(XNode),該方法對 mapper.xml 配置檔案的各個元素進行解析,讀取元素資訊後填充到 configuration 物件。
private void configurationElement(XNode context) {
try {
//獲取mapper節點的namespace屬性
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//設定builderAssistant的namespace屬性
builderAssistant.setCurrentNamespace(namespace);
//解析cache-ref節點
cacheRefElement(context.evalNode("cache-ref"));
//重點分析 :解析cache節點----------------1-------------------
cacheElement(context.evalNode("cache"));
//解析parameterMap節點(已廢棄)
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//重點分析 :解析resultMap節點----------------2-------------------
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析sql節點
sqlElement(context.evalNodes("/mapper/sql"));
//重點分析 :解析select、insert、update、delete節點 ----------------3-------------------
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
在 XMLMapperBuilder 解析過程中,有四個點需要注意:
- resultMapElements(List)方法用於解析 resultMap 節點,這個方法非常重要, 一定要跟原始碼理解;解析完之後資料儲存在 configuration 物件的 resultMaps 屬性中;如下圖
- 2XMLMapperBuilder 中在例項化二級快取(見 cacheElement(XNode))、例項化 resultMap (見 resultMapElements(List))過程中都使用了建造者模式,而且是建造者模 式的典型應用;
- XMLMapperBuilder 和 XMLMapperStatmentBuilder 有 自 己 的 “ 祕 書 ” MapperBuilderAssistant。XMLMapperBuilder 和 XMLMapperStatmentBuilder 負責解析 讀取配置檔案裡面的資訊,MapperBuilderAssistant 負責將資訊填充到 configuration。 將檔案解析和資料的填充的工作分離在不同的類中,符合單一職責原則;
- 在 buildStatementFromContext(List)方法中,建立 XMLStatmentBuilder 解析 Mapper.xml 中 select、insert、update、delete 節點
第四步:在 XMLStatmentBuilder 的 parseStatementNode()方法中,對 Mapper.xml 中 select、 insert、update、delete 節點進行解析,並呼叫 MapperBuilderAssistant 負責將資訊填充到 configuration。在理解 parseStatementNod()方法之前,有必要了解 MappedStatement,這個 類 用 於 封 裝 select 、 insert 、 update 、 delete 節 點 的 信 息 ; 如 下 圖 所 示 :
至此,整個Mybatis的配置即載入完畢,整個載入流程圖如下:
\
- 保姆級JAVA對接ChatGPT教程,實現自己的AI對話助手
- ChatGPT保姆級註冊教學
- 公司產品太多了,怎麼實現一次登入產品互通?
- 前一陣鬧得沸沸揚揚的IP歸屬地,到底是怎麼實現的?
- 漫談資料安全-老闆擔心敏感資料洩露,該如何建設安全的資料體系?
- 【JAVA祕籍功法篇-分散式事務】事務的實現原理
- 必知必會JVM三-面試必備,JVM堆記憶體詳解
- Spring Cloud Zuul閘道器修改為短連線方法
- Jar包問題查詢指令碼
- Myabtis原始碼分析五-Mybatis配置載入完全圖解,建造者模式的使用
- Myabtis原始碼分析四-快取模組分析 ,裝飾模式的使用
- Mybatis開發要點-resultType和resultMap有什麼區別?
- 程式碼review神器Upsource,讓你快樂的進行CodeReview