MyBatis源碼解析(一)——MyBatis初始化過程解
1. 準備工作
為了看清楚MyBatis的整個初始化過程,先建立一個簡單的Java項目,目錄結構如下圖所示:
1.1 Product 産品實體類
public class Product {
private long id;
private String productName;
private String productContent;
private String price;
private int sort;
private int falseSales;
private long category_id;
private byte type;
private byte state;
// PS:省略setter、getter函數
}
1.2 ProductMapper 産品持久化接口
public interface ProductMapper {
/**
* 查詢所有的産品
* @return
*/
List<Product> selectProductList();
}
1.3 ProductMapper.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="team.njupt.mapper.ProductMapper">
<select id="selectProductList" resultType="team.njupt.entity.Product">
select * from product
</select>
</mapper>
1.4 db.properties 資料庫配置檔案
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/waimai?useUnicode=true&characterEncoding=utf8
username=root
password=xxxxxx
1.5 mybatis.xml 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>
<properties resource="db.properties">
<!--<property name="username" value="dev_user"/>-->
<!--<property name="password" value="F2Fa3!33TYyg"/>-->
</properties>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="team/njupt/mapper/ProductMapper.xml"/>
</mappers>
</configuration>
1.6 Main 主函數
public class Main {
public static void main(String[] args) throws IOException {
String resource = "mybatis.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class);
List<Product> productList = productMapper.selectProductList();
for (Product product : productList) {
System.out.printf(product.toString());
}
} finally {
sqlSession.close();
}
}
}
2. MyBatis初始化過程
2.1 擷取配置檔案
當系統初始化時,首先會讀取配置檔案,并将其解析成InputStream
String resource = "mybatis.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
2.2 建立SqlSessionFactoryBuilder對象
從
SqlSessionFactoryBuilder
的名字中可以看出,
SqlSessionFactoryBuilder
是用來建立
SqlSessionFactory
對象的。
來看一下SqlSessionFactoryBuilder源碼:
SqlSessionFactoryBuilder中隻有一些重載的build函數,這些build函數的入參都是MyBatis配置檔案的輸入流,傳回值都是SqlSessionFactory;由此可見,SqlSessionFactoryBuilder的作用很純粹,就是用來通過配置檔案建立SqlSessionFactory對象的。
2.3 SqlSessionFactory建立過程
下面具體來看一下,build函數是如何建立SqlSessionFactory對象的。
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.
}
}
}
2.3.1 構造XMLConfigBuilder對象
build函數首先會構造一個
XMLConfigBuilder
對象,從名字上大緻可以猜到,該對象是用來解析XML配置檔案的。下面來看一下
XMLConfigBuilder
的體系結構。
-
是用來解析XML配置檔案的,不同類型XMLxxxBuilder
用來解析MyBatis配置檔案的不同部位。比如:XMLxxxBuilder
用來解析MyBatis的配置檔案,XMLConfigBuilder
用來解析MyBatis中的映射檔案(如上文提到的XMLMapperBuilder
),ProductMapper.xml
用來解析映射檔案中的SQL語句。XMLStatementBuilder
- 這些
都有一個共同的父類——XMLxxxBuilder
。這個父類維護了一個全局的BaseBuilder
對象,MyBatis的配置檔案解析後就以Configuration
對象的形式存儲。Configuration
- 當建立
對象時,就會初始化XMLConfigBuilder
對象,并且在初始化Configuration
對象的時候,一些别名會被注冊到Configuration
的Configuration
容器中。typeAliasRegistry
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; }
public Configuration() { typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class); typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class); typeAliasRegistry.registerAlias("FIFO", FifoCache.class); typeAliasRegistry.registerAlias("LRU", LruCache.class); typeAliasRegistry.registerAlias("SOFT", SoftCache.class); typeAliasRegistry.registerAlias("WEAK", WeakCache.class); …… }
2.3.2 解析配置檔案
當有了
XMLConfigBuilder
對象之後,接下來就可以用它來解析配置檔案了。
private void parseConfiguration(XNode root) {
try {
// 解析<properties>節點
propertiesElement(root.evalNode("properties"));
// 解析<settings>節點
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
// 解析<typeAliases>節點
typeAliasesElement(root.evalNode("typeAliases"));
// 解析<plugins>節點
pluginElement(root.evalNode("plugins"));
// 解析<objectFactory>節點
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析<reflectorFactory>節點
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// 解析<environments>節點
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析<mappers>節點
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
從上述代碼中可以看到,
XMLConfigBuilder
會依次解析配置檔案中的
<properties>
、
< settings >
、
< environments>
、
< typeAliases >
、
< plugins >
、
< mappers >
等屬性。下面介紹下幾個重要屬性的解析過程。
2.3.2.1 <properties>節點的解析過程
- <properties>節點的定義如下:
<properties resource="org/mybatis/example/config.properties"> <property name="username" value="dev_user"/> <property name="password" value="F2Fa3!33TYyg"/> </properties>
- <properties>節點的解析過程:
/** * @Param context <properties>節點 */ private void propertiesElement(XNode context) throws Exception { if (context != null) { // 擷取<properties>節點的所有子節點 Properties defaults = context.getChildrenAsProperties(); // 擷取<properties>節點上的resource屬性 String resource = context.getStringAttribute("resource"); // 擷取<properties>節點上的url屬性 String url = context.getStringAttribute("url"); // resource和url不能同時存在 if (resource != null && url != null) { throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other."); } if (resource != null) { // 擷取resource屬性值對應的properties檔案中的鍵值對,并添加至defaults容器中 defaults.putAll(Resources.getResourceAsProperties(resource)); } else if (url != null) { // 擷取url屬性值對應的properties檔案中的鍵值對,并添加至defaults容器中 defaults.putAll(Resources.getUrlAsProperties(url)); } // 擷取configuration中原本的屬性,并添加至defaults容器中 Properties vars = configuration.getVariables(); if (vars != null) { defaults.putAll(vars); } parser.setVariables(defaults); // 将defaults容器添加至configuration中 configuration.setVariables(defaults); } }
- 首先讀取
節點下的所有<resources>
節點,并将每個節點的<resource>
和name
屬性存入value
中。Properties
- 然後讀取
節點上的<resources>
、resource
屬性,并擷取指定配置檔案中的url
和name
,也存入value
中。(PS:由此可知,如果resource節點上定義的屬性和properties檔案中的屬性重名,那麼properties檔案中的屬性值會覆寫resource節點上定義的屬性值。)Properties
- 最終,攜帶所有屬性的
對象會被存儲在Properties
對象中。Configuration
- 首先讀取
2.3.2.2 <settings>節點的解析過程
- <settings>節點的定義如下:
<settings> <setting name="cacheEnabled" value="true"/> <setting name="lazyLoadingEnabled" value="true"/> <setting name="multipleResultSetsEnabled" value="true"/> </settings>
- <settings>節點的解析過程:
屬性的解析過程和<settings>
屬性的解析過程極為類似,這裡不再贅述。最終,所有的setting屬性都被存儲在<properties>
對象中。Configuration
2.3.2.3 <typeAliases>屬性的解析過程
<typeAliases>
屬性的定義方式有如下兩種:
- 方式1:
<typeAliases> <typeAlias alias="Author" type="domain.blog.Author"/> <typeAlias alias="Blog" type="domain.blog.Blog"/> </typeAliases>
- 方式2:
采用這種方式時,MyBatis會為指定包下的所有類起一個别名,該别名為首字母小寫的類名。<typeAliases> <package name="domain.blog"/> </typeAliases>
<typeAliases>
節點的解析過程如下:
private void typeAliasesElement(XNode parent) {
if (parent != null) {
// 周遊<typeAliases>下的所有子節點
for (XNode child : parent.getChildren()) {
// 若目前結點為<package>
if ("package".equals(child.getName())) {
// 擷取<package>上的name屬性(包名)
String typeAliasPackage = child.getStringAttribute("name");
// 為該包下的所有類起個别名,并注冊進configuration的typeAliasRegistry中
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
}
// 如果目前結點為< typeAlias >
else {
// 擷取alias和type屬性
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
// 注冊進configuration的typeAliasRegistry中
try {
Class<?> clazz = Resources.classForName(type);
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
- 如果
節點下定義了<typeAliases>
節點,那麼MyBatis會給該包下的所有類起一個别名(以類名首字母小寫作為别名)<package>
- 如果
節點下定義了<typeAliases>
節點,那麼MyBatis就會給指定的類起指定的别名。<typeAlias>
- 這些别名都會被存入
的configuration
容器中。typeAliasRegistry
2.3.2.4 <mappers>節點的解析過程
<mappers>
節點的定義方式有如下四種:
- 方式1:
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
- 方式2:
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
</mappers>
- 方式3:
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
</mappers>
- 方式4:
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
</mappers>
<mappers>
節點的解析過程如下:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
// 周遊<mappers>下所有子節點
for (XNode child : parent.getChildren()) {
// 如果目前節點為<package>
if ("package".equals(child.getName())) {
// 擷取<package>的name屬性(該屬性值為mapper class所在的包名)
String mapperPackage = child.getStringAttribute("name");
// 将該包下的所有Mapper Class注冊到configuration的mapperRegistry容器中
configuration.addMappers(mapperPackage);
}
// 如果目前節點為<mapper>
else {
// 依次擷取resource、url、class屬性
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
// 解析resource屬性(Mapper.xml檔案的路徑)
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
// 将Mapper.xml檔案解析成輸入流
InputStream inputStream = Resources.getResourceAsStream(resource);
// 使用XMLMapperBuilder解析Mapper.xml,并将Mapper Class注冊進configuration對象的mapperRegistry容器中
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
// 解析url屬性(Mapper.xml檔案的路徑)
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();
}
// 解析class屬性(Mapper Class的全限定名)
else if (resource == null && url == null && mapperClass != null) {
// 将Mapper Class的權限定名轉化成Class對象
Class<?> mapperInterface = Resources.classForName(mapperClass);
// 注冊進configuration對象的mapperRegistry容器中
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
- MyBatis會周遊
下所有的子節點,如果目前周遊到的節點是<mappers>
,則MyBatis會将該包下的所有Mapper Class注冊到<package>
的configuration
容器中。mapperRegistry
- 如果目前節點為
,則會依次擷取resource、url、class屬性,解析映射檔案,并将映射檔案對應的Mapper Class注冊到<mapper>
的configuration
容器中。mapperRegistry
其中,
<mapper>
節點的解析過程如下:
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
- 在解析前,首先需要建立
,建立過程如下:XMLMapperBuilder
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) { // 将configuration賦給BaseBuilder super(configuration); // 建立MapperBuilderAssistant對象(該對象為MapperBuilder的協助者) this.builderAssistant = new MapperBuilderAssistant(configuration, resource); this.parser = parser; this.sqlFragments = sqlFragments; this.resource = resource; }
- 首先會初始化父類
,并将BaseBuilder
賦給BaseBuilder;configuration
- 然後建立
對象,該對象為MapperBuilderAssistant
的協助者,用來協助XMLMapperBuilder
完成一些解析映射檔案的動作。XMLMapperBuilder
- 首先會初始化父類
- 當有了
後,便可進入解析XMLMapperBuilder
的過程:<mapper>
public void parse() { // 若目前的Mapper.xml尚未被解析,則開始解析 // PS:若<mappers>節點下有相同的<mapper>節點,那麼就無需再次解析了 if (!configuration.isResourceLoaded(resource)) { // 解析<mapper>節點 configurationElement(parser.evalNode("/mapper")); // 将該Mapper.xml添加至configuration的LoadedResource容器中,下回無需再解析 configuration.addLoadedResource(resource); // 将該Mapper.xml對應的Mapper Class注冊進configuration的mapperRegistry容器中 bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }
-
函數configurationElement
private void configurationElement(XNode context) { try { // 擷取<mapper>節點上的namespace屬性,該屬性必須存在,表示目前映射檔案對應的Mapper Class是誰 String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } // 将namespace屬性值賦給builderAssistant builderAssistant.setCurrentNamespace(namespace); // 解析<cache-ref>節點 cacheRefElement(context.evalNode("cache-ref")); // 解析<cache>節點 cacheElement(context.evalNode("cache")); // 解析<parameterMap>節點 parameterMapElement(context.evalNodes("/mapper/parameterMap")); // 解析<resultMap>節點 resultMapElements(context.evalNodes("/mapper/resultMap")); // 解析<sql>節點 sqlElement(context.evalNodes("/mapper/sql")); // 解析sql語句 buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e); } }
-
resultMapElements
函數
該函數用于解析映射檔案中所有的
節點,這些節點會被解析成<resultMap>
對象,存儲在ResultMap
對象的Configuration
容器中。resultMaps
-
節點定義如下:<resultMap>
<resultMap id="userResultMap" type="User"> <constructor> <idArg column="id" javaType="int"/> <arg column="username" javaType="String"/> </constructor> <result property="username" column="user_name"/> <result property="password" column="hashed_password"/> </resultMap>
-
節點的解析過程:<resultMap>
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception { ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier()); // 擷取<ResultMap>上的id屬性 String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier()); // 擷取<ResultMap>上的type屬性(即resultMap的傳回值類型) String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType", resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType")))); // 擷取extends屬性 String extend = resultMapNode.getStringAttribute("extends"); // 擷取autoMapping屬性 Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping"); // 将resultMap的傳回值類型轉換成Class對象 Class<?> typeClass = resolveClass(type); Discriminator discriminator = null; // resultMappings用于存儲<resultMap>下所有的子節點 List<ResultMapping> resultMappings = new ArrayList<ResultMapping>(); resultMappings.addAll(additionalResultMappings); // 擷取并周遊<resultMap>下所有的子節點 List<XNode> resultChildren = resultMapNode.getChildren(); for (XNode resultChild : resultChildren) { // 若目前節點為<constructor>,則将它的子節點們添加到resultMappings中去 if ("constructor".equals(resultChild.getName())) { processConstructorElement(resultChild, typeClass, resultMappings); } // 若目前節點為<discriminator>,則進行條件判斷,并将命中的子節點添加到resultMappings中去 else if ("discriminator".equals(resultChild.getName())) { discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings); } // 若目前節點為<result>、<association>、<collection>,則将其添加到resultMappings中去 else { // PS:flags僅用于區分目前節點是否是<id>或<idArg>,因為這兩個節點的屬性名為name,而其他節點的屬性名為property List<ResultFlag> flags = new ArrayList<ResultFlag>(); if ("id".equals(resultChild.getName())) { flags.add(ResultFlag.ID); } resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags)); } } // ResultMapResolver的作用是生成ResultMap對象,并将其加入到Configuration對象的resultMaps容器中(具體過程見下) ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping); try { return resultMapResolver.resolve(); } catch (IncompleteElementException e) { configuration.addIncompleteResultMap(resultMapResolver); throw e; } }
這個類很純粹,有且僅有一個函數ResultMapResolver
,用于構造resolve
對象,并将其存入Configuration對象的resultMaps容器中;而這個過程是借助于ResultMap
完成的。MapperBuilderAssistant.addResultMap
public ResultMap resolve() { return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping); }
-
-
sqlElement
函數
該函數用于解析映射檔案中所有的
節點,并将這些節點存儲在目前映射檔案所對應的XMLMapperBuilder對象的sqlFragments容器中,供解析sql語句時使用。<sql>
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
-
buildStatementFromContext
函數
該函數會将映射檔案中的sql語句解析成
對象,并存在MappedStatement
的configuration
。mappedStatements
2.3.3 建立SqlSessionFactory對象
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.
}
}
}
回過頭來再看一下
SqlSessionFactory
的
build
函數,剛才說了半天,介紹了
XMLConfigBuilder
解析映射檔案的過程,解析完成之後
parser.parse()
函數會傳回一個包含了映射檔案解析結果的
configuration
對象,緊接着,這個對象将作為參數傳遞給另一個build函數,如下:
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
這個函數将
configuration
作為參數,建立了
DefaultSqlSessionFactory
對象。
DefaultSqlSessionFactory
是接口
SqlSessionFactory
的一個實作類,
SqlSessionFactory
的體系結構如下圖所示:
此時,
SqlSessionFactory
建立完畢!