重拾-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 属性的过程以下:mybatis

  • 首先加载 properties 标签内全部子标签的 property
  • 其次加载 properties 标签属性 resourceurl 指定的外部属性配置
  • 最后加载 SqlSessionFactoryBuilder 的方法 build 传参的属性配置

所以,经过方法参数传递的 properties 具备最高优先级,resource/url 属性中指定的配置文件次之,最低优先级的是 properties 标签内的子标签 property 指定的属性。app

typeAliases 解析

类型别名是为 Java 类型设置一个短的名字。它只和 XML 配置有关,存在的意义仅在于用来减小类彻底限定名的冗余dom

配置示例

<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,好比:ide

<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-configuration
相关文章
相关标签/搜索