第二篇分析了Mybatis是如何把SQL和參數根據TyperHandler一一設值,並且如何實例化PrepareStatement對象的,和獲得數據庫連接的。
一開始我看源碼的時候我在思考一個問題,Mybatis是在什麼時候?如何拿到這些關鍵的信息的?主要就是第一篇中的至關重要的MapperStatement對象的。
在前兩篇的文章中,是直接在SqlSession.insert()的時候直接debug進去的,那個時候就有了MapperStatement對象了。那麼MapperStatement肯定在這段代碼執行之前。那麼就是這幾行代碼中。
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);
SqlSession sqlSession = factory.openSession();
debug進入builder.build,你要是問我怎麼就知道進這裏debug,那只有是看方法名猜的,就是這樣。
XMLConfigBuilder一看這個名字就猜得到是解析Mybatis的配置文件的,接着往下走。進入paresr.pares()方法;
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
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);
}
}
這些名字是不是特別熟悉,就是第一篇裏的mybatis-config.xml裏的標籤,可以知道,Mybatis這裏解析各個標籤。這裏重點關注mapperElement這個方法,這裏就是解析我們配置的mapper的方法。進入
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.");
}
}
}
}
}
可以看到,這裏Mybatis循環解析<Mappers>標籤下的<Mapper>標籤裏的xml文件,如果你是批量配置,則直接配置package,這裏先不說這種方式。這裏第一篇裏的mybatis-config.xml文件中<mapper>標籤配置的是resource元素,所以這裏會判斷進入對應的resource解析塊中,
接下來重點關注mapperParser.parse()方法。裏面這三行代碼是關鍵的,一個一個來看。
第一、configrationElement()這個方法。
拿到當前mapper.xml(UserMapper.xml)的namespace。接下來會執行buildStatementFromContext(context.evalNodes)這個方法,
意思是構建Statement從當前的這個XNode中,感覺這就是本篇的重點,終於找到了,MapperStatement對象應該就是這裏實例化的。裏面的context.evalNodes是解析這個當前的UserMapper.xml這個文件中有幾個SQL標籤。
源碼如下:
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
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);
}
}
}
這裏Mybatis拿到了當前UserMapper.xml文件裏所有的、最原始的我們寫的SQL語句,通過for(XNode: context:list)循環並一個個解析每個sql標籤。這裏拿第一個insert語句進行舉例。進入statementParser.parseStatementNode()。進入方法之前,可以根據方方法名字可知,這裏每一個SQL標籤節點對應一個StatementNode,在最原始裏的傳統的JDBC中就是一個SQL,一個Statement,所以很好理解。debug進入。
這裏清楚的看到,Mybatis解析從當前SQL標籤中解析出insert,並將sqlCommandType設置爲Insert,這裏的id就是UserMapper.xml中的<insert>標籤的ID,接下來
可以看到,這裏將原始的SQL語句轉換成values是問號(?)的形式,相當於把insert into user(id,name,age) values(#{id},#{name},#{age})轉換爲insert into user(id,name,age) values(?,?,?);這裏是否想到傳統的JDBC的我們書寫的時候樣子?
然後實例化爲sqlSource對象,第二篇曾經留下疑問,這個SQLSource就是在這裏實例化的
並且這裏Mybatis默認設置的statementTyper就是prepared類型的。接下來
在這個方法裏進行MapperStatement的初始化。終於找到它了。源碼裏可以看到,實例化MapperStatement通過statementBuilder.build()方法。這樣一個MapperStatement就實例化好了,接下里就是把這個對象放到configration中,這樣我們在第一篇裏說的獲取至關重要的mapperStaement對象就可以拿到了。
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
//在這裏將實例化好的statement對象放到configuration中,便於我們第二種方式時候使用
configuration.addMappedStatement(statement);
return statement;
}
第二、bindMapperForNamespace,這個方法比較簡單,就是把當前maper的完整路徑,放到configuration中,便於我們第一篇裏說的Mybatis的第一種方式執行時候調用。
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
}
這樣最後,build(paresr.parse())裏面的方法執行完畢之後,實例化SqlSessionfactoty對象就可以在下一步調用openSession()獲得sqlSession對象進行前兩篇中的操作了。
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
到此爲止,基本上Mybatsi上基本流程,從頭到尾,都差不多用肉眼看到了。那麼Mybatis另外方式SqlSession.getMapper()是如何執行的呢?
先來溫習一下代理模式