上文重点阐述源码分析MapperProxy初始化,但并没有介绍.Mapper.java(UserMapper.java)是如何与Mapper.xml文件中的SQL语句是如何建立关联的。本文将重点接开这个谜团。
接下来重点从源码的角度分析Mybatis MappedStatement的创建流程。
我们注意到这里有两三个与Mapper相关的配置:
- SqlSessionFactory#mapperLocations,指定xml文件的配置路径。
- SqlSessionFactory#configLocation,指定mybaits的配置文件,该配置文件也可以配置mapper.xml的配置路径信息。
- MapperScannerConfigurer,扫描Mapper的java类(DAO)。
我们已经详细介绍了Mybatis Mapper对象的扫描与构建,那接下来我们将重点介绍MaperProxy与mapper.xml文件是如何建立关联关系的。
根据上面的罗列以及上文的讲述xml映射文件与Mapper建立联系的入口有三:
- MapperScannerConfigurer扫描Bean流程中,在调用MapperReigistry#addMapper时如果Mapper对应的映射文件(Mapper.xml)未加载到内存,会触发加载。
- 实例化SqlSessionFactory时,如果配置了mapperLocations。
- 示例化SqlSessionFactory时,如果配置了configLocation。
本节的行文思路:从SqlSessionFacotry的初始化开始讲起,因为mapperLocations、configLocation都是SqlSessionFactory的属性。
温馨提示:下面开始从源码的角度对其进行介绍,大家可以先跳到文末看看其调用序列图。
buildSqlSessionFactory
1if (xmlConfigBuilder != null) { // XMLConfigBuilder // @1 2 try { 3 xmlConfigBuilder.parse(); 4 5 if (logger.isDebugEnabled()) { 6 logger.debug("Parsed configuration file: '" + this.configLocation + "'"); 7 } 8 } catch (Exception ex) { 9 throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex); 10 } finally { 11 ErrorContext.instance().reset(); 12 } 13 } 14 15if (!isEmpty(this.mapperLocations)) { // @2 16 for (Resource mapperLocation : this.mapperLocations) { 17 if (mapperLocation == null) { 18 continue; 19 } 20 21 try { 22 XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), 23 configuration, mapperLocation.toString(), configuration.getSqlFragments()); 24 xmlMapperBuilder.parse(); 25 } catch (Exception e) { 26 throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e); 27 } finally { 28 ErrorContext.instance().reset(); 29 } 30 31 if (logger.isDebugEnabled()) { 32 logger.debug("Parsed mapper file: '" + mapperLocation + "'"); 33 } 34 } 35 } else { 36 if (logger.isDebugEnabled()) { 37 logger.debug("Property 'mapperLocations' was not specified or no matching resources found"); 38 } 39 }
上文有两个入口:
代码@1:处理configLocation属性。
代码@2:处理mapperLocations属性。
我们先从XMLConfigBuilder#parse开始进行追踪。该方法主要是解析configLocation指定的配置路径,对其进行解析,具体调用parseConfiguration方法。
XMLConfigBuilder
我们直接查看其parseConfiguration方法。
1private void parseConfiguration(XNode root) { 2 try { 3 propertiesElement(root.evalNode("properties")); //issue #117 read properties first 4 typeAliasesElement(root.evalNode("typeAliases")); 5 pluginElement(root.evalNode("plugins")); 6 objectFactoryElement(root.evalNode("objectFactory")); 7 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); 8 settingsElement(root.evalNode("settings")); 9 environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631 10 databaseIdProviderElement(root.evalNode("databaseIdProvider")); 11 typeHandlerElement(root.evalNode("typeHandlers")); 12 mapperElement(root.evalNode("mappers")); // @1 13 } catch (Exception e) { 14 throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); 15 } 16 }
重点关注mapperElement,从名称与参数即可以看出,该方法主要是处理中mappers的定义,即mapper sql语句的解析与处理。如果使用过Mapper的人应该不难知道,我们使用mapper节点,通过resource标签定义具体xml文件的位置。
XMLConfigBuilder#mapperElement
1private void mapperElement(XNode parent) throws Exception { 2 if (parent != null) { 3 for (XNode child : parent.getChildren()) { 4 if ("package".equals(child.getName())) { 5 String mapperPackage = child.getStringAttribute("name"); 6 configuration.addMappers(mapperPackage); 7 } else { 8 String resource = child.getStringAttribute("resource"); 9 String url = child.getStringAttribute("url"); 10 String mapperClass = child.getStringAttribute("class"); 11 if (resource != null && url == null && mapperClass == null) { 12 ErrorContext.instance().resource(resource); 13 InputStream inputStream = Resources.getResourceAsStream(resource); 14 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); // @1 15 mapperParser.parse(); 16 } else if (resource == null && url != null && mapperClass == null) { 17 ErrorContext.instance().resource(url); 18 InputStream inputStream = Resources.getUrlAsStream(url); 19 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); 20 mapperParser.parse(); 21 } else if (resource == null && url == null && mapperClass != null) { 22 Class<?> mapperInterface = Resources.classForName(mapperClass); 23 configuration.addMapper(mapperInterface); 24 } else { 25 throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); 26 } 27 } 28 } 29 } 30 }
上面的代码比较简单,不难看出,解析出Mapper标签,解析出resource标签的属性,创建对应的文件流,通过构建XMLMapperBuilder来解析对应的mapper.xml文件。此时大家会惊讶的发现,在SqlSessionFacotry的初始化代码中,处理mapperLocations时就是通过构建XMLMapperBuilder来解析mapper文件,其实也不难理解,因为这是mybatis支持的两个地方可以使用mapper标签来定义mapper映射文件,具体解析代码当然是一样的逻辑。那我们解析来重点把目光投向XMLMapperBuilder。
XMLMapperBuilder
1XMLMapperBuilder#parse 2public void parse() { 3 if (!configuration.isResourceLoaded(resource)) { // @1 4 configurationElement(parser.evalNode("/mapper")); 5 configuration.addLoadedResource(resource); 6 bindMapperForNamespace(); 7 } 8 9 parsePendingResultMaps(); // @2 10 parsePendingChacheRefs(); // @3 11 parsePendingStatements(); // @4 12 }
代码@1:如果该映射文件(*.Mapper.xml)文件未加载,则首先先加载,完成xml文件的解析,提取xml中与mybatis相关的数据,例如sql、resultMap等等。
代码@2:处理mybatis xml中ResultMap。
代码@3:处理mybatis缓存相关的配置。
代码@4:处理mybatis statment相关配置,这里就是本篇关注的,Sql语句如何与Mapper进行关联的核心实现。
接下来我们重点探讨parsePendingStatements()方法,解析statement(对应SQL语句)。
XMLMapperBuilder
1private void parsePendingStatements() { 2 Collection<XMLStatementBuilder> incompleteStatements = configuration.getIncompleteStatements(); 3 synchronized (incompleteStatements) { 4 Iterator<XMLStatementBuilder> iter = incompleteStatements.iterator(); // @1 5 while (iter.hasNext()) { 6 try { 7 iter.next().parseStatementNode(); // @2 8 iter.remove(); 9 } catch (IncompleteElementException e) { 10 // Statement is still missing a resource... 11 } 12 } 13 } 14 }
代码@1:遍历解析出来的所有SQL语句,用的是XMLStatementBuilder对象封装的,故接下来重点看一下代码@2,如果解析statmentNode。
XMLStatementBuilder
1public void parseStatementNode() { 2 String id = context.getStringAttribute("id"); // @1 start 3 String databaseId = context.getStringAttribute("databaseId"); 4 5 if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) return; 6 7 Integer fetchSize = context.getIntAttribute("fetchSize"); 8 Integer timeout = context.getIntAttribute("timeout"); 9 String parameterMap = context.getStringAttribute("parameterMap"); 10 String parameterType = context.getStringAttribute("parameterType"); 11 Class<?> parameterTypeClass = resolveClass(parameterType); 12 String resultMap = context.getStringAttribute("resultMap"); 13 String resultType = context.getStringAttribute("resultType"); 14 String lang = context.getStringAttribute("lang"); 15 LanguageDriver langDriver = getLanguageDriver(lang); 16 17 Class<?> resultTypeClass = resolveClass(resultType); 18 String resultSetType = context.getStringAttribute("resultSetType"); 19 StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); 20 ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); 21 22 String nodeName = context.getNode().getNodeName(); 23 SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); 24 boolean isSelect = sqlCommandType == SqlCommandType.SELECT; 25 boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); 26 boolean useCache = context.getBooleanAttribute("useCache", isSelect); 27 boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); 28 29 // Include Fragments before parsing 30 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); 31 includeParser.applyIncludes(context.getNode()); 32 33 // Parse selectKey after includes and remove them. 34 processSelectKeyNodes(id, parameterTypeClass, langDriver); // @1 end 35 36 // Parse the SQL (pre: <selectKey> and <include> were parsed and removed) 37 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); // @2 38 String resultSets = context.getStringAttribute("resultSets"); 39 String keyProperty = context.getStringAttribute("keyProperty"); 40 String keyColumn = context.getStringAttribute("keyColumn"); 41 KeyGenerator keyGenerator; 42 String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; 43 keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); 44 if (configuration.hasKeyGenerator(keyStatementId)) { 45 keyGenerator = configuration.getKeyGenerator(keyStatementId); 46 } else { 47 keyGenerator = context.getBooleanAttribute("useGeneratedKeys", 48 configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) 49 ? new Jdbc3KeyGenerator() : new NoKeyGenerator(); 50 } 51 52 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, // @3 53 fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, 54 resultSetTypeEnum, flushCache, useCache, resultOrdered, 55 keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); 56 }
这个方法有点长,其关注点主要有3个:
代码@1:构建基本属性,其实就是构建MappedStatement的属性,因为MappedStatement对象就是用来描述Mapper-SQL映射的对象。
代码@2:根据xml配置的内容,解析出实际的SQL语句,使用SqlSource对象来表示。
代码@3:使用MapperBuilderAssistant对象,根据准备好的属性,构建MappedStatement对象,最终将其存储在Configuration中。
Configuration#addMappedStatement
1public void addMappedStatement(MappedStatement ms) { 2 mappedStatements.put(ms.getId(), ms); 3}
MappedStatement的id为:mapperInterface + methodName,例如com.demo.dao.UserMapper.findUser。
即上述流程完成了xml的解析与初始化,对终极目标是创建MappedStatement对象,上一篇文章介绍了mapperInterface的初始化,最终会初始化为MapperProxy对象,那这两个对象如何关联起来呢?
从下文可知,MapperProxy与MappedStatement是在调用具Mapper方法时,可以根据mapperInterface.getName + methodName构建出MappedStatement的id,然后就可以从Configuration的mappedStatements容器中根据id获取到对应的MappedStatement对象,这样就建立起联系了。
其对应的代码:
1// MapperMethod 构造器 2public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { 3 this.command = new SqlCommand(config, mapperInterface, method); 4 this.method = new MethodSignature(config, method); 5} 6 7// SqlCommand 构造器 8public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) throws BindingException { 9 String statementName = mapperInterface.getName() + "." + method.getName(); 10 MappedStatement ms = null; 11 if (configuration.hasStatement(statementName)) { 12 ms = configuration.getMappedStatement(statementName); 13 } else if (!mapperInterface.equals(method.getDeclaringClass().getName())) { // issue #35 14 String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName(); 15 if (configuration.hasStatement(parentStatementName)) { 16 ms = configuration.getMappedStatement(parentStatementName); 17 } 18 } 19 if (ms == null) { 20 throw new BindingException("Invalid bound statement (not found): " + statementName); 21 } 22 name = ms.getId(); 23 type = ms.getSqlCommandType(); 24 if (type == SqlCommandType.UNKNOWN) { 25 throw new BindingException("Unknown execution method for: " + name); 26 } 27 }
怎么样,从上面的源码分析中,大家是否已经了解MapperProxy与Xml中的SQL语句是怎样建立的关系了吗?为了让大家更清晰的了解上述过程,现给出其调用时序图:
本文的讲解就到此结束了,下文将介绍Mybaits与Sharding-Jdbc整合时,SQL语句的执行过程。