深入 MyBatis 源码系列 (三) 解析配置文件过程分析

本贴最后更新于 1717 天前,其中的信息可能已经时过境迁

继续上篇的 MyBatis 配置文件解析,上篇文章讲了如何解析以及解析执行的入口等,下面我们开始详细的阐述这部分解析的过程

1. 解析 Properties

properties 是我们预设的配置项,可以使用以下的形式配置. 具体的配置内容请查看: mybatis 核心配置文件 properties 元素

  • 引入 properties 配置文件
<properties resource="xxxxx.properties" url="xxxxxx"/>
  • 直接记录文件的配置 K-V
  <properties>
    <property name="KEY1" value="VALUE1"/>
    <property name="KEY2" value="VALUE2"/>
    <property name="KEY3" value="VALUE3"/>
  </properties>

基于这两种配置的情况,来看一下 MyBatis 是如何集合这两种配置问题,那么我们携带着问题去看:

  • 如果在引入外部配置文件的时候指定了 resource 又指定 url,会怎么样呢?

  • 如果 properties 标签指定了 resource 并且 内部也配置了 property 那么会怎么样呢?

源码中解析 Properties 的代码如下:

private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
      // 首先获取<properties/>表中的<property>列表并成Properties对象
      Properties defaults = context.getChildrenAsProperties();
      // 获取 properties的resource和url
      String resource = context.getStringAttribute("resource");
      String url = context.getStringAttribute("url");
      // Question_1: 如果同时配置了,那么则会抛出异常
      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.");
      }
      // 如果指定了resource或者url,那么从下面的代码会解析这个文件,然后添加到defaults中
      // defaults是<property>中的配置,相当于在原来的配置基础上再次添加了一些配置
      // Question2: 如果都配置的话,那么两者均生效
      if (resource != null) {
        defaults.putAll(Resources.getResourceAsProperties(resource));
      } else if (url != null) {
        defaults.putAll(Resources.getUrlAsProperties(url));
      }
      Properties vars = configuration.getVariables();
      if (vars != null) {
        defaults.putAll(vars);
      }
      // 最终的结果会保存在configuration中,configuration是一个非常重要的资源对象。贯穿整个MyBaits
      parser.setVariables(defaults);
      configuration.setVariables(defaults);
    }
  }

2. 解析 Environments

作为一个 ORM 框架,MyBatis 肯定需要配置环境,因此可以使用标签配置多个环境,例如:开发环境,生产环境以及测试环境。配置如下:

  <!--  配置环境,包含数据源等信息 -->
  <environments default="development">
    
    <!-- 开发环境    -->
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/solo?useAffectedRows=true"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
      </dataSource>
    </environment>
    
    <!-- 测试环境   -->
    <environment id="test">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/solo?useAffectedRows=true"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
      </dataSource>
    </environment>
  </environments>

<environments> 标签中 default 属性指定了默认的环境为 development 那么下面的 id = development 的环境将会生效,源码解析部分如下:

  private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      // 如果环境没有配置,将获取默认的环境
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      // 遍历其中的所有环境,找到id等于上面的default指定的环境,然后解析该环境
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        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());
        }
      }
    }
  }

不管是 transactionManagerElement() 方法还是 dataSourceElement() 均是通过其标签属性 type 来获取到 class 文件通过反射构造实例,然后将第一步解析的 Properties 设置进入。这里可以对比参考一下:

  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.");
  }

  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 中,因此再一次重申 configuration 是一个非常重要的对象。

3. 解析 TypeAliases

别名是 Mybatis 提供的一种简写实体的机制方案,通过配置别名我们可以少些很多冗余的代码并提高代码的可阅读性.

同 Properties 一样,typeAliases 也存在两种配置方式

  • 指定类的全称和别名
  • 通过指定 package 来批量指定别名

例如下面的就是配置一个简答的例子.

  <!-- 配置MyBatis的别名 -->
  <typeAliases>
    <typeAlias type="com.tao.source.mybatis.entity.User" alias="user"/>
  </typeAliases>

	  <!--  通过package的方式配置aliases-->
  <typeAliases>
    <package name="com.example.demo.entity"/>
  </typeAliases>

那么的话,这里存在疑问:

  • 如果是通过 package 配置的话,别名我们并没有提供,那么默认的别名是什么 class 的类名吗?

当我们在 Mapper 映射文件中使用的时候就可直接使用 user 代替 com.tao.source.mybatis.entity.User,非常的简便,解析别名的步骤如下:

  private void typeAliasesElement(XNode parent) {
    if (parent != null) {
     	// 如果是package配置的话,则通过批量注册来实现的,这个我们下面单独看
      for (XNode child : parent.getChildren()) {
        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);
            // 注册到typeAliasRegistry中,如果别名为null的话,则会有其他处理
            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);
          }
        }
      }
    }
  }
	// 如果别名为null,那么会使用type的SimpleName或者使用该类的Alias注解的属性
  public void registerAlias(Class<?> type) {
    String alias = type.getSimpleName();
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    if (aliasAnnotation != null) {
      alias = aliasAnnotation.value();
    }
    registerAlias(alias, type);
  }


	// 批量注册别名的代码
	  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()) {
        	// 调用上面的方法
          // Question_1: alias就是type的SimpleName
          registerAlias(type);
      	}
    	}
  	}
	
	// 核心注册别名
  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);
    // 如果别名已经注册,或者value已经存在,则会抛出异常
    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);
  }

未完待续~

  • 代码
    459 引用 • 591 回帖 • 8 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...