重拾-MyBatis-配置文件解析

前言

我们知道在使用 Mybatis 时,我们需要通过 SqlSessionFactoryBuild 去创建 SqlSessionFactory 实例,譬如:

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

那么我们看下 build 方法的具体实现

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, mappers

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 方法传参譬如:

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,比如:

<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 两种配置方式进行了不同的解析。 下面我们先看下通过包名的配置方式

通过包名解析
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;若有注解,则别名为其注解值。

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 集合中。

配置环境 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-配置文件解析

相关推荐