本文首发于个人公众号 Java 技术大杂烩,欢迎关注
相关文章
Mybatis Mapper 接口源码解析
Mybatis 数据库连接池源码解析
Mybatis 类型转换源码分析
Mybatis 解析配置文件的源码解析
前言在使用 Mybatis 的时候,我们在 Mapper.xml 配置文件中书写 SQL;文件中还配置了对应的dao,SQL 中还可以使用一些诸如for循环,if判断之类的高级特性,当数据库列和JavaBean属性不一致时定义的 resultMap等,接下来就来看下Mybatis 是如何从配置文件中解析出 SQL 并把用户传的参数进行绑定;
在 Mybatis 解析 SQL的时候,可以分为两部分来看,一是从 Mapper.xml 配置文件中解析SQL,二是把 SQL 解析成为数据库能够执行的原始 SQL,把占位符替换为 ? 等。
这篇文章先来看下第一部分,Mybatis 是如何从 Mapper.xml 配置文件中解析出 SQL 的。
配置文件的解析使用了大量的建造者模式(builder)
mybatis-config.xmlMybatis 有两个配置文件,mybaits-config.xml 配置的是 mybatis 的一些全局配置信息,而 mapper.xml 配置的是 SQL 信息,在 Mybatis 初始化的时候,会对这两个文件进行解析,mybatis-config.xml 配置文件的解析比较简单,不再细说,使用的 XMLConfigBuilder 类来对 mybatis-config.xml 文件进行解析。
1 public Configuration parse() { 2 // 如果已经解析过,则抛异常 3 if (parsed) { 4 throw new BuilderException("Each XMLConfigBuilder can only be used once."); 5 } 6 parsed = true; 7 parseConfiguration(parser.evalNode("/configuration")); 8 return configuration; 9 } 10 // 解析 mybatis-config.xml 文件下的所有节点 11 private void parseConfiguration(XNode root) { 12 propertiesElement(root.evalNode("properties")); 13 Properties settings = settingsAsProperties(root.evalNode("settings")); 14 // .... 其他的节点........ 15 // 解析 mapper.xml 文件 16 mapperElement(root.evalNode("mappers")); 17 } 18 19 // 解析 mapper.xml 文件 20 private void mapperElement(XNode parent) throws Exception { 21 // ...... 22 InputStream inputStream = Resources.getUrlAsStream(url); 23 XMLMapperBuilder mapperParser = 24 new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); 25 mapperParser.parse(); 26 }从上述代码可以看到,解析 Mapper.xml 配置文件是通过 XMLMapperBuilder 来解析的。接下来看下该类的实现:
XMLMapperBuilderXMLMapperBuilder 类是用来解析 Mapper.xml 文件的,它继承了 BaseBuilder ,BaseBuilder 类一个建造者基类,其中包含了 Mybatis 全局的配置信息 Configuration ,别名处理器,类型处理器等,如下所示:
1public abstract class BaseBuilder { 2 protected final Configuration configuration; 3 protected final TypeAliasRegistry typeAliasRegistry; 4 protected final TypeHandlerRegistry typeHandlerRegistry; 5 6 public BaseBuilder(Configuration configuration) { 7 this.configuration = configuration; 8 this.typeAliasRegistry = this.configuration.getTypeAliasRegistry(); 9 this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry(); 10 } 11}关于 TypeAliasRegistry, TypeHandlerRegistry 可以参考 Mybatis 类型转换源码分析
接下来看下 XMLMapperBuilder 类的属性定义:
1public class XMLMapperBuilder extends BaseBuilder { 2 // xpath 包装类 3 private XPathParser parser; 4 // MapperBuilder 构建助手 5 private MapperBuilderAssistant builderAssistant; 6 // 用来存放sql片段的哈希表 7 private Map<String, XNode> sqlFragments; 8 // 对应的 mapper 文件 9 private String resource; 10 11 private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) { 12 super(configuration); 13 this.builderAssistant = new MapperBuilderAssistant(configuration, resource); 14 this.parser = parser; 15 this.sqlFragments = sqlFragments; 16 this.resource = resource; 17 } 18 // 解析文件 19 public void parse() { 20 // 判断是否已经加载过该配置文件 21 if (!configuration.isResourceLoaded(resource)) { 22 // 解析 mapper 节点 23 configurationElement(parser.evalNode("/mapper")); 24 // 将 resource 添加到 configuration 的 addLoadedResource 集合中保存,该集合中记录了已经加载过的配置文件 25 configuration.addLoadedResource(resource); 26 // 注册 Mapper 接口 27 bindMapperForNamespace(); 28 } 29 // 处理解析失败的 <resultMap> 节点 30 parsePendingResultMaps(); 31 // 处理解析失败的 <cache-ref> 节点 32 parsePendingChacheRefs(); 33 // 处理解析失败的 SQL 节点 34 parsePendingStatements(); 35 }从上面的代码中,使用到了 MapperBuilderAssistant 辅助类,该类中有许多的辅助方法,其中有个 currentNamespace 属性用来表示当前的 Mapper.xml 配置文件的命名空间,在解析完成 Mapper.xml 配置文件的时候,会调用 bindMapperForNamespace 进行注册Mapper接口,表示该配置文件对应的Mapper接口`,关于 Mapper 的注册可以参考 Mybatis Mapper 接口源码解析
1 private void bindMapperForNamespace() { 2 // 获取当前的命名空间 3 String namespace = builderAssistant.getCurrentNamespace(); 4 if (namespace != null) { 5 Class<?> boundType = Resources.classForName(namespace); 6 if (boundType != null) { 7 // 如果还没有注册过该 Mapper 接口,则注册 8 if (!configuration.hasMapper(boundType)) { 9 configuration.addLoadedResource("namespace:" + namespace); 10 // 注册 11 configuration.addMapper(boundType); 12 } 13 } 14 }现在就来解析 Mapper.xml 文件的每个节点,每个节点的解析都封装成一个方法,很好理解:
1 private void configurationElement(XNode context) { 2 // 命名空间 3 String namespace = context.getStringAttribute("namespace"); 4 // 设置命名空间 5 builderAssistant.setCurrentNamespace(namespace); 6 // 解析 <cache-ref namespace=""/> 节点 7 cacheRefElement(context.evalNode("cache-ref")); 8 // 解析 <cache /> 节点 9 cacheElement(context.evalNode("cache")); 10 // 已废弃,忽略 11 parameterMapElement(context.evalNodes("/mapper/parameterMap")); 12 // 解析 <resultMap /> 节点 13 resultMapElements(context.evalNodes("/mapper/resultMap")); 14 // 解析 <sql> 节点 15 sqlElement(context.evalNodes("/mapper/sql")); 16 // 解析 select|insert|update|delete 这几个节点 17 buildStatementFromContext(context.evalNodes("select|insert|update|delete")); 18 }解析 <cache> 节点Mybatis 默认情况下是没有开启二级缓存的,除了局部的 session 缓存。如果要为某个命名空间开启二级缓存,则需要在 SQL 映射文件中添加<cache>标签来告诉 Mybatis 需要开启二级缓存,先来看看 <cache> 标签的使用说明:
1<cache eviction="LRU" flushInterval="1000" size="1024" readOnly="true" type="MyCache" blocking="true"/><cache> 一共有 6 个属性,可以用来改变Mybatis 缓存的默认行为:
eviction: 缓存的过期策略,可以取 4 个值:LRU – 最近最少使用的:移除最长时间不被使用的对象。(默认)FIFO – 先进先出:按对象进入缓存的顺序来移除它们。SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。 flushInterval: 刷新缓存的时间间隔,默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新size: 缓存大小readOnly: 是否是只读type : 自定义缓存的实现blocking:是否是阻塞该类中主要使用 cacheElement 方法来解析 <cache> 节点:
1 // 解析 <cache> 节点 2 private void cacheElement(XNode context) throws Exception { 3 if (context != null) { 4 // 获取 type 属性,默认为 PERPETUAL 5 String type = context.getStringAttribute("type", "PERPETUAL"); 6 Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); 7 // 获取过期策略 eviction 属性 8 String eviction = context.getStringAttribute("eviction", "LRU"); 9 Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); 10 Long flushInterval = context.getLongAttribute("flushInterval"); 11 Integer size = context.getIntAttribute("size"); 12 boolean readWrite = !context.getBooleanAttribute("readOnly", false); 13 boolean blocking = context.getBooleanAttribute("blocking", false); 14 // 获取 <cache> 节点下的子节点,将用于初始化二级缓存 15 Properties props = context.getChildrenAsProperties(); 16 // 创建 Cache 对象,并添加到 configuration.caches 集合中保存 17 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); 18 } 19 }接下来看下 MapperBuilderAssistant 辅助类如何创建缓存,并添加到 configuration.caches 集合中去:
1 public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, 2 Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { 3 // 创建缓存,使用构造者模式设置对应的属性 4 Cache cache = new CacheBuilder(currentNamespace) 5 .implementation(valueOrDefault(typeClass, PerpetualCache.class)) 6 .addDecorator(valueOrDefault(evictionClass, LruCache.class)) 7 .clearInterval(flushInterval) 8 .size(size) 9 .readWrite(readWrite) 10 .blocking(blocking) 11 .properties(props) 12 .build(); 13 // 进入缓存集合 14 configuration.addCache(cache); 15 // 当前缓存 16 currentCache = cache; 17 return cache; 18 }再来看下 CacheBuilder 是个什么东西,它是 Cache 的建造者,如下所示:
1public class CacheBuilder { 2 // Cache 对象的唯一标识,对应配置文件中的 namespace 3 private String id; 4 // Cache 的实现类 5 private Class<? extends Cache> implementation; 6 // 装饰器集合 7 private List<Class<? extends Cache>> decorators; 8 private Integer size; 9 private Long clearInterval; 10 private boolean readWrite; 11 // 其他配置信息 12 private Properties properties; 13 // 是否阻塞 14 private boolean blocking; 15 16 // 创建 Cache 对象 17 public Cache build() { 18 // 设置 implementation 的默认值为 PerpetualCache ,decorators 的默认值为 LruCache 19 setDefaultImplementations(); 20 // 创建 Cache 21 Cache cache = newBaseCacheInstance(implementation, id); 22 // 设置 <properties> 节点信息 23 setCacheProperties(cache); 24 if (PerpetualCache.class.equals(cache.getClass())) { 25 for (Class<? extends Cache> decorator : decorators) { 26 cache = newCacheDecoratorInstance(decorator, cache); 27 setCacheProperties(cache); 28 } 29 cache = setStandardDecorators(cache); 30 } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) { 31 cache = new LoggingCache(cache); 32 } 33 return cache; 34 } 35}解析 <cache-ref> 节点在使用了 <cache> 配置了对应的缓存后,多个 namespace 可以引用同一个缓存,使用 <cache-ref> 进行指定
1<cache-ref namespace="com.someone.application.data.SomeMapper"/> 2 3cacheRefElement(context.evalNode("cache-ref"));解析的源码如下,比较简单:
1 private void cacheRefElement(XNode context) { 2 // 当前文件的namespace 3 String currentNamespace = builderAssistant.getCurrentNamespace(); 4 // ref 属性所指向引用的 namespace 5 String refNamespace = context.getStringAttribute("namespace"); 6 // 会存入到 configuration 的一个 map 中, cacheRefMap.put(namespace, referencedNamespace); 7 configuration.addCacheRef(currentNamespace , refNamespace ); 8 CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, refNamespace); 9 // 实际上调用 构建助手 builderAssistant 的 useCacheRef 方法进行解析 10 cacheRefResolver.resolveCacheRef(); 11 } 12 }构建助手 builderAssistant 的 useCacheRef 方法:
1 public Cache useCacheRef(String namespace) { 2 // 标识未成功解析的 Cache 引用 3 unresolvedCacheRef = true; 4 // 根据 namespace 中 configuration 的缓存集合中获取缓存 5 Cache cache = configuration.getCache(namespace); 6 if (cache == null) { 7 throw new IncompleteElementException("...."); 8 } 9 // 当前使用的缓存 10 currentCache = cache; 11 // 已成功解析 Cache 引用 12 unresolvedCacheRef = false; 13 return cache; 14 }解析 <resultMap> 节点resultMap 节点很强大,也很复杂,会单独另写一篇文章来介绍。
解析 <sql> 节点<sql> 节点可以用来定义重用的SQ片段,
1 <sql id="commSQL" databaseId="" lang=""> 2 id, name, job, age 3 </sql> 4 5 sqlElement(context.evalNodes("/mapper/sql"));sqlElement 方法如下,一个 Mapper.xml 文件可以有多个 sql 节点:
1 private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception { 2 // 遍历,处理每个 sql 节点 3 for (XNode context : list) { 4 // 数据库ID 5 String databaseId = context.getStringAttribute("databaseId"); 6 // 获取 id 属性 7 String id = context.getStringAttribute("id"); 8 // 为 id 加上 namespace 前缀,如原来 id 为 commSQL,加上前缀就变为了 com.aa.bb.cc.commSQL 9 id = builderAssistant.applyCurrentNamespace(id, false); 10 if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) { 11 // 如果 SQL 片段匹配对应的数据库,则把该节点加入到缓存中,是一个 map 12 // Map<String, XNode> sqlFragments 13 sqlFragments.put(id, context); 14 } 15 } 16 }为ID 加上namespace前缀的方法如下:
1 public String applyCurrentNamespace(String base, boolean isReference) { 2 if (base == null) { 3 return null; 4 } 5 // 是否已经包含 namespace 了 6 if (isReference) { 7 if (base.contains(".")) { 8 return base; 9 } 10 } else { 11 // 是否是一 namespace. 开头 12 if (base.startsWith(currentNamespace + ".")) { 13 return base; 14 } 15 } 16 // 返回 namespace.id,即 com.aa.bb.cc.commSQL 17 return currentNamespace + "." + base; 18 }insert | update | delete | select 节点的解析关于这些与操作数据库的SQL的解析,主要是由 XMLStatementBuilder 类来进行解析。在 Mybatis 中使用 SqlSource 来表示 SQL语句,但是这些SQL 语句还不能直接在数据库中进行执行,可能还有动态SQL语句和占位符等。
接下来看下这类节点的解析:
1buildStatementFromContext(context.evalNodes("select|insert|update|delete")); 2 3private void buildStatementFromContext(List<XNode> list) { 4// 匹配对应的数据库 5if (configuration.getDatabaseId() != null) { 6 buildStatementFromContext(list, configuration.getDatabaseId()); 7} 8buildStatementFromContext(list, null); 9} 10 11private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { 12for (XNode context : list) { 13 // 为 XMLStatementBuilder 对应的属性赋值 14 final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); 15 // 解析每个节点 16 statementParser.parseStatementNode(); 17}可以看到 selelct | insert | update | delete 这类节点是使用 XMLStatementBuilder 类的 parseStatementNode() 方法来解析的,接下来看下该方法的实现:
1 public void parseStatementNode() { 2 // id 属性和数据库标识 3 String id = context.getStringAttribute("id"); 4 String databaseId = context.getStringAttribute("databaseId"); 5 // 如果数据库不匹配则不加载 6 if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { 7 return; 8 } 9 // 获取节点的属性和对应属性的类型 10 Integer fetchSize = context.getIntAttribute("fetchSize"); 11 Integer timeout = context.getIntAttribute("timeout"); 12 Integer fetchSize = context.getIntAttribute("fetchSize"); 13 Integer timeout = context.getIntAttribute("timeout"); 14 String parameterMap = context.getStringAttribute("parameterMap"); 15 String parameterType = context.getStringAttribute("parameterType"); 16 // 从注册的类型里面查找参数类型 17 Class<?> parameterTypeClass = resolveClass(parameterType); 18 String resultMap = context.getStringAttribute("resultMap"); 19 String resultType = context.getStringAttribute("resultType"); 20 String lang = context.getStringAttribute("lang"); 21 LanguageDriver langDriver = getLanguageDriver(lang); 22 // 从注册的类型里面查找返回值类型 23 Class<?> resultTypeClass = resolveClass(resultType); 24 String resultSetType = context.getStringAttribute("resultSetType"); 25 StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); 26 ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); 27 28 // 获取节点的名称 29 String nodeName = context.getNode().getNodeName(); 30 // 根据节点的名称来获取节点的类型,枚举:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH; 31 SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); 32 // 下面这三行代码,如果是select语句,则不会刷新缓存和需要使用缓存 33 boolean isSelect = sqlCommandType == SqlCommandType.SELECT; 34 boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); 35 boolean useCache = context.getBooleanAttribute("useCache", isSelect); 36 boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); 37 38 // 解析 <include> 节点 39 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); 40 includeParser.applyIncludes(context.getNode()); 41 42 // 解析 selectKey 节点 43 processSelectKeyNodes(id, parameterTypeClass, langDriver); 44 // 创建 sqlSource 45 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); 46 // 处理 resultSets keyProperty keyColumn 属性 47 String resultSets = context.getStringAttribute("resultSets"); 48 String keyProperty = context.getStringAttribute("keyProperty"); 49 String keyColumn = context.getStringAttribute("keyColumn"); 50 // 处理 keyGenerator 51 KeyGenerator keyGenerator; 52 String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; 53 keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); 54 if (configuration.hasKeyGenerator(keyStatementId)) { 55 keyGenerator = configuration.getKeyGenerator(keyStatementId); 56 } else { 57 keyGenerator = context.getBooleanAttribute("useGeneratedKeys", 58 configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) 59 ? new Jdbc3KeyGenerator() : new NoKeyGenerator(); 60 } 61 // 创建 MapperedStatement 对象,添加到 configuration 中 62 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, 63 fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, 64 resultSetTypeEnum, flushCache, useCache, resultOrdered, 65 keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); 66}该方法主要分为几个部分:
解析属性解析 include 节点解析 selectKey 节点创建MapperedStatment对象并添加到configuration对应的集合中解析属性比较简单,接下来看看后面几个部分:
解析 include 子节点解析include节点就是把其包含的SQL片段替换成 <sql> 节点定义的SQL片段,并将 ${xxx} 占位符替换成真实的参数:
它是使用 XMLIncludeTransformer 类的 applyIncludes 方法来解析的:
1 public void applyIncludes(Node source) { 2 // 获取参数 3 Properties variablesContext = new Properties(); 4 Properties configurationVariables = configuration.getVariables(); 5 if (configurationVariables != null) { 6 variablesContext.putAll(configurationVariables); 7 } 8 // 解析 9 applyIncludes(source, variablesContext, false); 10 } 11 12 private void applyIncludes(Node source, final Properties variablesContext, boolean included) { 13 if (source.getNodeName().equals("include")) { 14 // 这里是根据 ref 属性对应的值去 <sql> 节点对应的集合查找对应的SQL片段, 15 // 在解析 <sql> 节点的时候,把它放到了一个map中,key为namespace+id,value为对应的节点, 16 // 现在要拿 ref 属性去这个集合里面获取对应的SQL片段 17 Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext); 18 // 解析include的子节点<properties> 19 Properties toIncludeContext = getVariablesContext(source, variablesContext); 20 // 递归处理<include>节点 21 applyIncludes(toInclude, toIncludeContext, true); 22 if (toInclude.getOwnerDocument() != source.getOwnerDocument()) { 23 toInclude = source.getOwnerDocument().importNode(toInclude, true); 24 } 25 // 将 include 节点替换为 sql 节点 26 source.getParentNode().replaceChild(toInclude, source); 27 while (toInclude.hasChildNodes()) { 28 toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude); 29 } 30 toInclude.getParentNode().removeChild(toInclude); 31 } else if (source.getNodeType() == Node.ELEMENT_NODE) { 32 // 处理当前SQL节点的子节点 33 NodeList children = source.getChildNodes(); 34 for (int i = 0; i < children.getLength(); i++) { 35 applyIncludes(children.item(i), variablesContext, included); 36 } 37 } else if (included && source.getNodeType() == Node.TEXT_NODE 38 && !variablesContext.isEmpty()) { 39 // 绑定参数 40 source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext)); 41 } 42 }selectKey 就是生成主键,可以不用看。
到这里,mapper.xml 配置文件中的节点已经解析完毕了 除了 resultMap 节点,在文章的开头部分,在解析节点的时候,有时候可能会出错,抛出异常,在解析每个解析抛出异常的时候,都会把该解析放入到对应的集合中再次进行解析,所以在解析完成后,还有如下三行代码:
1 // 处理解析失败的 <resultMap> 节点 2 parsePendingResultMaps(); 3 // 处理解析失败的 <cache-ref> 节点 4 parsePendingChacheRefs(); 5 // 处理解析失败的 SQL 节点 6 parsePendingStatements();就是用来从新解析失败的那些节点的。
到这里,Mapper.xml 配置文件就解析完毕了。
---来自腾讯云社区的---Java技术大杂烩
微信扫一扫打赏
支付宝扫一扫打赏