重拾-MyBatis-配置文件解析

前言

咱們知道在使用 Mybatis 時,咱們須要經過 SqlSessionFactoryBuild 去建立 SqlSessionFactory 實例,譬如:java

// resource 爲 mybatis 的配置文件 
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

那麼咱們看下 build 方法的具體實現sql

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
        // 建立 XMLConfigBuilder 實例並執行解析
      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 {
        reader.close();
      } catch (IOException e) {

      }
    }
}

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

Mybatis 主要經過 XMLConfigBuilder 執行對配置文件的解析,具體實現以下文:數據庫

配置文件解析

private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      // 解析 properties 標籤
      propertiesElement(root.evalNode("properties"));
      // 解析 settings 標籤
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      // 解析 typeAliases 別名標籤
      typeAliasesElement(root.evalNode("typeAliases"));
      // 解析 plugins 插件標籤
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      // 解析 environments 標籤
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      // 解析 typeHandlers 標籤
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析 mappers 標籤
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

XMLConfigBuilder 的方法 parseConfiguration 實現咱們知道,MyBatis 會依次解析配置文件中的相應標籤,本文將針對開發中經常使用的配置進行分析;主要包括 properties, typeAliases, enviroments, typeHandlers, mappersapache

properties 解析

配置示例

<configuration>
    <!-- 能夠指定 resource 屬性,也能夠指定 url 屬性 -->
    <properties resource="org/mybatis/example/config.properties">
          <property name="username" value="dev_user"/>
          <property name="password" value="F2Fa3!33TYyg"/>
    </properties>

</configuration>

從配置示例能夠看出 properties 屬性變量的來源能夠是外部的配置文件,也能夠是配置文件中自定義的,也能夠是 SqlSessionFactoryBuilderbuild 方法傳參譬如:session

public SqlSessionFactory build(InputStream inputStream, Properties properties) {
    return build(inputStream, null, properties);
  }
那麼當存在同名的屬性時,將採用哪一種方式的屬性值呢?

解析

private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
      // 獲取 properties 標籤下的全部 property 子標籤
      Properties defaults = context.getChildrenAsProperties();
      // 獲取 resource,url 屬性
      String resource = context.getStringAttribute("resource");
      String url = context.getStringAttribute("url");

      // resource url 兩個屬性不能同時存在
      if (resource != null && url != null) {
        throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
      }
      if (resource != null) {
        // 加載 resource 指定的配置文件
        defaults.putAll(Resources.getResourceAsProperties(resource));
      } else if (url != null) {
        // 加載 url 指定的配置文件
        defaults.putAll(Resources.getUrlAsProperties(url));
      }

      /**
       * 獲取傳參的 properties
       * 構建 sqlSessionFactory 時能夠傳參 properties
       *
       * @see SqlSessionFactoryBuilder.build(InputStream inputStream, Properties properties)
       */
      Properties vars = configuration.getVariables();
      if (vars != null) {
        defaults.putAll(vars);
      }
      parser.setVariables(defaults);
      // 將 properties 賦值 configuration 中的 variables 變量
      configuration.setVariables(defaults);
    }
  }
public Properties getChildrenAsProperties() {
    Properties properties = new Properties();
    // 遍歷 properties 標籤下的 propertry 子標籤
    for (XNode child : getChildren()) {
      // 獲取 propertry 的 name value 屬性
      String name = child.getStringAttribute("name");
      String value = child.getStringAttribute("value");
      if (name != null && value != null) {
        properties.setProperty(name, value);
      }
    }
    return properties;
  }

properties 標籤解析的實現來看,MyBatis 加載 properties 屬性的過程以下:數據結構

  • 首先加載 properties 標籤內全部子標籤的 property
  • 其次加載 properties 標籤屬性 resourceurl 指定的外部屬性配置
  • 最後加載 SqlSessionFactoryBuilder 的方法 build 傳參的屬性配置
所以,經過方法參數傳遞的 properties 具備最高優先級,resource/url 屬性中指定的配置文件次之,最低優先級的是 properties 標籤內的子標籤 property 指定的屬性。

typeAliases 解析

類型別名是爲 Java 類型設置一個短的名字。它只和 XML 配置有關,存在的意義僅在於用來減小類徹底限定名的冗餘

配置示例

<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
  <typeAlias alias="Blog" type="domain.blog.Blog"/>
  <typeAlias alias="Comment" type="domain.blog.Comment"/>
</typeAliases>

也能夠指定一個包名,MyBatis 會在包名下面搜索須要的 Java Bean,好比:mybatis

<typeAliases>
  <package name="domain.blog"/>
</typeAliases>

解析

private void typeAliasesElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 若是是 package 標籤,對整個包下的 java bean 進行別名處理
        // 若 java bean 沒有配置註解的話,使用 bean 的首字母小寫類名做爲別名
        // 若 java bean 配置了註解,使用註解值做爲別名
        if ("package".equals(child.getName())) {
          // 獲取指定的包名
          String typeAliasPackage = child.getStringAttribute("name");
          configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
        } else {
          // 別名
          String alias = child.getStringAttribute("alias");
          // 別名對應的類
          String type = child.getStringAttribute("type");
          try {
            Class<?> clazz = Resources.classForName(type);
            if (alias == null) {
              // 默認別名爲類名,若配置了別名註解則取註解值映射類
              typeAliasRegistry.registerAlias(clazz);
            } else {
              // 經過指定的別名映射類
              typeAliasRegistry.registerAlias(alias, clazz);
            }
          } catch (ClassNotFoundException e) {
            throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
          }
        }
      }
    }
  }

typeAliasesElement 在對 typeAliases 標籤解析時,針對採用 packagetypeAlias 兩種配置方式進行了不一樣的解析。 下面咱們先看下經過包名的配置方式app

經過包名解析
public void registerAliases(String packageName) {
    registerAliases(packageName, Object.class);
  }

  public void registerAliases(String packageName, Class<?> superType) {
      // 獲取包下全部的類
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
    for (Class<?> type : typeSet) {
      // Ignore inner classes and interfaces (including package-info.java)
      // Skip also inner classes. See issue #6
      // 忽略內部類 接口
      if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
        registerAlias(type);
      }
    }
  }

  public void registerAlias(Class<?> type) {
    // 別名爲類名
    String alias = type.getSimpleName();
    // 是否配置了別名註解,若配置了則別名取註解值
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    if (aliasAnnotation != null) {
      alias = aliasAnnotation.value();
    }
    registerAlias(alias, type);
  }

當經過 package 指定包名時,MyBatis 會掃描包下全部的類(忽略內部類,接口),若類沒有采用 @Alias 註解的狀況下,會使用 Bean 的首字母小寫的非限定類名來做爲它的別名, 好比 domain.blog.Author 的別名爲 author;如有註解,則別名爲其註解值。dom

public void registerAlias(String alias, Class<?> value) {
    if (alias == null) {
      throw new TypeException("The parameter alias cannot be null");
    }
    // issue #748
    // 別名小寫處理
    String key = alias.toLowerCase(Locale.ENGLISH);
    if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
      throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
    }
    // 別名與類映射
    typeAliases.put(key, value);
  }

在完成別名的解析以後會將其註冊到 typeAliasRegistry 的變量 typeAliases Map 集合中。ide

配置環境 environments 解析

environments 用於事務管理器及數據源相關配置

配置示例

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
      <property name="..." value="..."/>
    </transactionManager>
    <dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>
  </environment>
  <environment id="test">
    <transactionManager type="JDBC">
      <property name="..." value="..."/>
    </transactionManager>
    <dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>
  </environment>
</environments>
environments 的配置來看 MyBatis 是支持多數據源的,但每一個 SqlSessionFactory 實例只能選擇其中一個; 若須要鏈接多個數據庫,就得須要建立多個 SqlSessinFactory 實例。

解析

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
        /**
         * @see org.apache.ibatis.session.SqlSessionFactoryBuilder.build 時未指定 enviorment, 則取默認的
         */
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        // 查找與 environment 匹配的配置環境
        if (isSpecifiedEnvironment(id)) {
          // 解析事務管理
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          // 解析數據源
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          // 獲取數據源實例
          DataSource dataSource = dsFactory.getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          // 設置配置環境
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }
private boolean isSpecifiedEnvironment(String id) {
    if (environment == null) {
        // 若 environment 爲空說明未指定當前 SqlSessionFactory 實例所需的配置環境;同時 environments 標籤未配置 default 屬性
      throw new BuilderException("No environment specified.");
    } else if (id == null) {
        // environment 標籤須要配置 id 屬性
      throw new BuilderException("Environment requires an id attribute.");
    } else if (environment.equals(id)) {
        // environment == id 說明當前匹配配置環境
      return true;
    }
    return false;
  }

environments 支持多數據源的配置,因此在解析時會先查找匹配當前 SqlSessionFactoryenvironment; 而後在解析當前配置環境所需的事務管理器和數據源。

事務管理器解析
private TransactionFactory transactionManagerElement(XNode context) throws Exception {
    if (context != null) {
      // 獲取配置事務管理器的類別,也就是別名
      String type = context.getStringAttribute("type");
      // 獲取事務屬性配置 
      Properties props = context.getChildrenAsProperties();
      // 經過別名查找對應的事務管理器類並實例化
      TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance();
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a TransactionFactory.");
  }

事務管理器解析時會經過配置中指定的 type 別名去查找對應的 TransactionFactory 並實例化。

那麼 MyBatis 內部內置了哪些事務管理器呢?
public Configuration() {
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

    // 省略
  }

Configuration 的構造能夠看出,其構造時會經過 typeAliasRegistry 註冊了別名爲 JDBC,MANAGED 的兩種事務管理器。

數據源解析
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    if (context != null) {
        // 獲取配置數據源的類別,也就是別名
      String type = context.getStringAttribute("type");
      // 獲取數據源屬性配置
      Properties props = context.getChildrenAsProperties();
      // 經過別名查找數據源並實例化
      DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a DataSourceFactory.");
  }

同事務管理器同樣,數據源解析時也會經過指定的別名查找對應的數據源實現類一樣其在 Configuration 構造時向 typeAliasRegistry 註冊了三種數據源

public Configuration() {
    
    // 省略

    typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
    typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);

    // 省略
  }

類型轉換器 typeHandlers 解析

配置示例

<typeHandlers>
  <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
<typeHandlers>
  <package name="org.mybatis.example"/>
</typeHandlers>

解析

private void typeHandlerElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String typeHandlerPackage = child.getStringAttribute("name");
          typeHandlerRegistry.register(typeHandlerPackage);
        } else {
          // 映射 java 對象類型
          String javaTypeName = child.getStringAttribute("javaType");
          // 映射 jdbc 類型
          String jdbcTypeName = child.getStringAttribute("jdbcType");
          // 類型轉換器類名
          String handlerTypeName = child.getStringAttribute("handler");

          Class<?> javaTypeClass = resolveClass(javaTypeName);
          JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
          Class<?> typeHandlerClass = resolveClass(handlerTypeName);
          if (javaTypeClass != null) {
            if (jdbcType == null) {
              // 指定了 java type,未指定 jdbc type
              typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
            } else {
              // 指定了 java type,指定了 jdbc type
              typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
            }
          } else {
              // 未指定 java type 按 typeHandlerClass 註冊
            typeHandlerRegistry.register(typeHandlerClass);
          }
        }
      }
    }
  }
typeHandler 解析
指定 javaType 和 jdbcType
public void register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass) {
    register(javaTypeClass, jdbcType, getInstance(javaTypeClass, typeHandlerClass));
  }
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
      // 一個 java type 可能會映射多個 jdbc type
      Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
      if (map == null || map == NULL_TYPE_HANDLER_MAP) {
        map = new HashMap<>();
        typeHandlerMap.put(javaType, map);
      }
      map.put(jdbcType, handler);
    }

    // 存儲 typeHandler
    allTypeHandlersMap.put(handler.getClass(), handler);
  }
當指定了 javaTypejdbcType 最終會將兩者及 typeHandler 映射並註冊到 typeHandlerMap 中,從 typeHandlerMap 的數據結構來看, javaType 可能會與多個 jdbcType 映射。 譬如 String -> CHAR, VARCHAR
指定 javaType 未指定 jdbcType
public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
    // 將 type handler 實例化
    register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
  }
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
    // 獲取 MappedJdbcTypes 註解
    // 該註解用於設置類型轉換器匹配的 jdbcType
    MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
    if (mappedJdbcTypes != null) {
        // 遍歷匹配的 jdbcType 並註冊
      for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
        register(javaType, handledJdbcType, typeHandler);
      }
      if (mappedJdbcTypes.includeNullJdbcType()) {
        register(javaType, null, typeHandler);
      }
    } else {
      // 未指定 jdbcType 時按 null 處理    
      register(javaType, null, typeHandler);
    }
  }
當類型轉換器配置了 javaType 未配置 jdbcType 時,會判斷類型轉換器是否配置了 @MappedJdbcTypes 註解; 若配置了則使用註解值做爲 jdbcType 並註冊,若未配置則按 null 註冊。
未指定 javaType 和 jdbcType
public void register(Class<?> typeHandlerClass) {
    boolean mappedTypeFound = false;
    // 獲取 MappedTypes 註解
    // 該註解用於設置類型轉換器匹配的 javaType
    MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
      for (Class<?> javaTypeClass : mappedTypes.value()) {
          // 執行註冊
        register(javaTypeClass, typeHandlerClass);
        mappedTypeFound = true;
      }
    }
    if (!mappedTypeFound) {
      register(getInstance(null, typeHandlerClass));
    }
  }
javaType, jdbcType 均爲指定時,會判斷類型轉換器是否配置了 @MappedTypes 註解; 若配置了則使用註解值做爲 javaType 並註冊。
package 解析
public void register(String packageName) {
    // 掃描指定包下的全部類
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
    Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
    for (Class<?> type : handlerSet) {
      //Ignore inner classes and interfaces (including package-info.java) and abstract classes
        // 忽略內部類 接口 抽象類
      if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
          // 執行註冊
        register(type);
      }
    }
  }
當按指定包名解析時,會掃描包下的全部類(忽略內部類,接口,抽象類)並執行註冊

小結

本文咱們主要分析了 Mybatis 配置文件中標籤 properties,typeAliases,enviroments,typeHandlers 的解析過程,因爲 mappers 的解析比較複雜後續在進行分析;經過本文的分析咱們瞭解到 Configuration 實例中包括如下內容:

  • variables : Properties 類型,存儲屬性變量
  • typeAliasRegistry : 別名註冊中心,經過一個 Map 集合變量 typeAliases 存儲別名與類的映射關係
  • environment : 配置環境,綁定事務管理器和當前數據源
  • typeHandlerRegistry : 類型轉換器註冊中心,存儲 javaTypejdbcType,typeHandler 的映射關係,內置 jdbcTypetypeHandler 的映射關係

mybatis-configuration

相關文章
相關標籤/搜索