本專輯將介紹mybatis的原理和源碼分析。
1、概述
在不使用spring的情況下,我們從官網上可以看到mybatis的使用方法:
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
Blog blog = session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
} finally {
session.close();
}
我們可以看到,mybatis啟動,首先要加載自己的配置文件,通過配置文件創建SqlSessionFactory,然后再使用SqlSessionFactory對象創建SqlSession對象,通過SqlSession對象來操作數據庫。本篇先介紹mybatis加載配置文件的過程。
2、SqlSessionFactoryBuilder
首先我們看一下上面例子中SqlSessionFactoryBuilder的build()方法,我們找到這個方法的最終實現:
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.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
我們可以看到默認SqlSessionFactoryBuilder創建SqlSessionFactory,是直接實例化了一個DefaultSqlSessionFactory對象,在創建DefaultSqlSessionFactory對象的時候需要一個Configuration對象作為構造方法的參數,我們也可以看到Configuration對象是由XMLConfigBuilder讀取mybatis-config.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;
}
private void parseConfiguration(XNode root) {
try {
// 第一步,加載properties節點,把配置文件中定義配置變量解析出來
propertiesElement(root.evalNode("properties"));
// 第二步,加載setting節點,
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
// 第三步,加載類的別名
typeAliasesElement(root.evalNode("typeAliases"));
// 第四步,加載插件
pluginElement(root.evalNode("plugins"));
// 第五步,加載objectFactory節點
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 第八步,將前面settings節點解析出來的配置設置到Configuration中
settingsElement(settings);
// 第九步,加載environments節點,
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);
}
}
我們可以看到,mybatis加載配置文件的時候,以configuration節點作為根節點開始解析,首先,加載properties節點,將properties下定義的變量加載到Configuration的variables屬性,具體實現比較簡單,這里不看源碼,有興趣的朋友可以自己看一下。
第二步,解析settings節點,把配置解析成Properties對象,在mybatis運行的過程中,可以通過修改這些屬性來改變mybatis 的行為,具體有哪些配置可以參考Mybatis的文檔。這里對vfsImpl參數有個特殊處理,可以指定VFS的實現。
第三步,加載類的別名信息,注冊到Configuration的typeAliasRegistry對象中,這個typeAliasRegistry對象中也包含了許多默認的類的別名,如:registerAlias("string", String.class)。注冊有兩種方式,一種是在<typeAliases></typeAliases>中添加<typeAlias alias="" type=""/>節點,另一種是通過添加<package name="domain.blog"/>直接指定一個包,掃描包中的@Alias("")注解來尋找別名。這些都可以從源碼中體現,具體實現源碼不再貼出。
第四步,加載mybatis的插件(Interceptor),這里把定義的插件信息讀出來,通過反射創建實例,然后注冊到Configuration的interceptorChain中。
第五步,加載自定義的ObjectFactory,ObjectFactory是mybatis查詢出結果后,創建查詢結果使用的對象工廠,默認是直接使用目標類的構造方法進行創建(具體實現可以看一下DefaultObjectFactory這個類),這里用戶可以自定義實現。這里把用戶自定義的ObjectFactory實現類注冊到Configuration的objectFactory屬性。
第六步和第七步,分別是加載objectWrapperFactory和reflectorFactory節點,具體過程和加載ObjectFactory節點類似,而且這兩個節點在官方文檔中沒有提及,所以我們大可不必看這兩個過程。
第八步,將前面settings節點解析出來的配置設置到Configuration中。
第九步,加載environments節點,environments節點下配置了不同環境下的不同的environment節點,environment節點主要配置了transactionManager和dataSource信息。我們可以看一下具體實現:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
我們可以看到,在加載environment的時候,只會去讀取default屬性指定的environment節點。environment節點下必須要有transactionManager和dataSource節點,不然讀取的時候會拋出異常。讀取完這些信息之后,會創建Environment對象,并將該對象設置到Configuration的environment屬性。
第十步,加載databaseIdProvider節點,databaseIdProvider的具體作用可以看一下mybatis官方文檔,我們可以看一下具體實現:
private void databaseIdProviderElement(XNode context) throws Exception {
DatabaseIdProvider databaseIdProvider = null;
if (context != null) {
String type = context.getStringAttribute("type");
// awful patch to keep backward compatibility
if ("VENDOR".equals(type)) {
type = "DB_VENDOR";
}
Properties properties = context.getChildrenAsProperties();
databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance();
databaseIdProvider.setProperties(properties);
}
Environment environment = configuration.getEnvironment();
if (environment != null && databaseIdProvider != null) {
String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
configuration.setDatabaseId(databaseId);
}
}
如果定義了databaseIdProvider,mybatis會根據上面environment定義的datasource來選擇會用到的databaseId,并設置到configuration的databaseId屬性,以供后面加載statements使用。
第十一步,加載typeHandlers,typeHandlers的功能可以看一下mybatis的官方文檔。其作用為:在預處理語句(PreparedStatement)中設置一個參數時,或者從結果集中取出一個值時, 都會用類型處理器將獲取的值以合適的方式轉換成 Java 類型。解析的最終結果會被注冊到Configuration的typeHandlerRegistry中,mybatis定義了很多類型的默認實現,有興趣的可以看一下源碼。typeHandler也可以通過注解的方式定義,這里就不再多說了。
最后一步是加載mappers節點,我們先來看一下解析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.");
}
}
}
}
}
首先我們可以看到,在解析mappers子節點的時候,解析方法會被分成兩大類,一種是從xml文件中解析(url、source),一種是從Class解析(class、package)。從Class解析比較簡單,僅僅是將Class注冊到Configuration的mapperRegistry屬性中。而解析xml比較復雜,我們來看一下具體過程,其中url和resource通過xml方式定義mapper(也就是我們平時使用的XXXMapper.xml文件),解析xml定義的mapper是交給XMLMapperBuilder來完成的,我們看一下XMLMapperBuilder的parse()方法:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 加載mapper節點下的所有元素
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
第一步,加載mapper節點下的所有元素,我們看一下configurationElement()方法的具體實現:
private void configurationElement(XNode context) {
try {
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"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
首先獲取mapper的namespace屬性,然后將namespace暫存到builderAssistant中,因為在接下來的過程中會頻繁用到namespace屬性。
接下來解析cache-ref和cache屬性,這里很簡單,就是單純的解析節點的屬性而已,所以不再贅述,如果不知道cache-ref和cache節點定義的作用的讀者,建議去官方文檔了解一下這兩個標簽的作用。
然后是加載parameterMap節點,官方文檔已經將這個配置標記為廢棄,所以我們可以不用關注這個,我們看一下接下來的解析resultMap的過程。
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
String extend = resultMapNode.getStringAttribute("extends");
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
Class<?> typeClass = resolveClass(type);
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
resultMappings.addAll(additionalResultMappings);
List<XNode> resultChildren = resultMapNode.getChildren();
// 加載子節點
for (XNode resultChild : resultChildren) {
if ("constructor".equals(resultChild.getName())) {
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
List<ResultFlag> flags = new ArrayList<ResultFlag>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
我們可以看到,解析resultMap節點時,首先會解析出id屬性,然后再解析出type屬性(“ofType”、“resultType”、“javaType”),然后是解析extend屬性、autoMapping屬性。
接下來就是解析resultMap下的子節點,加載子節點的時候對constructor和discriminator會做特殊處理。constructor和其他的節點一樣,都會被解析成一個ResultMapping對象,并加到一個list中。我們可以看一下ResultMapping又那些參數:
private Configuration configuration;
private String property;
private String column;
private Class<?> javaType;
private JdbcType jdbcType;
private TypeHandler<?> typeHandler;
private String nestedResultMapId;
private String nestedQueryId;
private Set<String> notNullColumns;
private String columnPrefix;
private List<ResultFlag> flags;
// 一個<result>節點的column屬性中包含多個column的話會被加載成composites
private List<ResultMapping> composites;
private String resultSet;
private String foreignColumn;
private boolean lazy;
加載邏輯比較簡單,這里我們不在贅述,有興趣的同學可以看一下具體實現。
我們再看一下加載discriminator節點為Discriminator對象的邏輯:
private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
String column = context.getStringAttribute("column");
String javaType = context.getStringAttribute("javaType");
String jdbcType = context.getStringAttribute("jdbcType");
String typeHandler = context.getStringAttribute("typeHandler");
Class<?> javaTypeClass = resolveClass(javaType);
@SuppressWarnings("unchecked")
Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
Map<String, String> discriminatorMap = new HashMap<String, String>();
for (XNode caseChild : context.getChildren()) {
String value = caseChild.getStringAttribute("value");
String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings));
discriminatorMap.put(value, resultMap);
}
return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
}
這里也很簡單,分別讀取column、javaType、jdbcType、typeHandler屬性,然后讀取子節點case節點,把不同的value對應的resultMap解析成一個map,之后創建成一個Discriminator對象返回。
最后,系統會將resultMap節點解析出來的各種屬性封裝成一個ResultMap注冊到configuration中。到此,resultMap節點的解析我們就看完了。
我們接下里回到configurationElement()方法中來,在解析完resultMap節點后,接下里會解析sql節點,這里就是生成一個Map,key是sql的id,value是一個XNode節點的對象,解析過程比較簡單,我們就不單獨看了。
最后就是解析select、insert、update、delete這些節點了,我們可以看一下buildStatementFromContext()方法的實現,我們一層一層最終,最后可以發現,這些節點是交給XMLStatementBuilder的parseStatementNode()方法來解析的,我們先把源碼貼出:
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 此處省略部分代碼
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 此處省略部分代碼
// 解析節點的子節點
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 解析SelectKey節點
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 將節點內容解析成SqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// 此處省略部分代碼
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
解析這些節點屬性的方法都很簡單,大家可以看一下源碼具體實現,上面的源碼省略了這些過程。而解析這些標簽子節點的內容這塊比較復雜,我們一起來看一下。我們很容易發現,解析節點內容是通過XMLIncludeTransformer的applyIncludes()方法實現的,我們貼出源碼來分析:
public void applyIncludes(Node source) {
Properties variablesContext = new Properties();
Properties configurationVariables = configuration.getVariables();
if (configurationVariables != null) {
variablesContext.putAll(configurationVariables);
}
applyIncludes(source, variablesContext, false);
}
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
if (source.getNodeName().equals("include")) {
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
Properties toIncludeContext = getVariablesContext(source, variablesContext);
applyIncludes(toInclude, toIncludeContext, true);
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
source.getParentNode().replaceChild(toInclude, source);
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
toInclude.getParentNode().removeChild(toInclude);
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
applyIncludes(children.item(i), variablesContext, included);
}
} else if (included && source.getNodeType() == Node.TEXT_NODE
&& !variablesContext.isEmpty()) {
// replace variables ins all text nodes
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
我們可以看到,這是一個遞歸方法,當加載的節點是include節點,或者節點屬性是ELEMENT_NODE的時候,會遞歸執行,直到節點屬性是TEXT_NODE節點。這里我們需要了解一下java解析xml的一些知識,我們舉個例子:
有一個xml串:
<a>
head
<b></b>
tail
<a/>
在解析a標簽的子Node的時候,會解析出三個子Node,也就是說 head和tail都會被解析成一個Node,其類型為TEXT_NODE,而<b>節點會被解析成ELEMENT_NODE節點。
好了,我們再回到mybatis的解析過程中來。我們先看一下TEXT_NODE解析的過程,這個過程只有在included這個參數傳入true的時候才會被觸發。我們先來看看這個過程做了什么,這個過程是根據我們之前configuration節點下的properties節點解析出的變量,來替換源text文本中的變量(變量以$()這種方式表示),替換過程比較復雜,我們也沒必要細看,這里就不多說,我們知道只要知道在解析TEXT_NODE節點的時候會做一個變量替換的過程即可。
而解析ELEMENT_NODE的過程很簡單,就是遞歸調用而已,我們也不多說。
最后我們看一下解析include節點,這一步其實也比較簡單,就是把當前的include節點替換成include的目標節點。
我們再回到parseStatementNode()方法,在解析完子節點后,mybatis會處理SelectKey節點,具體方法是:processSelectKeyNodes(),處理過程可以概述為:將SelectKey節點解析成一個MappedStatement對象,然后再將MappedStatement對象封裝成SelectKeyGenerator對象,然后根據String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;這個規則生成一個id作為SelectKeyGenerator的id,注冊到configuration對象的keyGenerators屬性中,最后再把這個SelectKey節點移除,這里我們不貼出源碼了,有興趣的同學可以自己看一下。
接下來我們看到mybatis通過這種方式將節點內容解析成對象:
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
LanguageDriver有兩個實現類:RawLanguageDriver和XMLLanguageDriver。RawLanguageDriver的注釋中寫到不推薦使用RawLanguageDriver,除非確定要解析的sql是非動態sql,這里我們只看XMLLanguageDriver的實現就行了。我們一步一步追蹤,會發現,創建SqlSource 的過程是交給XMLScriptBuilder類的parseScriptNode()方法實現的,我們來看一下:
public SqlSource parseScriptNode() {
List<SqlNode> contents = parseDynamicTags(context);
MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
SqlSource sqlSource = null;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
List<SqlNode> parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<SqlNode>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlers(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return contents;
}
我們可以看到,mybatis會將標簽的子節點解析成一個SqlNode的List,如果子節點是TEXT_NODE和CDATA_SECTION_NODE,則直接封裝成一個TextSqlNode,如果是ELEMENT_NODE節點,說明是動態sql節點,這里使用NodeHandler來生成SqlNode對象。我們可以先來看一下SqlNode有多少子類:
這些子類正好對應我們使用mybatis動態sql的時候用的節點標簽。SqlNode的具體作用我們留到后面分析,我們暫且可以把SqlNode的作用理解為一個sql對象,這個對象可以根據輸入內容生成對應的sql語句。看到這里,我們可以猜測一下,在執行動態sql構建的時候,動態sql的構建是根據List<SqlNode>一步一步創建sql拼接而成的。其實我們從接下里的SqlSource構建過程也可以看出,所有的SqlNode節點會被封裝成一個MixedSqlNode節點,這個節點算是所有的SqlNode的一個外觀節點:
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
}
生成的MixedSqlNode對象會作為SqlSource的構造方法參數被傳入SqlSource,到這里,SqlSource的創建過程我們分析完了。我們繼續回到上文提到的parseStatementNode()方法,在解析完各種屬性,創建完SqlSource對象后,mybatis會根據解析完的參數生成一個MappedStatement對象添加到Configuration對象的mappedStatements屬性中,mappedStatements是一個Map,key是節點的id(id屬性會被替換成currentNamespace + "." + base)屬性,value就是MappedStatement對象。
從上面的分析,我們看到了mapper節點的解析過程,接下來我們繼續回到XMLMapperBuilder的parse()方法,解析完mapper節點之后,mybatis會根據mapper的namespace去尋找對應的Mapper接口,并把接口加載到Configuration的mapperRegistry屬性中。這些操作在bindMapperForNamespace()方法中有體現。做完這些動作,parse()方法會調用下面三個方法做一些后續動作,這里我們不再進行詳細分析。大該的作用就是將未注冊的ResultMap、CacheRefs、Statements注冊到Configuration中。
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
到這里Configuration的創建過程分析完了,大家是不是感到很亂?我把Configuration的幾個比較重要的屬性加了注釋,來方便大家理解:
public class Configuration {
/**
* 加載配置文件中的<environment></environment>節點產生的對象,
* 對象持有transactionFactory和dataSource
*/
protected Environment environment;
protected boolean safeRowBoundsEnabled;
protected boolean safeResultHandlerEnabled = true;
protected boolean mapUnderscoreToCamelCase;
protected boolean aggressiveLazyLoading;
protected boolean multipleResultSetsEnabled = true;
protected boolean useGeneratedKeys;
protected boolean useColumnLabel = true;
protected boolean cacheEnabled = true;
protected boolean callSettersOnNulls;
protected boolean useActualParamName = true;
protected boolean returnInstanceForEmptyRow;
protected String logPrefix;
protected Class <? extends Log> logImpl;
protected Class <? extends VFS> vfsImpl;
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));
protected Integer defaultStatementTimeout;
protected Integer defaultFetchSize;
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
/**
* <properties></properties>節點解析出的值
*/
protected Properties variables = new Properties();
protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
/**
* 解析的<objectFactory></objectFactory>節點生成的objectFactory對象,有默認值
*/
protected ObjectFactory objectFactory = new DefaultObjectFactory();
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
protected boolean lazyLoadingEnabled = false;
protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
/**
* 根據environment節點下配置的DataSource獲取的數據源id
*/
protected String databaseId;
/**
* Configuration factory class.
* Used to create Configuration for loading deserialized unread properties.
*
* @see <a >Issue 300 (google code)</a>
*/
protected Class<?> configurationFactory;
/**
* 通過類方式(或者掃描包)方式注冊的Mapper類接口和通過xml文件注冊的namespace對應的接口
*/
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
/**
* 解析的<plugins></plugins>節點注冊的插件
*/
protected final InterceptorChain interceptorChain = new InterceptorChain();
/**
* /mapper/parameterMap節點下的typeHandler屬性,默認會有一些通用的typeHandler
*/
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
/**
*<typeAliases></typeAliases>節點解析出來的別名信息
*/
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
/**
* 解析的insert、update、delete、select節點,id為 namespace + "." + id
*/
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
/**
* 解析的<resultMap></resultMap>節點,id為 namespace + "." + id
*/
protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
/**
* 解析的<parameterMap></parameterMap>節點,id為 namespace + "." + id
*/
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");
/**
* 解析的insert、update、delete、select節點中的<SelectKey></SelectKey>節點,
* key是父節點id(insert、update、delete、select這些節點的id)+"!selectKey"
*/
protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<KeyGenerator>("Key Generators collection");
/**
* 已經加載的資源,(xml文件和Class,xml文件添加namespace,Class添加類名)
*/
protected final Set<String> loadedResources = new HashSet<String>();
protected final Map<String, XNode> sqlFragments = new StrictMap<XNode>("XML fragments parsed from previous mappers");
protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<XMLStatementBuilder>();
protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<CacheRefResolver>();
protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<ResultMapResolver>();
protected final Collection<MethodResolver> incompleteMethods = new LinkedList<MethodResolver>();
/*
* A map holds cache-ref relationship. The key is the namespace that
* references a cache bound to another namespace and the value is the
* namespace which the actual cache is bound to.
*/
protected final Map<String, String> cacheRefMap = new HashMap<String, String>();
}
SqlSessionFactoryBuilder創建SqlSessionFactory的過程其實最主要是解析配置并生成Configuration對象的過程,創建完Configuration對象之后,SqlSessionFactoryBuilder直接調用DefaultSqlSessionFactory的構造方法來創建SqlSessionFactory對象。
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
到此我們對Mybatis啟動過程中加載配置文件有了一定的了解,后面我們會繼續分析mybatis的源碼。