第二篇分析了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()是如何执行的呢?
先来温习一下代理模式