MyBatis源码系列:MyBatis解析配置文件、二级缓存、SQL

转载 作者:来者不拒 更新时间:2024-01-31 08:59:04 24 4

解析全局配置文件

启动流程分析 。

String resource = "mybatis-config.xml";
//将XML配置文件构建为Configuration配置类
reader = Resources.getResourceAsReader(resource);
// 通过加载配置文件流构建一个SqlSessionFactory  DefaultSqlSessionFactory
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);

通过上面代码发现,创建SqlSessionFactory的代码在SqlSessionFactoryBuilder中,进去一探究竟:

//整个过程就是将配置文件解析成Configration对象,然后创建SqlSessionFactory的过程
//Configuration是SqlSessionFactory的一个内部属性 
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        if (reader != null) {
          reader.close();
        }
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

下面是解析配置文件的核心方法:

  public Configuration parse() {

    //若已经解析过了 就抛出异常
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    /**
     * 解析我们的mybatis-config.xml的
     * 节点
     * <configuration>
     * </configuration>
     */
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  /**
   * 解析我们mybatis-config.xml的 configuration节点
   * @param root
   */
  private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      /**
       * 解析 properties节点
       *     <properties resource="db.properties" />
       *     解析到org.apache.ibatis.parsing.XPathParser#variables
       *          org.apache.ibatis.session.Configuration#variables
       */
      propertiesElement(root.evalNode("properties"));
      /**
       * 解析我们的mybatis-config.xml中的settings节点
       * 具体可以配置哪些属性:http://www.mybatis.org/mybatis-3/zh/configuration.html#settings
       * <settings>
       <setting name="cacheEnabled" value="true"/>
       <setting name="lazyLoadingEnabled" value="true"/>
       <setting name="mapUnderscoreToCamelCase" value="false"/>
       <setting name="localCacheScope" value="SESSION"/>
       <setting name="jdbcTypeForNull" value="OTHER"/>
       ..............
       </settings>
       *
       */
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      /**
       * 基本没有用过该属性
       * VFS含义是虚拟文件系统;主要是通过程序能够方便读取本地文件系统、FTP文件系统等系统中的文件资源。
       Mybatis中提供了VFS这个配置,主要是通过该配置可以加载自定义的虚拟文件系统应用程序
       解析到:org.apache.ibatis.session.Configuration#vfsImpl
       */
      loadCustomVfsImpl(settings);
      /**
       * 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
       * SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
       * 解析到org.apache.ibatis.session.Configuration#logImpl
       */
      loadCustomLogImpl(settings);
      /**
       * 解析我们的别名
       * <typeAliases>
       <typeAlias alias="User" type="com.mcode.entity.User"/>
       </typeAliases>
       <typeAliases>
       <package name="com.mcode.entity"/>
       </typeAliases>
       解析到oorg.apache.ibatis.session.Configuration#typeAliasRegistry.typeAliases
       除了自定义的,还有内置的
       */
      typeAliasesElement(root.evalNode("typeAliases"));
      /**
       * 解析我们的插件(比如分页插件)
       * mybatis自带的
       * Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
       ParameterHandler (getParameterObject, setParameters)
       ResultSetHandler (handleResultSets, handleOutputParameters)
       StatementHandler (prepare, parameterize, batch, update, query)
       解析到:org.apache.ibatis.session.Configuration#interceptorChain.interceptors
       */
      pluginsElement(root.evalNode("plugins"));
      /**
       * 可以配置  一般不会去设置
       * 对象工厂 用于反射实例化对象、对象包装工厂、
       * 反射工厂 用于属性和setter/getter 获取
       */
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));

      // 设置settings 和默认值到configuration
      settingsElement(settings);

      /**
       * 解析我们的mybatis环境
       <environments default="dev">
       <environment id="dev">
       <transactionManager type="JDBC"/>
       <dataSource type="POOLED">
       <property name="driver" value="${jdbc.driver}"/>
       <property name="url" value="${jdbc.url}"/>
       <property name="username" value="root"/>
       <property name="password" value="Zw726515"/>
       </dataSource>
       </environment>
       </environments>
       *  解析到:org.apache.ibatis.session.Configuration#environment
       *  在集成spring情况下由 spring-mybatis提供数据源 和事务工厂
       */
      environmentsElement(root.evalNode("environments"));
      /**
       * 解析数据库厂商
       *     <databaseIdProvider type="DB_VENDOR">
       <property name="SQL Server" value="sqlserver"/>
       <property name="DB2" value="db2"/>
       <property name="Oracle" value="oracle" />
       <property name="MySql" value="mysql" />
       </databaseIdProvider>
       *  解析到:org.apache.ibatis.session.Configuration#databaseId
       */
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      /**
       * 解析我们的类型处理器节点
       * <typeHandlers>
       <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
       </typeHandlers>
       解析到:org.apache.ibatis.session.Configuration#typeHandlerRegistry.typeHandlerMap
       */
      typeHandlersElement(root.evalNode("typeHandlers"));
      /**
       * 解析我们的mapper
       *
       resource:来注册我们的class类路径下的
       url:来指定我们磁盘下的或者网络资源的
       class:
       若注册Mapper不带xml文件的,这里可以直接注册
       若注册的Mapper带xml文件的,需要把xml文件和mapper文件同名 同路径
       -->
       <mappers>
       <mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
       <mapper class="com.mcode.mapper.EmployeeMapper"></mapper>


       <package name="com.mcode.mapper"></package>
       -->
       </mappers>
       * package
       *     ·解析mapper接口代理工厂(传入需要代理的接口) 解析到:org.apache.ibatis.session.Configuration#mapperRegistry.knownMappers
       ·解析mapper.xml  最终解析成MappedStatement 到:org.apache.ibatis.session.Configuration#mappedStatements
       */
      mappersElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

image

上面解析流程结束后会生成一个Configration对象,包含所有配置信息,然后会创建一个SqlSessionFactory对象,这个对象包含了Configration对象.

简单总结 。

对于MyBatis启动的流程(获取SqlSession的过程)这边简单总结下:

  • SqlSessionFactoryBuilder解析配置文件,包括属性配置、别名配置、拦截器配置、环境(数据源和事务管理器)、Mapper配置等;解析完这些配置后会生成一个Configration对象,这个对象中包含了MyBatis需要的所有配置,然后会用这个Configration对象创建一个SqlSessionFactory对象,这个对象中包含了Configration对象;

image

二级缓存解析

解析二级缓存

解析mapper文件 。

  private void mappersElement(XNode context) throws Exception {
    if (context == null) {
      return;
    }
    /**
     * 获取我们mappers节点下的一个一个的mapper节点
     */
    for (XNode child : context.getChildren()) {
      /**
       * 判断我们mapper是不是通过批量注册的
       * <package name="com.mcode.mapper"></package>
       */
      if ("package".equals(child.getName())) {
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        /**
         * 判断从classpath下读取我们的mapper
         * <mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
         */
        String resource = child.getStringAttribute("resource");
        /**
         * 判断是不是从我们的网络资源读取(或者本地磁盘得)
         * <mapper url="D:/mapper/EmployeeMapper.xml"/>
         */
        String url = child.getStringAttribute("url");
        /**
         * 解析这种类型(要求接口和xml在同一个包下)
         * <mapper class="com.mcode.mapper.DeptMapper"></mapper>
         *
         */
        String mapperClass = child.getStringAttribute("class");
        /**
         * 我们得mappers节点只配置了
         * <mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
         */
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          /**
           * 把我们的文件读取出一个流
           */
          try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
            /**
             * 创建读取XmlMapper构建器对象,用于来解析我们的mapper.xml文件
             */
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
                configuration.getSqlFragments());
            /**
             * 真正的解析我们的mapper.xml配置文件(说白了就是来解析我们的sql)
             */
            mapperParser.parse();
          }
        } else if (resource == null && url != null && mapperClass == null) {        
          ErrorContext.instance().resource(url);
          try (InputStream inputStream = Resources.getUrlAsStream(url)) {
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url,
                configuration.getSqlFragments());
            mapperParser.parse();
          }
        } 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 。

  /**
   * 真正的去解析我们的Mapper.xml(EmployeeMapper.xml)
   */
  public void parse() {
    /**
     * 判断当前的Mapper是否被加载过
     */
    if (!configuration.isResourceLoaded(resource)) {
      /**
       * 真正的解析我们的 <mapper namespace="com.mcode.mapper.EmployeeMapper">
       *
       */
      configurationElement(parser.evalNode("/mapper"));
      /**
       * 把资源保存到我们Configuration中
       */
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

  public XNode getSqlFragment(String refid) {
    return sqlFragments.get(refid);
  }

  /**
   * 解析我们的<mapper></mapper>节点
   * @param context
   */
  private void configurationElement(XNode context) {
    try {
      /**
       * 解析我们的namespace属性
       * <mapper namespace="com.cmode.mapper.EmployeeMapper">
       */
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.isEmpty()) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      /**
       * 保存我们当前的namespace  并且判断接口完全类名==namespace
       */
      builderAssistant.setCurrentNamespace(namespace);
      /**
       * 解析我们的缓存引用
       * 说明我当前的缓存引用和DeptMapper的缓存引用一致
       * <cache-ref namespace="com.tuling.mapper.DeptMapper"></cache-ref>
       解析到org.apache.ibatis.session.Configuration#cacheRefMap<当前namespace,ref-namespace>
       异常下(引用缓存未使用缓存):org.apache.ibatis.session.Configuration#incompleteCacheRefs
       */
      cacheRefElement(context.evalNode("cache-ref"));
      /**
       * 解析我们的cache节点
       * <cache ></cache>
       解析到:org.apache.ibatis.session.Configuration#caches
       org.apache.ibatis.builder.MapperBuilderAssistant#currentCache
       */
      cacheElement(context.evalNode("cache"));
      /**
       * 解析paramterMap节点(该节点mybaits3.5貌似不推荐使用了)
       */
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      /**
       * 解析我们的resultMap节点
       * 解析到:org.apache.ibatis.session.Configuration#resultMaps
       *    异常 org.apache.ibatis.session.Configuration#incompleteResultMaps
       *
       */
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      /**
       * 解析我们通过sql片段
       *  解析到org.apache.ibatis.builder.xml.XMLMapperBuilder#sqlFragments
       *   其实等于 org.apache.ibatis.session.Configuration#sqlFragments
       *   因为他们是同一引用,在构建XMLMapperBuilder 时把Configuration.getSqlFragments传进去了
       */
      sqlElement(context.evalNodes("/mapper/sql"));
      /**
       * 解析我们的select | insert |update |delete节点
       * 解析到org.apache.ibatis.session.Configuration#mappedStatements
       */
      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);
    }
  }

解析Cache 。

  /**
   * 解析cache
   * @param context
   */
  private void cacheElement(XNode context) {
    if (context != null) {
      //解析cache节点的type属性
      String type = context.getStringAttribute("type", "PERPETUAL");
      // 根据别名(或完整限定名)  加载为Class
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        /*获取缓存过期策略:默认是LRU
      LRU – 最近最少使用:移除最长时间不被使用的对象。(默认)
      FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
      SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
      WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
      */
      String eviction = context.getStringAttribute("eviction", "LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      //flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
      Long flushInterval = context.getLongAttribute("flushInterval");
      //size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
      Integer size = context.getIntAttribute("size");
      //只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      boolean blocking = context.getBooleanAttribute("blocking", false);
      Properties props = context.getChildrenAsProperties();
      //把缓存节点加入到Configuration中
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }

image

缓存中的调用过程