1 前言
这节我们分析一个大头,也是我们平时写的最多的,就是我们写的增删改查了,我们来看下它的解析。
既然 MyBatis 的行为已经由上述元素配置完了,我们现在就要来定义 SQL 映射语句了。 但首先,我们需要告诉 MyBatis 到哪里去找到这些语句。 在自动查找资源方面,Java 并没有提供一个很好的解决方案,所以最好的办法是直接告诉 MyBatis 到哪里去找映射文件。 你可以使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:///
形式的 URL),或类名和包名等。例如:
<!-- 使用相对于类路径的资源引用 --> <mappers> <mapper resource="org/mybatis/builder/AuthorMapper.xml"/> <mapper resource="org/mybatis/builder/BlogMapper.xml"/> <mapper resource="org/mybatis/builder/PostMapper.xml"/> </mappers>
<!-- 使用完全限定资源定位符(URL) --> <mappers> <mapper url="file:///var/mappers/AuthorMapper.xml"/> <mapper url="file:///var/mappers/BlogMapper.xml"/> <mapper url="file:///var/mappers/PostMapper.xml"/> </mappers>
<!-- 使用映射器接口实现类的完全限定类名 --> <mappers> <mapper class="org.mybatis.builder.AuthorMapper"/> <mapper class="org.mybatis.builder.BlogMapper"/> <mapper class="org.mybatis.builder.PostMapper"/> </mappers>
<!-- 将包内的映射器接口全部注册为映射器 --> <mappers> <package name="org.mybatis.builder"/> </mappers>
这些配置就是会告诉 MyBatis 去哪里找映射文件。
2 源码分析
2.1 方法通读
我们先来看下 mapperElement 方法:
private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { // 获取 <package> 节点中的 name 属性 String mapperPackage = child.getStringAttribute("name"); // 从指定包中查找 mapper 接口,并根据 mapper 接口解析映射配置 configuration.addMappers(mapperPackage); } else { // 获取 resource/url/class 等属性 String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); // resource 不为空,且其他两者为空,则从指定路径中加载配置 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(); // url 不为空,且其他两者为空,则通过 url 加载配置 } 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(); // mapperClass 不为空,且其他两者为空,则通过 mapperClass 解析映射配置 } 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."); } } } } }
以上就是分别针对 mapper 的四种情况分别进行判断解析,那么我们该看哪个呢,我们就从我们最熟悉的平时在XML写的SQL,也就是XML方式去看看如何一步步解析成相应的对象的。
下面开始分析映射文件的解析过程,在展开分析之前,先来看一下映射文件解析入口。如下:
// XMLMapperBuilder public void parse() { // 检测映射文件是否已经被解析过 if (!configuration.isResourceLoaded(resource)) { // 解析 mapper 节点 configurationElement(parser.evalNode("/mapper")); // 添加资源路径到已解析资源集合中 configuration.addLoadedResource(resource); // 通过命名空间绑定 Mapper 接口 bindMapperForNamespace(); } // 处理未完成解析的节点 parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }
如上,映射文件解析入口逻辑包含三个核心操作,分别如下:
- 解析 mapper 节点
- 通过命名空间绑定 Mapper 接口
- 处理未完成解析的节点
我们再来熟悉下 mapper 中的一些常用的节点信息:
cache
– 该命名空间的缓存配置。cache-ref
– 引用其它命名空间的缓存配置。resultMap
– 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。sql
– 可被其它语句引用的可重用语句块。insert
– 映射插入语句。update
– 映射更新语句。delete
– 映射删除语句。select
– 映射查询语句。
2.2 解析 mapper
对 mapper 的解析,每种节点的逻辑都封装在了相应的方法中,这些方法由 XMLMapperBuilder 类的 configurationElement 方法统一调用。该方法的逻辑如下:
private void configurationElement(XNode context) { try { // 获取 mapper 命名空间 String namespace = context.getStringAttribute("namespace"); // 名称空间至关重要 没有的话会直接报错 if (namespace == null || namespace.isEmpty()) { throw new BuilderException("Mapper's namespace cannot be empty"); } // 设置命名空间到 builderAssistant 中 builderAssistant.setCurrentNamespace(namespace); // 解析 <cache-ref> 节点 cacheRefElement(context.evalNode("cache-ref")); // 解析 <cache> 节点 cacheElement(context.evalNode("cache")); // 被废弃掉了并可能在将来被移除!请使用行内参数映射。 parameterMapElement(context.evalNodes("/mapper/parameterMap")); // 解析 <resultMap> 节点 resultMapElements(context.evalNodes("/mapper/resultMap")); // 解析 <sql> 节点 sqlElement(context.evalNodes("/mapper/sql")); // 解析 <select>、...、<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); } }
好了,接下来我们就要逐一进行分析了。
2.3 解析 <cache> 节点
MyBatis 提供了一、二级缓存,其中一级缓存是 SqlSession 级别的,默认为开启状态。二级缓存配置在映射文件中,使用者需要显示配置才能开启。我们看看二级缓存的一些配置:
<cache/> <!-- 参数更改--> <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
关于缓存的东西很多,我们这里重点讲解析哈,我们先看看它的dtd约束:
<!ELEMENT cache (property*)> <!ATTLIST cache type CDATA #IMPLIED eviction CDATA #IMPLIED flushInterval CDATA #IMPLIED size CDATA #IMPLIED readOnly CDATA #IMPLIED blocking CDATA #IMPLIED >
缓存的解析也是位于 XMLMapperBuilder类中的 cacheElement 方法,我们看下源码:
private void cacheElement(XNode context) { if (context != null) { // 获取缓存配置各种属性 String type = context.getStringAttribute("type", "PERPETUAL"); Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); String eviction = context.getStringAttribute("eviction", "LRU"); Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); Long flushInterval = context.getLongAttribute("flushInterval"); Integer size = context.getIntAttribute("size"); boolean readWrite = !context.getBooleanAttribute("readOnly", false); boolean blocking = context.getBooleanAttribute("blocking", false); // 获取缓存下的 property 属性配置 Properties props = context.getChildrenAsProperties(); // 构建缓存 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } }
我们可以看到在获取属性值的过程中就会有默认的属性值了,比如 type 默认的就是 PERPETUAL ,淘汰策略默认的就是 LRU,那我们就继续看下构建缓存的过程。
2.3.1 useNewCache 缓存构建
缓存的构建是通过 XMLMapperBuilder 中的 builderAssistant 进行构建的,这个属性的赋值是通过创建 XMLMapperBuilder 对象的时候初始化默认的构建器来进行的。
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) { super(configuration); // 赋值默认的构建器 this.builderAssistant = new MapperBuilderAssistant(configuration, resource); this.parser = parser; this.sqlFragments = sqlFragments; this.resource = resource; }
那我们看下 MapperBuilderAssistant 的 useNewCache 方法看下如何构建缓存的:
/** * 通过 CacheBuilder 建造器来构建的 */ public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { /** * 可以知道 每个 mapper 的缓存的 id值 就是mapper上的 namespace名字 * public CacheBuilder(String id) {} */ Cache cache = new CacheBuilder(currentNamespace) // 又是初始化默认值 缓存类型PerpetualCache .implementation(valueOrDefault(typeClass, PerpetualCache.class)) // 淘汰策略 LruCache .addDecorator(valueOrDefault(evictionClass, LruCache.class)) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .blocking(blocking) .properties(props) .build(); /** * 把当前 mapper 的缓存放进 configuration 中 * caches.put(cache.getId(), cache); * protected final Map<String, Cache> caches = new Configuration.StrictMap<>("Caches collection"); */ configuration.addCache(cache); currentCache = cache; return cache; }
可以看到确实是当我们 mapper 里有缓存的标签了,configuration才会有当前 mapper 的缓存,我们加下来看看 build() 里又做了些什么。
2.3.2 build 缓存构建中的初始化
MapperBuilderAssistant 中的缓存创建时通过 CacheBuilder 进行创建的,那么我们看下 build 里做了些什么:
public Cache build() { /** * 又是再一次的初始化 表示理解哈 防止空指针我懂 * 设置默认的PerpetualCache、LruCache * private void setDefaultImplementations() { * if (implementation == null) { * implementation = PerpetualCache.class; * if (decorators.isEmpty()) { * decorators.add(LruCache.class); * } * } * } */ setDefaultImplementations(); // 这里就是根据提供的缓存类,进行反射创建的 Cache cache = newBaseCacheInstance(implementation, id); /** * 设置缓存的配置属性值 常用于第三方缓存的一些配置 比如你用的 EhcacheCache 配置的 timeToIdleSeconds一些属性值 * 这个方法就是对属性值进行类型转换 并设置的 */ setCacheProperties(cache); // 如果当前的缓存用的时默认的PerpetualCache if (PerpetualCache.class.equals(cache.getClass())) { /** * private final List<Class<? extends Cache>> decorators; * decorators 是缓存的装饰器 也就是把缓存外边包装一层 */ for (Class<? extends Cache> decorator : decorators) { cache = newCacheDecoratorInstance(decorator, cache); // 设置属性值 setCacheProperties(cache); } // 应用标准的装饰器,比如 LoggingCache、SynchronizedCache cache = setStandardDecorators(cache); } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) { // 应用具有日志功能的缓存装饰器 cache = new LoggingCache(cache); } return cache; }
里边东西其实不是很复杂,我们大致总结一下整体的过程:
- 创建了 XMLMapperBuilder 对象(会初始化属性builderAssistant=MapperBuilderAssistant),用于解析我们的 mapper 信息;
- 通过 builderAssistant 也就是 MapperBuilderAssistant 构建我们的缓存, 当然前提是 mapper 里配置了 cache 才会去构建;
- MapperBuilderAssistant 又是借助于 CacheBuilder 通过建造器模式进行建造;
- CacheBuilder 的build 过程中对缓存的一些属性值进行填充和装饰。
好了,大概就是我们的缓存解析的一个过程了,具体一些小方法我就不罗列了哈。
2.4 解析 <cache-ref> 节点
对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新。 但你可能会想要在多个命名空间中共享相同的缓存配置和实例。要实现这种需求,你可以使用 cache-ref 元素来引用另一个缓存。
<cache-ref namespace="com.someone.application.data.SomeMapper"/>
我们来看下解析的过程:
private void cacheRefElement(XNode context) { if (context != null) { /** * configuration 中添加映射关系 key是当前 mapper 的名称空间的名字 val 是 cache-ref上引用的名称空间的名字 * public void addCacheRef(String namespace, String referencedNamespace) { * cacheRefMap.put(namespace, referencedNamespace); * } */ configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace")); // 创建一个CacheRefResolver对象 CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace")); try { // CacheRefResolver对象进行解析 cacheRefResolver.resolveCacheRef(); } catch (IncompleteElementException e) { /** * 抛异常的话 把它放进configuration的未完全加载的缓存中 * public void addIncompleteCacheRef(CacheRefResolver incompleteCacheRef) { * incompleteCacheRefs.add(incompleteCacheRef); * } */ configuration.addIncompleteCacheRef(cacheRefResolver); } } }
CacheRefResolver这个对象其实很简单,它的 resolveCacheRef 方法其实还是通过传参的 builderAssistant 缓存构造器进行构建的,我们简单看下 CacheRefResolver 的内容:
public class CacheRefResolver { private final MapperBuilderAssistant assistant; private final String cacheRefNamespace; public CacheRefResolver(MapperBuilderAssistant assistant, String cacheRefNamespace) { this.assistant = assistant; this.cacheRefNamespace = cacheRefNamespace; } public Cache resolveCacheRef() { return assistant.useCacheRef(cacheRefNamespace); } }
那么 assistant.useCacheRef,其实就是相当于 MapperBuilderAssistant.useCacheRef,我们来看下源码:
/** * 首先方法的入参 namespace 是要引用的缓存 * 比如 mapper1 里有 cache-ref 引用的 mapper2中的缓存 * 那么当前的 namespace 值就是 mapper2的名称空间 * @param namespace * @return */ public Cache useCacheRef(String namespace) { // 名称空间为空的话 直接报错 if (namespace == null) { throw new BuilderException("cache-ref element requires a namespace attribute."); } try { // 将当前的 unresolvedCacheRef 设置为true unresolvedCacheRef = true; // 判断 configuration 中是否存在 namespace的缓存 Cache cache = configuration.getCache(namespace); /** * 没有的话 直接报错 也就是说被引用的要先加载进去,后边的才能引用 * 比如 mapper1 中引用了 mapper2 的缓存,那么 mapper2 要先加载
* 这里的报错 假如正好 mapper2 还没加载报异常 正好被捕获放进incompleteCacheRefs 咦前后呼应上了 */ if (cache == null) { throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found."); } // 这个 currentCache 具体是用来干啥的 也不是很清楚 我们后续再看 currentCache = cache; // 再将 unresolvedCacheRef 设置为 false 咦 两个咱没看懂 是为了个啥意思= = unresolvedCacheRef = false; return cache; } catch (IllegalArgumentException e) { throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e); } }
我们可以看到解析 cache-ref 其实就是 将这种映射关系保存到 configuration 中,再判断一下被引用的缓存是否已经创建,好了关于解析 cache-ref 就到这里了。
3 小结
我们的 mapper 解析还有 resultMap、sql、增删改查语句没说,因为内容比较多,所以我们放到下节再说哈,有不对的地方欢迎指正哈。
标签:mapper,缓存,配置文件,cache,namespace,configuration,Mybatis,解析 From: https://www.cnblogs.com/kukuxjx/p/17156875.html