Mybatis源碼淺析(一)

前言

最近項目中使用到了Mybatis持久層框架哩簿,由于從來沒有深入的了解過基于Java語言實現(xiàn)的持久層框架宵蕉,于是有點心血來潮,所以就有了這篇長文节榜。下面是來自mybatis官網(wǎng)對其的簡單介紹羡玛。

MyBatis 是支持定制化 SQL、存儲過程以及高級映射的優(yōu)秀的持久層框架宗苍。MyBatis 避免了幾乎所有的 JDBC 代碼和手動設(shè)置參數(shù)以及獲取結(jié)果集稼稿。MyBatis 可以對配置和原生Map使用簡單的 XML 或注解,將接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java對象)映射成數(shù)據(jù)庫中的記錄讳窟。

深入方式

個人覺得最好的學習新東西的方式就是demo让歼,所以打算從頭到位搭建一個demo來貫通整篇文章,下面一一介紹demo中用到的文件挪钓,完整示例可參考附件是越。

Demo入口 (MybatisDemo.java)

import com.hackx.hackspring.domain.memeber.MemberDO;
import com.hackx.hackspring.mapper.member.MemberMapper;
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;

/**
* Created by hackx on 9/26/16.
*/
public class MybatisDemo {

public static void main(String[] args) throws IOException {
String resource = "mybatis-demo-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession session = sqlSessionFactory.openSession();
try {
MemberMapper memberMapper = session.getMapper(MemberMapper.class);
MemberDO memberDO = memberMapper.queryById(1L);
System.out.println(memberDO.toString());
} finally {
session.close();
}
}
}

Mybatis配置 (mybatis-demo-config.xml)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<package name="com.hackx.hackspring.domain"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/db_spring"/>
<property name="username" value="root"/>
<property name="password" value="admin"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mappers/member-mapper.xml"/>
</mappers>
</configuration>

Mapper XML (member-mapper.xml)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.hackx.hackspring.mapper.member.MemberMapper">

<resultMap id="MemberDOResult" type="MemberDO">
<result property="id" column="id"/>
<result property="gmtCreate" column="gmt_create"/>
<result property="gmtModified" column="gmt_modified"/>
<result property="name" column="name"/>
<result property="age" column="age"/>
<result property="email" column="email"/>
<result property="password" column="password"/>
</resultMap>

<sql id="MemberDOFields">
id, gmt_create, gmt_modified, name, age, email, password
</sql>

<!-- id必須與Mapper中對應(yīng)的方法的名稱一致 -->
<select id="queryById" resultMap="MemberDOResult" parameterType="java.lang.Long">
SELECT
<include refid="MemberDOFields"/>
FROM members
WHERE id=#{id}
</select>
</mapper>

Mapper 接口(MemberMapper.java)

import com.hackx.hackspring.domain.memeber.MemberDO;
import org.apache.ibatis.annotations.Mapper;

/**
* Created by hackx on 8/21/16.
*/
@Mapper
public interface MemberMapper {

MemberDO queryById(Long id);
}

DataObject (MemberDO.java)

import java.io.Serializable;
import java.util.Date;

/**
* Created by hackx on 8/21/16.
*/
public class MemberDO implements Serializable {
/**
* 主鍵ID
*/
private Long id;
/**
* 創(chuàng)建時間
*/
private Date gmtCreate;
/**
* 修改時間
*/
private Date gmtModified;
/**
* 會員名稱
*/
private String name;
/**
* 會員年齡
*/
private Integer age;
/**
* 會員郵箱地址
*/
private String email;
/**
* 會員密碼
*/
private String password;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public Date getGmtCreate() {
return gmtCreate;
}

public void setGmtCreate(Date gmtCreate) {
this.gmtCreate = gmtCreate;
}

public Date getGmtModified() {
return gmtModified;
}

public void setGmtModified(Date gmtModified) {
this.gmtModified = gmtModified;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

@Override
public String toString() {
return "MemberDO{" +
"id=" + id +
", gmtCreate=" + gmtCreate +
", gmtModified=" + gmtModified +
", name='" + name + '\'' +
", age=" + age +
", email='" + email + '\'' +
", password='" + password + '\'' +
'}';
}
}

上述就是此Demo用到的所有相關(guān)的文件耳舅,下面按照程序運行的順序依次介紹Mybatis的核心功能模塊碌上。

Mybatis應(yīng)用入口之配置文件

每個基于 MyBatis 的應(yīng)用都是以一個 SqlSessionFactory 的實例為中心的倚评。SqlSessionFactory 的實例可以通過 SqlSessionFactoryBuilder 獲得。而 SqlSessionFactoryBuilder 則可以從 XML 配置文件或一個預先定制的 Configuration 的實例構(gòu)建出 SqlSessionFactory 的實例馏予。所以Mybatis的入口點加載Mybatis的配置文件(本示例中的mybatis-demo-config.xml), Mybatis源碼中org.apache.ibatis.io包下負責文件的讀取天梧,將本地文件以Reader(字符)或者InputStream(字節(jié))的方式讀入內(nèi)存. 下面兩行代碼完成了Mybatis配置文件的加載過程。

String resource = "mybatis-demo-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);

加載過程中霞丧,主要涉及了兩個類:Resources和ClassLoaderWrapper,兩個類都在包org.apache.ibatis.io下呢岗。下面我們先簡單介紹下Resources類:

Resources類

resource
resource

上圖是Resources類中含有的成員變量和方法的簽名,其中有幾個比較重要的方法:

public static URL getResourceURL(ClassLoader, String)
public static InputStream getResourceAsStream(ClassLoader, String)
public static Properties getResourceAsProperties(ClassLoader, String)
public static Reader getResourceAsReader(ClassLoader, String)
public static File getResourceAsFile(ClassLoader, String)

以上幾個不同的方法提供了文件在內(nèi)存的不同表現(xiàn)形式蛹尝,相信每個方法的意義后豫,我們從字面上就已經(jīng)很好的理解了。對于加載Mybatis配置XML文件而言突那,最常用的是下面兩個方法:

public static InputStream getResourceAsStream(ClassLoader, String)
public static Reader getResourceAsReader(ClassLoader, String)

ClassLoaderWrapper類

classLoader
classLoader

Resources類在Mybatis配置文件加載的過程中挫酿,僅僅是為Mybatis框架提供接口,并不參與真正的文件加載操作愕难。而真正的文件加載到內(nèi)容的操作是由ClassLoaderWrapper完成的早龟。ClassLoaderWrapper封裝了java.lang.ClassLoader這個類,而配置文件的加載是使用ClassLoader完成的猫缭。下圖是配置文件加載的時序圖葱弟。

_
_

ClassLoader是java提供對外開放的類加載機制,至于ClassLoader的詳細使用猜丹,可以參考這兩篇文章深入分析Java ClassLoader原理 , Java Classloader Wiki 詳細了解下芝加,本文不再做過多的介紹。

SqlSessionFactory創(chuàng)建

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSessionFactoryBuilder根據(jù)Resources類生成返回的配置文件inputStream來構(gòu)建SqlSessionFactory射窒,一旦創(chuàng)建了SqlSessionFactory,就不再需要它了妖混,其中涉及到的相關(guān)類的關(guān)系如下:

_
_

SqlSessionFactoryBuilder實例調(diào)用build方法,返回SqlSessionFactory實例

public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}

而真正執(zhí)行build邏輯的是下面通的用build方法轮洋,注意這里的environment和properties均為null

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}

從上述代碼中我們可以看出制市,首先創(chuàng)建了XMLConfigBuilder實例,暫時先忽略XMLConfigBuilder的執(zhí)行邏輯弊予,后面會詳細介紹祥楣;然后調(diào)用XMLConfigBuilder實例的parse方法,返回一個Configuration對象汉柒,然后將返回的Configuration對象當作參數(shù)傳給下面的build方法误褪,生成SqlSessionFactory實例。

public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}

在SqlSessionFactory創(chuàng)建過程中碾褂,我們用到了XMLConfigBuilder兽间,它與Configuration類的關(guān)系如下圖,

_2
_2

XMLConfigBuilder構(gòu)造方法

//創(chuàng)建XPathParser實例
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}

其中比較重要的部分是創(chuàng)建XPathParser實例

//創(chuàng)建XPathParser實例
public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
this.document = createDocument(new InputSource(inputStream));
}

commonConstructor完成的工作如下,最重要的是創(chuàng)建了了xpath實例對象正塌,有了它嘀略,我們有可以使用JDK提供的Xpath工具類來來解析XML文件了(此處為mybatis-demo-config.xml)

private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
this.validation = validation;
this.entityResolver = entityResolver;
this.variables = variables;
XPathFactory factory = XPathFactory.newInstance();
this.xpath = factory.newXPath();
}

構(gòu)建了XMLConfigBuilder的實例后恤溶,調(diào)用其parse()方法,其中parser.evalNode("/configuration")獲取到的是根結(jié)點

public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

然后通過以parser.evalNode("/configuration")返回的根節(jié)點為參數(shù)帜羊,調(diào)用parseConfiguration咒程,分別將對應(yīng)的值解析出來塞進
Configuration實例configuration中

private void parseConfiguration(XNode root) {
try {
Properties settings = settingsAsPropertiess(root.evalNode("settings"));
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectionFactoryElement(root.evalNode("reflectionFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
}

從parseConfiguration中我們可以看出mybatis配置文件的大致結(jié)構(gòu),根節(jié)點為configuration讼育,子節(jié)點包括properties帐姻、typeAliases、plugins奶段、objectFactory饥瓷、objectWrapperFactory、reflectionFactory痹籍、environments扛伍、databaseIdProvider、typeHandlers词裤、mappers刺洒、settings等,因為我們平時大部分都是使用Spring來進行管理的吼砂,所有有些配置項可能會比較陌生逆航,隨后我們會重點解釋。上述代碼中比較重要的類有XNode,XPathParser渔肩;XNode是Node類的擴展,XPathParser是xml文件的解析器工具類因俐。XPathParser中比較重要的方法是:public XNode evalNode(String expression)而evalNode最終調(diào)用的是com.sun.org.apache.xpath.internal.jaxp.XPathImpl
里的public Object evaluate(String expression, Object item, QName returnType).

下面是解析mappers的源碼,供參考周偎。

private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}

SqlSession創(chuàng)建

SqlSession session = sqlSessionFactory.openSession();

通過調(diào)用sqlSessionFactory的openSession方法來創(chuàng)建SqlSession實例

//DefaultSqlSessionFactory里的openSession
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

上述代碼涉及到了執(zhí)行器抹剩,因為最終我們是要執(zhí)行SQL的,所以這東西一定不能少蓉坎。執(zhí)行器有三類:SIMPLE(普通執(zhí)行器),REUSE(執(zhí)行器會重用預處理語句)和BATCH(執(zhí)行器將重用語句并批量執(zhí)行)

_3
_3
//執(zhí)行器生成過程
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}

在生成執(zhí)行器時有個是否緩存的判斷if (cacheEnabled),這個配置時二級緩存的開關(guān)澳眷,在配置mybatis的時候,可按照下面的配置將二級緩存打開

<settings>
<setting name="cacheEnabled" value="true"/>
</settings>

執(zhí)行器創(chuàng)建后,通過生成DefaultSqlSession的實例對象蛉艾,最終創(chuàng)建SqlSession钳踊,需要注意的是SqlSession 實例不是線程安全的,是不能被共享的,所以它的最佳范圍是請求或方法范圍.每個線程都應(yīng)該有自己的SqlSession實例.

public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
this.configuration = configuration;
this.executor = executor;
this.dirty = false;
this.autoCommit = autoCommit;
}

Member對象創(chuàng)建及SQL執(zhí)行

這個過程沒看太懂,其中涉及了一些Proxy代理的東西勿侯,先把代碼羅列在這拓瞪,后續(xù)在慢慢補充。

MemberMapper memberMapper = session.getMapper(MemberMapper.class);
MemberDO memberDO = memberMapper.queryById(1L);
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末助琐,一起剝皮案震驚了整個濱河市祭埂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兵钮,老刑警劉巖蛆橡,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舌界,死亡現(xiàn)場離奇詭異,居然都是意外死亡航罗,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門屁药,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粥血,“玉大人,你說我怎么就攤上這事酿箭「纯鳎” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵缭嫡,是天一觀的道長缔御。 經(jīng)常有香客問我,道長妇蛀,這世上最難降的妖魔是什么耕突? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮评架,結(jié)果婚禮上眷茁,老公的妹妹穿的比我還像新娘。我一直安慰自己纵诞,他們只是感情好上祈,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著浙芙,像睡著了一般登刺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嗡呼,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天纸俭,我揣著相機與錄音,去河邊找鬼南窗。 笑死掉蔬,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的矾瘾。 我是一名探鬼主播女轿,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼壕翩!你這毒婦竟也來了蛉迹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤放妈,失蹤者是張志新(化名)和其女友劉穎北救,沒想到半個月后荐操,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡珍策,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年托启,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片攘宙。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡屯耸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蹭劈,到底是詐尸還是另有隱情疗绣,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布铺韧,位于F島的核電站多矮,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏哈打。R本人自食惡果不足惜塔逃,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望料仗。 院中可真熱鬧患雏,春花似錦、人聲如沸罢维。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肺孵。三九已至匀借,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間平窘,已是汗流浹背吓肋。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瑰艘,地道東北人是鬼。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像紫新,于是被迫代替她去往敵國和親均蜜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容