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

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

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

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

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

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

第一步:通過 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.
}
}
}
第二步:進(jìn)入 XMLConfigBuilder 的 parseConfiguration 方法,對 MyBatis 核心配置文件的各個(gè)元素進(jìn)行解析,讀取元素信息后填充到 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é)點(diǎn)
propertiesElement(root.evalNode("properties"));
//解析<settings>節(jié)點(diǎn)
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
//解析<typeAliases>節(jié)點(diǎn)
typeAliasesElement(root.evalNode("typeAliases"));
//解析<plugins>節(jié)點(diǎn)
pluginElement(root.evalNode("plugins"));
//解析<objectFactory>節(jié)點(diǎn)
objectFactoryElement(root.evalNode("objectFactory"));
//解析<objectWrapperFactory>節(jié)點(diǎn)
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//解析<reflectorFactory>節(jié)點(diǎn)
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);//將settings填充到configuration
// read it after objectFactory and objectWrapperFactory issue #631
//解析<environments>節(jié)點(diǎn)
environmentsElement(root.evalNode("environments"));
//解析<databaseIdProvider>節(jié)點(diǎn)
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//解析<typeHandlers>節(jié)點(diǎn)
typeHandlerElement(root.evalNode("typeHandlers"));
//解析<mappers>節(jié)點(diǎn)
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
第三步:XMLMapperBuilder 的核心方法為 configurationElement(XNode),該方法對 mapper.xml 配置文件的各個(gè)元素進(jìn)行解析,讀取元素信息后填充到 configuration 對象。
private void configurationElement(XNode context) {
try {
//獲取mapper節(jié)點(diǎn)的namespace屬性
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//設(shè)置builderAssistant的namespace屬性
builderAssistant.setCurrentNamespace(namespace);
//解析cache-ref節(jié)點(diǎn)
cacheRefElement(context.evalNode("cache-ref"));
//重點(diǎn)分析 :解析cache節(jié)點(diǎn)----------------1-------------------
cacheElement(context.evalNode("cache"));
//解析parameterMap節(jié)點(diǎn)(已廢棄)
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//重點(diǎn)分析 :解析resultMap節(jié)點(diǎn)----------------2-------------------
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析sql節(jié)點(diǎn)
sqlElement(context.evalNodes("/mapper/sql"));
//重點(diǎn)分析 :解析select、insert、update、delete節(jié)點(diǎn) ----------------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 解析過程中,有四個(gè)點(diǎn)需要注意:
resultMapElements(List)方法用于解析 resultMap 節(jié)點(diǎn),這個(gè)方法非常重要, 一定要跟源碼理解;解析完之后數(shù)據(jù)保存在 configuration 對象的 resultMaps 屬性中;如下圖

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

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

