首页 > 编程语言 >【Mybatis】【配置文件解析】【四】Mybatis源码解析-mappers的解析一

【Mybatis】【配置文件解析】【四】Mybatis源码解析-mappers的解析一

时间:2023-02-26 18:25:20浏览次数:43  
标签:mapper 缓存 配置文件 cache namespace configuration Mybatis 解析

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();
}

如上,映射文件解析入口逻辑包含三个核心操作,分别如下:

  1. 解析 mapper 节点
  2. 通过命名空间绑定 Mapper 接口
  3. 处理未完成解析的节点

我们再来熟悉下 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;
}

里边东西其实不是很复杂,我们大致总结一下整体的过程:

  1. 创建了 XMLMapperBuilder 对象(会初始化属性builderAssistant=MapperBuilderAssistant),用于解析我们的 mapper 信息;
  2. 通过 builderAssistant 也就是 MapperBuilderAssistant 构建我们的缓存, 当然前提是 mapper 里配置了 cache 才会去构建;
  3. MapperBuilderAssistant 又是借助于 CacheBuilder 通过建造器模式进行建造;
  4. 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

相关文章