圖文并茂解析Mybatis配置加載過程!
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你來,我們一起精進!你不來,我和你的競爭對手一起精進!
編輯:業(yè)余草
blog.csdn.net/b379685397
推薦:https://www.xttblog.com/?p=5353
一、Mybatis運行流程概述
為了熟悉Mybatis的運行流程,我們先看一段代碼。
public class MybatisDemo {
private SqlSessionFactory sqlSessionFactory;
@Before
public void init() throws IOException {
//--------------------第一步:加載配置---------------------------
// 1.讀取mybatis配置文件創(chuàng)SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 1.讀取mybatis配置文件創(chuàng)SqlSessionFactory
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
inputStream.close();
}
@Test
// 快速入門
public void quickStart() throws IOException {
//--------------------第二部,創(chuàng)建代理對象---------------------------
// 2.獲取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3.獲取對應mapper
TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
//--------------------第三步:獲取數據---------------------------
// 4.執(zhí)行查詢語句并返回單條數據
TUser user = mapper.selectByPrimaryKey(2);
System.out.println(user);
System.out.println("----------------------------------");
// 5.執(zhí)行查詢語句并返回多條數據
// List<TUser> users = mapper.selectAll();
// for (TUser tUser : users) {
// System.out.println(tUser);
// }
}
}
以上是我們一個使用mybatis訪問數據的demo,通過對快速入門代碼的分析,可以把 MyBatis 的運行流程分為三大階段:
「初始化階段」:讀取 XML 配置文件和注解中的配置信息,創(chuàng)建配置對象,并完成各個模塊的初始化的工作; 「代理封裝階段」:封裝 iBatis 的編程模型,使用 mapper 接口開發(fā)的初始化工作; 「數據訪問階段」:通過 SqlSession 完成 SQL 的解析,參數的映射、SQL 的執(zhí)行、結果的解析過程;
今天我們就介紹以下第一個階段中,Mybatis是如何讀取配置的
二、配置加載的核心類
建造器三個核心類
在 MyBatis 中負責加載配置文件的核心類有三個,類圖如下:

BaseBuilder:所有解析器的父類,包含配置文件實例,為解析文件提供的一些通用的方法; XMLConfigBuilder:主要負責解析 mybatis-config.xml; XMLMapperBuilder:主要負責解析映射配置 Mapper.xml 文件; XMLStatementBuilder:主要負責解析映射配置文件中的 SQL 節(jié)點;
XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder 這三個類在配置文件加載過程中非常重要,具體分工如下圖所示:

這三個類使用了建造者模式對 configuration 對象進行初始化,但是沒有使用建造者模式
的“肉體”(流式編程風格),只用了靈魂(屏蔽復雜對象的創(chuàng)建過程),把建造者模式演繹
成了工廠模式;后面還會對這三個類源碼進行分析;
居然這三個對象使用的是建造者模式,那么我們稍后介紹下什么是建造者模式
三、建造者模式
什么是建造者模式
建造者模式(BuilderPattern)使用多個簡單的對象一步一步構建成一個復雜的對象。這種類型的設計模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對象的最佳方式。
建造者模式類圖如下:

各要素如下:
「Product」:要創(chuàng)建的復雜對象 「Builder」:給出一個抽象接口,以規(guī)范產品對象的各個組成成分的建造。這個接口規(guī)定要實現復雜對象的哪些部分的創(chuàng)建,并不涉及具體的對象部件的創(chuàng)建; 「ConcreteBuilder」:實現 Builder 接口,針對不同的商業(yè)邏輯,具體化復雜對象的各部分的創(chuàng)建。在建造過程完成后,提供產品的實例; 「Director」:調用具體建造者來創(chuàng)建復雜對象的各個部分,在指導者中不涉及具體產品的信息,只負責保證對象各部分完整創(chuàng)建或按某種順序創(chuàng)建;
應用舉例:紅包的創(chuàng)建是個復雜的過程,可以使用構建者模式進行創(chuàng)建
代碼示例:
1、紅包對象RedPacket
public class RedPacket {
private String publisherName; //發(fā)包人
private String acceptName; //收包人
private BigDecimal packetAmount; //紅包金額
private int packetType; //紅包類型
private Date pulishPacketTime; //發(fā)包時間
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 的流式編程等等都是例子。流式編程的優(yōu)點在于代碼編程性更高、可讀性更好,缺點在于對程序員編碼要求更高、不太利于調試。建造者模式是實現流式編程風格的一種方式;
與工廠模式區(qū)別
建造者模式應用場景如下:
需要生成的對象具有復雜的內部結構,實例化對象時要屏蔽掉對象代碼與復雜對象的實例化過程解耦,可以使用建造者模式;簡而言之,如果“遇到多個構造器參數時要考慮用構建器”; 對象的實例化是依賴各個組件的產生以及裝配順序,關注的是一步一步地組裝出目標對 象,可以使用建造器模式;
建造者模式與工程模式的區(qū)別在于:
| 「設計模式」 | 「形象比喻」 | 「對象復雜度」 | 「客戶端參與程度」 |
| 工廠模式 | 生產大眾版 | 關注的是一個產品整體,無須關心產品的各部分是如何創(chuàng)建出來的; | 客戶端對產品的創(chuàng)建過程參與度低,對象實例化時屬性值相對比較固定; |
| 建造者模式 | 生產定制版 | 建造的對象更加復雜,是一個復合產品,它由各個部件復合而成,部件不同產品對象不同,生成的產品粒度細; | 客戶端參與了產品的創(chuàng)建,決定了產品的類型和內容,參與度高;適合實例化對象時屬性變化頻繁的場景; |
四、Configuration 對象介紹
實例化并初始化 Configuration 對象是第一個階段的最終目的,所以熟悉 configuration 對
象是理解第一個階段代碼的核心;configuration 對象的關鍵屬性解析如下:
MapperRegistry:mapper 接口動態(tài)代理工廠類的注冊中心。在 MyBatis 中,通過mapperProxy 實現 InvocationHandler 接口,MapperProxyFactory 用于生成動態(tài)代理的實例對象; ResultMap:用于解析 mapper.xml 文件中的 resultMap 節(jié)點,使用 ResultMapping 來封裝id,result 等子元素; MappedStatement:用于存儲 mapper.xml 文件中的 select、insert、update 和 delete 節(jié)點,同時還包含了這些節(jié)點的很多重要屬性; SqlSource:用于創(chuàng)建 BoundSql,mapper.xml 文件中的 sql 語句會被解析成 BoundSql 對象,經過解析 BoundSql 包含的語句最終僅僅包含?占位符,可以直接提交給數據庫執(zhí)行;
Configuration對象圖解:

需要特別注意的是 Configuration 對象在 MyBatis 中是單例的,生命周期是應用級的,換句話說只要 MyBatis 運行 Configuration 對象就會獨一無二的存在;在 MyBatis 中僅在
org.apache.ibatis.builder.xml.XMLConfigBuilder.XMLConfigBuilder(XPathParser, String, Properties)中有實例化 configuration 對象的代碼,如下圖:

Configuration 對象的初始化(屬性復制),是在建造 SqlSessionfactory 的過程中進行的,接下
來分析第一個階段的內部流程;
五、配置加載流程解析
配置加載過程
可以把第一個階段配置加載過程分解為四個步驟,四個步驟如下圖:

第一步:通過 SqlSessionFactoryBuilder 建造 SqlSessionFactory,并創(chuàng)建 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
//解析<properties>節(jié)點
propertiesElement(root.evalNode("properties"));
//解析<settings>節(jié)點
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
//解析<typeAliases>節(jié)點
typeAliasesElement(root.evalNode("typeAliases"));
//解析<plugins>節(jié)點
pluginElement(root.evalNode("plugins"));
//解析<objectFactory>節(jié)點
objectFactoryElement(root.evalNode("objectFactory"));
//解析<objectWrapperFactory>節(jié)點
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//解析<reflectorFactory>節(jié)點
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);//將settings填充到configuration
// read it after objectFactory and objectWrapperFactory issue #631
//解析<environments>節(jié)點
environmentsElement(root.evalNode("environments"));
//解析<databaseIdProvider>節(jié)點
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//解析<typeHandlers>節(jié)點
typeHandlerElement(root.evalNode("typeHandlers"));
//解析<mappers>節(jié)點
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
第三步:XMLMapperBuilder 的核心方法為 configurationElement(XNode),該方法對 mapper.xml 配置文件的各個元素進行解析,讀取元素信息后填充到 configuration 對象。
private void configurationElement(XNode context) {
try {
//獲取mapper節(jié)點的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節(jié)點
cacheRefElement(context.evalNode("cache-ref"));
//重點分析 :解析cache節(jié)點----------------1-------------------
cacheElement(context.evalNode("cache"));
//解析parameterMap節(jié)點(已廢棄)
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//重點分析 :解析resultMap節(jié)點----------------2-------------------
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析sql節(jié)點
sqlElement(context.evalNodes("/mapper/sql"));
//重點分析 :解析select、insert、update、delete節(jié)點 ----------------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 節(jié)點,這個方法非常重要, 一定要跟源碼理解;解析完之后數據保存在 configuration 對象的 resultMaps 屬性中;如下圖

2XMLMapperBuilder 中在實例化二級緩存(見 cacheElement(XNode))、實例化 resultMap (見 resultMapElements(List))過程中都使用了建造者模式,而且是建造者模 式的典型應用; XMLMapperBuilder 和 XMLMapperStatmentBuilder 有 自 己 的 “ 秘 書 ” MapperBuilderAssistant。XMLMapperBuilder 和 XMLMapperStatmentBuilder 負責解析 讀取配置文件里面的信息,MapperBuilderAssistant 負責將信息填充到 configuration。將文件解析和數據的填充的工作分離在不同的類中,符合單一職責原則; 在 buildStatementFromContext(List)方法中,創(chuàng)建 XMLStatmentBuilder 解析 Mapper.xml 中 select、insert、update、delete 節(jié)點
第四步:在 XMLStatmentBuilder 的 parseStatementNode()方法中,對 Mapper.xml 中 select、 insert、update、delete 節(jié)點進行解析,并調用 MapperBuilderAssistant 負責將信息填充到 configuration。在理解 parseStatementNod()方法之前,有必要了解 MappedStatement,這個類用于封裝 select、insert、update、delete 節(jié)點的信息;如下圖所示:

至此,整個Mybatis的配置即加載完畢,整個加載流程圖如下:

