随着上一篇文章中 configuration.setEnvironment(environmentBuilder.build()) 语句被执行
数据源已经锁定,环境已经确定
接下来就是如何获取 Sql 语句
还记得上一篇文章遗留下的一个问题
mapperElement(root.evalNode("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总共有四种方法进行配置,在 MyBatis 官方文档中也有描述这四种映射器的使用
resource:配置所有xml文件在resource包下的相对路径
url:配置所有xml文件在系统盘中的绝对路径
class:所有mapper 接口的包名 + 类名
package:接口的包名,一般只需要配置一个
回到代码中,我们可以看到方法的主体是遍历 mappers 中的 mapper 节点,然后逐一解析
解析 mapper
我们可以看出,解析 package 和 class 节点的流程是一样的,都是通过类名去创建 class 类,再通过类名绑定 xml 文件
而 resource 和 url 的解析方式是一样的,都是通过输入流创建 XMLMapperBuilder 类并执行 parse 方法
他们的区别就是 package 和 class 加载的是 mapper 接口,而 resource 和 url 加载的是 resource 包下的 xml 文件
package(class) 是如何解析的
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
如上所示,先调用 getStringAttribute 方法获得包名
再使用 addMappers 方法解析包名
addMappers(mapperPackage)
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
public void addMappers(String packageName, Class> superType) {
ResolverUtil> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);// 通过 resolverUtil 解析包名,并将包下的所有类存放进 Set 中
Set>> mapperSet = resolverUtil.getClasses();// 遍历 Set,对每个mapper的class类进行解析for (Class> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}public void addMapper(Class type) {if (type.isInterface()) {if (hasMapper(type)) {// 判断是否已经有了该 mapper 的类型throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}boolean loadCompleted = false;try {// 将该mapper的类型移入已知的范围
knownMappers.put(type, new MapperProxyFactory<>(type));// It's important that the type is added before the parser is run// otherwise the binding may automatically be attempted by the// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);// 最终调用的是parse()方法
parser.parse();
loadCompleted = true;
} finally {// 如果try中的代码块执行出错,则将该mapper的类型移除已知的范围if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
在几个 addMapper 方法的调用中,核心是将 package 下的所有 mapper 接口解析成 class 类,并交由 MapperAnnotationBuilder 的 parse 方法去解析
parse()
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
// 如果对应的xml文件还未被加载,则加载对应xml文件
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
// 获取mapper下的每个方法,并进行遍历
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// 这里是对mapper上的注解进行解析,因为我们没有注解,因此跳过
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
// 最终配置文件加载了一个默认的方法解析器
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
这个方法的作用是加载 xml 文件,并解析接口上的注解。
因为我们没有注解,因此 loadXmlResource() 方法是这个类中最重要的方法,它的底层通过输入流执行了 xmlParser.parse() 方法,对xml 文件进行详细解析,这个方法在 resource 中也会被调用,我们接下来就来看 resource 方式是如何解析 xml 文件的。
resource(url)的解析方式
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
通过将 resource 的值,即xml文件转化为输入流,调用 mapperParser.parse() 进行解析。
这个 mapperParser.parse() 与上文提及的 xmlParser.parse() 其实是是同一个方法
mapperParser.parse()
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
这个方法的主体是 configurationElement(parser.evalNode("/mapper")),在 evalNode 方法加载了 mapper 节点后,进行解析。后面的几个方法是对处理的结果进行再次解析。
configurationElement(parser.evalNode("/mapper"))
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. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
这个方法执行了缓存的解析、parameterMap 的解析、resultMap 的解析等,当然,我们要把重点放到这个方法的第13行:buildStatementFromContext(context.evalNodes("select|insert|update|delete")) 方法,这个方法是解析 Sql 语句的核心
buildStatementFromContext
这个方法过长,但这个方法的根本目的就是将 xml 文件解析成数据库能够读取的 Sql 文件
public void parseStatementNode() {
// 获取 id 和 databaseId 属性
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 获取节点名称,比如节点就会被解析为"select"
String nodeName = context.getNode().getNodeName();
// 根据节点名称解析 SqlCommandType
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// 解析节点
XMLIncludeTransformer Parser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
Class> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// 解析节点
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 解
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;
}
// 解析Sql语句的方法
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
// 以下是解析各种节点中对应的属性
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
这段代码很长,长得我一开始不想贴上这段代码。但如果耐心仔细看一下这段代码,发现里面其实很大篇幅是在获取节点的属性并解析。如果忽视这部分代码,其实这段代码只是干了下面这些事:
解析节点
解析节点
解析 Sql 语句,获取 SqlSource
构建 MappedStatement 实例
经历这四个步骤之后,Sql 语句就被解析并存储在 MappedStatement 当中,因为篇幅原因,这四个步骤我们在下一篇文章再进行学习。