簡介
Mybatis是一款優秀的ORM框架,是國內使用的ORM框架中的主流,它使用簡單,入們很快。對於有過幾年Java開發經驗的老司機,我相信都對它如何使用已經能夠輕車熟路了,但是,閱讀Mybatis的源碼的估計不多,如果各位朋友和要是和本人一樣,對閱讀源碼很感興趣,想要知道Mybatis廬山真面目的,可以一起探討探討。
Mybatis的使用的入口
我們先來看一下最簡單的查詢一個對象的實例 。
//讀取配置
Reader reader = Resources.getResourceAsReader("mybatisCfg.xml");
//創建sqlSeessionFactory
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);
//打開sqlSession
SqlSession ssn = ssf.openSession();
//查詢一個user對象
User user = ssn.selectOne("mapping.userMapper.getUserByName", "testUser");
//關閉會話
ssn.close();
上面的代碼是Mybatis最簡單的使用例子,麻雀雖小,五臟俱全,雖然只有短短5行代碼,它已經完整的包括了Mybatis的使用週期。Mybatis的週期從大體上可分爲三部分
我們要對Mybatis源碼解析,首先我們需要先列出Mybatis整個流程當中可能會用到的對象類型列出來,先弄清大體的框架,再去摳細節
上面的類型,其實還可以分得更簡單一些,分爲三個過程,再將每個類型對位到不同的過程,像下圖那樣
初始化
那麼如何從代碼的角度去分析mybatis的初始化過程,首先我用一個圖簡單表示一下需要用到的類型前後關係
Mybatis的使用的第一步是傳入根配置信息,我們拿最開頭的事例的頭兩行代碼出來看看
//Reader只是一個讀文件的接口,這裏還沒有開始讀
Reader reader = Resources.getResourceAsReader("mybatisCfg.xml");
//將配置文入口傳給SqlSessionFactoryBuilder的實例方法
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);
我再點擊進去看一下SqlSessionFactoryBuilder的build()方法的實現
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} 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.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
build代碼很簡單,它有幾個重載但沒有多大區別,我們看到new XMLConfigBuilder(reader, environment, properties),它有個reader參數就是我們在外面傳的配置文件入口,environment是指定的環境(是一個字符串),properties是系統配置信息,我們上面的例子中,沒有用到這兩個參數,所以傳的都是空。接下來,我們在看到build(parser.parse()),parse是解析的意思,它的作用是把配置信息解析成爲Configuration對象,然後再創建DefaultSqlSessionFactory對象,DefaultSqlSessionFactory有一個Configuration類型的參數的構造函數。
我們知道DefaultSqlSessionFactory是怎麼new來了,那我們再一探個究竟,看看DefaultSqlSessionFactory的部分源碼和SqlSessionFactory接口定義
public interface SqlSessionFactory {
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
Configuration getConfiguration();
}
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
}
從上面的代碼可以看到SqlSessionFactory 的接口定義的都是openSession方法和getConfiguration方法,也就是說它的作用只有一個就是用來創建SqlSession對象,並且裏面存有一個Configuration的字段。我們再來看看DefaultSqlSessionFactory 的構造函數的實現,DefaultSqlSessionFactory 構造函數只做一件事情,就是保存configuration字段變量。
看了上面的代碼之後,其實我們還是不知道Mybatis怎麼初始化,因爲整個初始化的核心是其實不在SqlSessionFactoryBuiler上,而是在XMLConfigBuilder上。它纔是解析整個配置的核心,XMLConfigBuilder的作用是讀取根配置文件的配置信息,然後解析爲Java對象,最終將它放到Configuration對象裏面。
在講解XMLConfigBuilder之前,我們先來看一下Configuration類內部情況,因爲XMLConfigBuilder解析的過程離不開configuration對象
上面的代碼很多,但我們不需要細看每句代碼,從大體上看,它分爲各種配置字段和工廠方法,它的構造函數會初始化一部分配置。那爲什麼要把工廠方法也放到Configuration上呢? 其實這裏的工廠方法都是和配置相關的,例如創建MappedStatement對象,我們知道Mybatis的sql都是寫在Mapper配置裏面, 創建MappedStatement對象需要讀取配置信息,所以把工廠方法放到Configuration裏面也是合情合理的,所以不要把Configuration理解爲只有配置信息(其實還包括工廠方法,不過個人覺得Configuration和工廠方法放在一起耦合有點大,如果要自己寫個自定義Executor類型,想要簡單的加到Configuration上是沒有辦法的)。
接下來,我們來分析加載過程的真正核心,XMLConfigBuilder的內部實現。下面列出XMLConfigBuilder的部分源碼
public abstract class BaseBuilder {
protected final Configuration configuration;
protected final TypeAliasRegistry typeAliasRegistry;
protected final TypeHandlerRegistry typeHandlerRegistry;
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
}
public class XMLConfigBuilder extends BaseBuilder {
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
}
上面列出了XMLConfigBuilder的構造函數和它的父類,先看構造函數,首先它new了一個configuration對象,然後調用父類的構造函數,父類的構造函數做了什麼呢?BaseBuilder它只有有三個字段成員,分別爲configuration ,typeAliasRegistry,typeHandlerRegistry。configuration是外面傳進來的,只需把它存起來就行了, typeAliasRegistry和typeHandlerRegistry是從configuration內部取出來的,它的作用是什麼了,看下面的截圖,其實不難猜到,typeAliasRegistry是一個別名(Alias)和類型(Type)的映射器(有多個別名對應一個type),因爲Java的基礎類型和數據庫的數據類型是不一樣的,所以下面TypeHandlerRegistry可以看到,每個基礎類型都註冊了一個Handler類型處理程序,所以它的作用就是用來處理類型轉換的。例如:我們從數據表裏面拿到了一個int類型的字段, 那我們就需要從TypeHandlerRegistry裏面找到一個IntegerTypeHandler對象,然後把它轉爲Java的Integer對象。現在調用完父類的構造函數後,接下來要處理的是設置environment (是環境的名稱,比如dev,prod等等)和系統環境參數props, 設置parsed爲false,最後設置parser解析器,parser的類型是XPathParser,它的作用是用來解析Xml文件。
接下來,我們再進一步分析,看看XMLConfigBuilder類裏面的parse()方法是怎麼解析的
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
首先執行parse方法時,需要判斷是否已經解析過,因爲Configuration實例的初始化不是冪等的,不能重複執行,不然會造成configuration配置重複的問題,接下來我們看到parser.evalNode("/configuration")方法,evalNode()方法的作用是找到Xml文件中的某個節點並將它轉爲XNode對象返回。
下面是一個簡單的MyBatis的根配置信息
<?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>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC" />
<!-- 配置數據庫連接信息 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://192.168.31.18:3306/UM?useUnicode=true&characterEncoding=UTF-8" />
<property name="username" value="root" />
<property name="password" value="abc123" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapping/userMapper.xml"/>
<mapper class="mapping.account_mapper"/>
</mappers>
</configuration>
通過parser.evalNode("/configuration")拿到了<configuration>節點信息之後,我們還需要給它解析下面的所有子節點.parse()方法裏面通過parseConfiguration()方法解析configuration節點的每個子節點。接下來我們繼續在XMLConfigBuilder類中找到parseConfiguration()方法的實現
private void parseConfiguration(XNode root) {
try {
//解析properties節點
propertiesElement(root.evalNode("properties"));
//解析settings節點
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
//解析typeAliases節點
typeAliasesElement(root.evalNode("typeAliases"));
//解析plugins節點
pluginElement(root.evalNode("plugins"));
//解析objectFactory節點
objectFactoryElement(root.evalNode("objectFactory"));
//解析objectWrapperFactory節點
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//解析reflectorFactory節點
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
//解析environments節點
environmentsElement(root.evalNode("environments"));
//解析databaseIdProvider節點
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//解析typeHandlers節點
typeHandlerElement(root.evalNode("typeHandlers"));
//解析mappers
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
上面看到configuration的子節點有十多種,每一種節點的特點和作用都不一樣,但節點的讀取統一都是evalNode()方法,不過拿到了它們的XNode節點之後,每個都是要通過不同的方式把它加到configuration裏面去的。這裏我們就只拿我們最關心的mapper節點去作介紹,Mybatis是怎麼將Mapper讀取到配置的,下面可以看mapperElement()方法的實現
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.");
}
}
}
}
}
parent參數對應的是的<mappers>節點(如下圖),由於mappers的是一個集合,所以上面的源碼對它的子節點進行了遍歷, maper節點有個name屬性,如果name等於"package"值時,它是一個包(就是把多個配置包在一起),其他不等於"package"的對應的都是一個單獨的Mapper。
如果節點的name屬性是package的話,那麼通過configuration.addMappers()方法直接加到configuration的配置裏面(在addMappers()方法裏面還會做進一步的解析)。
如果非package節點,又分爲三種情況:
1.只存在resource的屬性的節點:它是一個資源文件的地址,需要將它轉爲inputStream類型,再通過XMLMapperBuilder去解析,並且將解析後的結果存到configuration的裏面。
2.只存在url屬性的節點:和第1種很像,只是resource是項目內的路徑,url是項目外部的路徑,其實作用一樣。
3.只存在class屬性的節點:這種使用的是Java配置,採用註解的形式,它不需要將配置解析爲XNode對象,而是拿到它的接口信息,再通過configuration.addMapper()方法直接加到configuration裏面。
下面是XMLMapperBuilder的處理細節,由於代碼太長,這裏就不一一細說
public class XMLMapperBuilder extends BaseBuilder {
//Xml解析器
private final XPathParser parser;
//Mapper輔助建造器
private final MapperBuilderAssistant builderAssistant;
private final Map<String, XNode> sqlFragments;
//資源路徑
private final String resource;
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
configuration, resource, sqlFragments);
}
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
super(configuration);
this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
this.parser = parser;
this.sqlFragments = sqlFragments;
this.resource = resource;
}
private void configurationElement(XNode context) {
try {
//解析namespace
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
//解析select|insert|update|delete節點
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);
}
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
}
mappers裏面的mapper節點對應的是一個mapper.xml文件,XMLMapperBuilder是專門對它進行解析的,XMLMapperBuilder通過很多步驟之後,拿到context節點,它也是mapper.xml文件的根節點mapper,然後把它傳給configurationElement()方法去處理,從中拿到"select、insert、update、delete"四種節點(如下圖)再傳給buildStatementFromContext()方法,最後通過XMLStatementBuilder類的parseStatementNode()方法將它們轉成MappedStatement並添加到configuration的mappedStatements緩存
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
String parameterType = context.getStringAttribute("parameterType");
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
。。。。。。
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
上面是parseStatementNode()方法的實現(省略了大部分代碼),它從context節點裏面拿到id,sql腳本等信息,然後再調用builderAssistant.addMappedStatement()方法。(Mybatis的builder的類有點多, BuilderAssistant也是繼承了BaseBuilder類的,內部有configuration的成員變量,這裏最終會調用configuration的addMappedStatement()方法)
結語
好吧,Mybatis的初始化過程已經講完了,它的作用其實就是爲了初始化configuration,然後把它放到SqlSessionFactory裏面,雖然初始過程的代碼很複雜,但是Mybatis的總體的結構還是很清晰的。
這裏只講了第最開始部分,下一篇繼續介紹SqlSession管理。
謝謝能夠看到這裏的各位小夥伴,本人能力有限,如果有什麼說錯的地方請指正。