前言
在前两篇文章中,简单了说了下Mybatis全局配置文件解析加载流程和和全局配置文件的部分标签解析,但是还并没有涉及到核心的解析。所以本篇文章将开始核心解析的部分,那就是mappers标签的解析。可能有人很奇怪这为什么不在前两篇文章中拿出来说,因为mapper标签的解析在configuration标签同层标签解析中是一个比较特殊的部分,因此需要单独拿出来进行说明。
好吧,说了这么多废话,还是进入正题吧!
一、mapper标签解析
由于前两篇文章已经贴了mapper标签解析的入口代码了,这里就不在贴代码了,那我们来看看入口mapperElement方法中是怎么实现解析的:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
// 获取<mappers>标签的子标签
for (XNode child : parent.getChildren()) {
// <package>子标签
if ("package".equals(child.getName())) {
// 获取mapper接口和mapper映射文件对应的package包名
String mapperPackage = child.getStringAttribute("name");
// 将包下所有的mapper接口以及它的代理对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
configuration.addMappers(mapperPackage);
} else {// <mapper>子标签
// 获取<mapper>子标签的resource属性
String resource = child.getStringAttribute("resource");
// 获取<mapper>子标签的url属性
String url = child.getStringAttribute("url");
// 获取<mapper>子标签的class属性
String mapperClass = child.getStringAttribute("class");
// 它们是互斥的
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
// 专门用来解析mapper映射文件
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 通过XMLMapperBuilder解析mapper映射文件
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());
// 通过XMLMapperBuilder解析mapper映射文件
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
// 将指定mapper接口以及它的代理对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
看完这段代码,可能人要说了,这段代码的逻辑和上一篇文章的代码不是几乎差不多嘛。的确是差不多,但是里的代码是mapper映射文件的入口处,因此这就是它的特殊之处,还有这段代码中涉及到了XMLMapperBuilder对象,而这个对象的作用正是专门用于解析mapper映射文件的。
我们还是来说下这段代码的意思吧,首先通过mappers标签来获得它的字标签,然后判断它下面是否存在package标签(<package name="xx.xx.Mapper"/>),如果存在,则直接进入它的逻辑代码中,然后获取其name属性,获取到后再将其放到configuration对象中。否则则进入其它代码逻辑,当进入反之的代码后,我们可以看到它获取了resource、url、class这三个标签的属性,然后根据这三个属性值的有无来执行相关代码,其实我们会发现当resource或url不为null的时候,都是先通过获取流,然后在调用XMLMapperBuilder的构造方法来进行创建XMLMapperBuilder对象,只有当class不为null的时候,才会通过反射来获取mapperClass对象,然后将其放到configuration对象中。
二、XMLMapperBuilder对象的构造
前面我们说到了这个方法中有XMLMapperBuilder这个对象,还有通过这个对象来调用parse()解析方法,那我们就来看看这个对象的构造方法,代码如下:
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
configuration, resource, sqlFragments);
}
这段代码很简单,但还是要进行相关说明,我们在这段代码中看到构造了这样几个对象XPathParser和XMLMapperEntityResolver,那么我们首先来看下XMLMapperEntityResolver这个类的代码:
/**
* Offline entity resolver for the MyBatis DTDs
*
*/
public class XMLMapperEntityResolver implements EntityResolver {
private static final String IBATIS_CONFIG_SYSTEM = "ibatis-3-config.dtd";
private static final String IBATIS_MAPPER_SYSTEM = "ibatis-3-mapper.dtd";
private static final String MYBATIS_CONFIG_SYSTEM = "mybatis-3-config.dtd";
private static final String MYBATIS_MAPPER_SYSTEM = "mybatis-3-mapper.dtd";
private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";
@Override
public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
try {
if (systemId != null) {
String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);
if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
} else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) {
return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId);
}
}
return null;
} catch (Exception e) {
throw new SAXException(e.toString());
}
}
private InputSource getInputSource(String path, String publicId, String systemId) {
InputSource source = null;
if (path != null) {
try {
InputStream in = Resources.getResourceAsStream(path);
source = new InputSource(in);
source.setPublicId(publicId);
source.setSystemId(systemId);
} catch (IOException e) {
// ignore, null is ok
}
}
return source;
}
}
这个类的功用其实MyBatis DTD离线实体解析器,具体代码也不复杂,这里就不多说了,有兴趣的朋友可以自己看看。那么我们来继续看下XPathParser类的代码吧!
三、XPathParser对象的创建流程
public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
// 解析XML文档为Document对象
this.document = createDocument(new InputSource(inputStream));
}
private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
this.validation = validation;
this.entityResolver = entityResolver;
this.variables = variables;
XPathFactory factory = XPathFactory.newInstance();
this.xpath = factory.newXPath();
}
private Document createDocument(InputSource inputSource) {
// important: this must only be called AFTER common constructor
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 进行dtd或者Schema校验
factory.setValidating(validation);
factory.setNamespaceAware(false);
// 设置忽略注释为true
factory.setIgnoringComments(true);
// 设置是否忽略元素内容中的空白
factory.setIgnoringElementContentWhitespace(false);
factory.setCoalescing(false);
factory.setExpandEntityReferences(true);
DocumentBuilder builder = factory.newDocumentBuilder();
builder.setEntityResolver(entityResolver);
builder.setErrorHandler(new ErrorHandler() {
@Override
public void error(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void warning(SAXParseException exception) throws SAXException {
}
});
// 通过dom解析,获取Document对象
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
}
上面一口气贴了三个方法的代码,大家不要慌,容我慢慢来细说。在XPathParser对象构建的时候,首先会调用commonConstructor方法,其实这个方法的本质就是个构造函数,在这个构造函数中最主要的就是通过XPathFactory获得了factory这个对象,然后通过factory获得了path。
在调完commonConstructor方法后,便调用了createDocument方法,从传入参数InputSource来看,我们一开始以为在这个方法中会通过流信息来做了什么,但细看代码会发现,前面大部分都和传入的参数无关,我们来看看commonConstructor方法代码的具体步骤:
- 首先通过DocumentBuilderFactory获得一个factory实例,然后对factory这个实例进行各种属性的设置。
- 当属性设置完成后,又通过factory实例穿件DocumentBuilder对象,在获得这个对象后又对这个对象进行属性设置。
- 最后DocumentBuilder这个对象通过传如的流进行解析。
这里我给大家展示下builder.parse(inputSource)方法做了什么,代码如下:
public Document parse(InputSource is) throws SAXException, IOException {
if (is == null) {
throw new IllegalArgumentException(
DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN,
"jaxp-null-input-source", null));
}
if (fSchemaValidator != null) {
if (fSchemaValidationManager != null) {
fSchemaValidationManager.reset();
fUnparsedEntityHandler.reset();
}
resetSchemaValidator();
}
domParser.parse(is);
Document doc = domParser.getDocument();
domParser.dropDocumentReferences();
return doc;
}
public void parse(InputSource inputSource)
throws SAXException, IOException {
// parse document
try {
XMLInputSource xmlInputSource =
new XMLInputSource(inputSource.getPublicId(),
inputSource.getSystemId(),
null);
xmlInputSource.setByteStream(inputSource.getByteStream());
xmlInputSource.setCharacterStream(inputSource.getCharacterStream());
xmlInputSource.setEncoding(inputSource.getEncoding());
parse(xmlInputSource);
}
// 此处省略代码
}
这里最重要的就是调用domParser.parse(is)方法去解析,然后通过domParser去获取Document对象,再调用dropDocumentReferences方法销毁解析器对Document的引用。最后返回Document对象。
四、解析mapper映射文件
这里所要说的就是最后的XMLMapperBuilder.parse(),我们就直接看parse方法的代码吧!代码如下:
public void parse() {
// mapper映射文件是否已经加载过
if (!configuration.isResourceLoaded(resource)) {
// 从映射文件中的<mapper>根标签开始解析,直到完整的解析完毕
configurationElement(parser.evalNode("/mapper"));
// 标记已经解析
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
代码看上去还是如此的熟悉,首先判断mapper映射文件是否已经加载过,没有则进入逻辑代码进行执行。先来看看 configurationElement(parser.evalNode("/mapper")),代码如下
/**
* 解析映射文件
* @param context 映射文件根节点<mapper>对应的XNode
*/
private void configurationElement(XNode context) {
try {
// 获取<mapper>标签的namespace值,也就是命名空间
String namespace = context.getStringAttribute("namespace");
// 命名空间不能为空
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 设置当前的命名空间为namespace的值
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>子标签,也就是SQL片段
sqlElement(context.evalNodes("/mapper/sql"));
// 解析<select>\<insert>\<update>\<delete>子标签
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);
}
}
这里基本上都是解析标签的方法,其他的标签解析还是相对简单,我们直接看buildStatementFromContext这个方法:
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
// 构建MappedStatement
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
// MappedStatement解析器
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 解析select等4个标签,创建MappedStatement对象
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
上面代码中通过传入的节点list来构建MappedStatement对象,然后通过MappedStatement来解析相关的标签信息。这里XMLStatementBuilder对象的构造代码我们就不看了,因为代码很简单,我们还是来看看核心的parseStatementNode方法:
/**
* 解析<select>\<insert>\<update>\<delete>子标签
*/
public void parseStatementNode() {
// 获取statement的id属性(特别关键的值)
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
// 获取入参类型
String parameterType = context.getStringAttribute("parameterType");
// 别名处理,获取入参对应的Java类型
Class<?> parameterTypeClass = resolveClass(parameterType);
// 获取ResultMap
String resultMap = context.getStringAttribute("resultMap");
// 获取结果映射类型
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// 别名处理,获取返回值对应的Java类型
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
// 设置默认StatementType为Prepared,该参数指定了后面的JDBC处理时,采用哪种Statement
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
String nodeName = context.getNode().getNodeName();
// 解析SQL命令类型是什么?确定操作是CRUD中的哪一种
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);
// Include Fragments before parsing
// <include>标签解析
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
// 解析<selectKey>标签
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
// 创建SqlSource,解析SQL,封装SQL语句(未参数绑定)和入参信息
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
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;
}
// 通过构建者助手,创建MappedStatement对象
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
这段代码看上去很复杂,其实重要的几个点无非是createSqlSource(创建SqlSource,解析SQL,封装SQL语句(未参数绑定)和入参信息)和addMappedStatement(通过构建者助手,创建MappedStatement对象)。
由于篇幅的原因,今天就先到这里了,夜也已深,童鞋们也都要早点休息。关于SQL的解析和addMappedStatement方法的调用我们后面将再写一篇文章来进行解析。最后我们还是来个简单的总结。
总结
相关类和接口:
- XMLConfigBuilder
- XMLMapperBuilder
- XPathParser
- MapperBuilderAssistant (下篇文章进行解析)
- XMLStatementBuilder
- MappedStatement
- XMLMapperBuilder#构造方法:专门用来解析映射文件的
- XPathParser#构造方法:
- XPathParser#createDocument():创建Mapper映射文件对应的Document对象
- MapperBuilderAssistant#构造方法:用于构建MappedStatement对象的 (下篇文章进行解析)
- XPathParser#构造方法:
- XMLMapperBuilder#parse():
- XMLMapperBuilder#configurationElement:专门用来解析mapper映射文件
- XMLMapperBuilder#buildStatementFromContext:用来创建MappedStatement对象的
- XMLMapperBuilder#buildStatementFromContext
- XMLStatementBuilder#构造方法:专门用来解析MappedStatement
- XMLStatementBuilder#parseStatementNode:
- MapperBuilderAssistant#addMappedStatement:创建MappedStatement对象(下篇文章进行解析)
- MappedStatement.Builder#构造方法(下篇文章进行解析)
- MappedStatement#build方法:创建MappedStatement对象,并存储到Configuration对象中(下篇文章进行解析)
- MapperBuilderAssistant#addMappedStatement:创建MappedStatement对象(下篇文章进行解析)
- XMLMapperBuilder#buildStatementFromContext
- XMLMapperBuilder#buildStatementFromContext:用来创建MappedStatement对象的
- XMLMapperBuilder#configurationElement:专门用来解析mapper映射文件