MyBatis 學習筆記(四)---源碼分析篇---配置文件的解析過程(二)

概述

接上一篇MyBatis 學習筆記(四)—源碼分析篇—配置文件的解析過程(一) 。上一篇我們介紹了properties 和settings配置的解析過程,今天我們接著來看看其他常用屬性的解析過程,重點介紹typeAliases,environments等配置的解析。

typeAliases的解析過程

一個簡單的別名配置如下:


   <typeAliases>
        <typeAlias type="com.jay.chapter2.entity.ClassRoom" alias="ClassRoom"/>
        <typeAlias type="com.jay.chapter2.entity.Student" alias="Student"/>
    </typeAliases>
	
//or    
<typeAliases>
  <package name="domain.blog"/>
</typeAliases> 

如上,typeAliases配置的使用也比較簡單,該配置主要是減少在映射文件中填寫全限定名的冗余。下面我們來看看解析過程

//* XMLConfigBuilder
  private void typeAliasesElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          //如果是package
          String typeAliasPackage = child.getStringAttribute("name");
          //(一)調用TypeAliasRegistry.registerAliases,去包下找所有類,然后注冊別名(有@Alias注解則用,沒有則取類的simpleName)
          configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
        } else {
          //如果是typeAlias
          String alias = child.getStringAttribute("alias");
          String type = child.getStringAttribute("type");
          try {
            Class<?> clazz = Resources.classForName(type);
            //根據(jù)Class名字來注冊類型別名
            //(二)調用TypeAliasRegistry.registerAlias
            if (alias == null) {
              //alias可以省略
              typeAliasRegistry.registerAlias(clazz);
            } else {
              typeAliasRegistry.registerAlias(alias, clazz);
            }
          } catch (ClassNotFoundException e) {
            throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
          }
        }
      }
    }
  }

如上,該入口程序方法執(zhí)行流程如下:

  1. 根據(jù)節(jié)點名稱判斷是否是package,如果是的話則調用TypeAliasRegistry.registerAliases,去包下找所有類,然后注冊別名(有@Alias注解則用,沒有則取類的simpleName)
  2. 如果不是的話,則進入另外一個分支,則根據(jù)Class名字來注冊類型別名。
    接下來我們按照兩個分支進行分析。

package解析分支

按照前面說的如果配置的是package的話,那么首先去包下找所有的類,然后注冊別名。
那么它是如何找到包下的所有類的呢?帶著疑問我們來看看源碼。

//* TypeAliasRegistry
  public void registerAliases(String packageName){
    registerAliases(packageName, Object.class);
  }
  
    public void registerAliases(String packageName, Class<?> superType){
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    //掃描并注冊包下所有繼承于superType的類型別名
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
    for(Class<?> type : typeSet){
	 //
      if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
        registerAlias(type);
      }
    }
  }
  
  	//注冊類型別名
  public void registerAlias(Class<?> type) {
    //如果沒有類型別名,用Class.getSimpleName來注冊
    String alias = type.getSimpleName();
	//或者通過Alias注解來注冊(Class.getAnnotation)
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    if (aliasAnnotation != null) {
      alias = aliasAnnotation.value();
    } 
    registerAlias(alias, type);
  }

如上,流程主要有兩部分

  1. 通過ResolverUtil的find方法找到該包下所有的類,傳入的父類是Object
  2. 循環(huán)注冊別名,只有非匿名類及非接口及內部類及非成員類才能注冊。
  3. 注冊別名最終還是調用registerAlias(alias, type)完成的。
    接著我們再來看看ResolverUtil到底是如何查找包下的所有類的。
 //主要的方法,找一個package下滿足條件的所有類,被TypeHanderRegistry,MapperRegistry,TypeAliasRegistry調用
  public ResolverUtil<T> find(Test test, String packageName) {
    String path = getPackagePath(packageName);

    try {
        //通過VFS來深入jar包里面去找一個class
      List<String> children = VFS.getInstance().list(path);
      for (String child : children) {
        if (child.endsWith(".class")) {
		  //將.class的class對象放入Set集合中,供后面調用
          addIfMatching(test, child);
        }
      }
    } catch (IOException ioe) {
      log.error("Could not read package: " + packageName, ioe);
    }

    return this;
  }

如上,核心是通過VFS來找到packageName下面的子包,包下面的class以及子包下面的class。PS: VFS 是虛擬文件系統(tǒng),用來讀取服務器里的資源。在此處我們不做分析。

根據(jù)Class名字注冊解析分支

	//注冊類型別名
  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);
    //如果已經存在key了,且value和之前不一致,報錯
    //這里邏輯略顯復雜,感覺沒必要,一個key對一個value唄,存在key直接報錯不就得了(與系統(tǒng)內置的類型別名相同的別名直接報錯)
    if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
      throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
    }
    TYPE_ALIASES.put(key, value);
  }
  
   public <T> Class<T> resolveAlias(String string) {
    try {
      if (string == null) {
        return null;
      }
      //這里轉個小寫也有bug?見748號bug(在google code上)   https://code.google.com/p/mybatis/issues
      //比如如果本地語言是Turkish,那i轉成大寫就不是I了,而是另外一個字符(?)。這樣土耳其的機器就用不了mybatis了!這是一個很大的bug,但是基本上每個人都會犯......
      String key = string.toLowerCase(Locale.ENGLISH);
      Class<T> value;
      //原理就很簡單了,從HashMap里找對應的鍵值,找到則返回類型別名對應的Class
      if (TYPE_ALIASES.containsKey(key)) {
        value = (Class<T>) TYPE_ALIASES.get(key);
      } else {
        //找不到,再試著將String直接轉成Class(這樣怪不得我們也可以直接用java.lang.Integer的方式定義,也可以就int這么定義)
        value = (Class<T>) Resources.classForName(string);
      }
      return value;
    } catch (ClassNotFoundException e) {
      throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
    }
  }

至此,我們的類型別名就注冊和解析就全部完成了。

environments的解析過程

我們都知道MyBatis中environments配置主要是用來配置數(shù)據(jù)源信息,是MyBatis中一定會有的配置。首先我們還是來看看environments配置的使用。

    <!-- 設置一個默認的連接環(huán)境信息 -->
	<environments default="development">
	 <!--連接環(huán)境信息,取一個任意唯一的名字 -->
	  <environment id="development">
	      <!-- mybatis使用jdbc事務管理方式 -->
	    <transactionManager type="JDBC">
	      <property name="..." value="..."/>
	    </transactionManager>
		            <!-- mybatis使用連接池方式來獲取連接 -->
	    <dataSource type="POOLED">
		                <!-- 配置與數(shù)據(jù)庫交互的4個必要屬性 -->
	      <property name="driver" value="${driver}"/>
	      <property name="url" value="${url}"/>
	      <property name="username" value="${username}"/>
	      <property name="password" value="${password}"/>
	    </dataSource>
	  </environment>
	</environments>

如上,配置了連接環(huán)境信息,我們心中肯定會有個疑問,${} 這種參數(shù)是如何解析的?我一會再分析。
下面我們就來看看這個配置的解析過程。

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
		//循環(huán)比較id是否就是指定的environment
        if (isSpecifiedEnvironment(id)) {
          //1 創(chuàng)建事務工廠TransactionFactory
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          //2創(chuàng)建數(shù)據(jù)源
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          //3.構建Environment對象
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
//          將創(chuàng)建的Environment對象設置到configuration中
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }

如上,解析environments 的流程有三個:

  1. 創(chuàng)建事務工廠TransactionFactory
  2. 創(chuàng)建數(shù)據(jù)源
  3. 創(chuàng)建Environment對象
    我們看看第一步和第二步的代碼
//* XMLConfigBuilder
 private TransactionFactory transactionManagerElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type");
      Properties props = context.getChildrenAsProperties();
		//根據(jù)type="JDBC"解析返回適當?shù)腡ransactionFactory
      TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance();
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a TransactionFactory.");
  }
  
   protected Class<?> resolveClass(String alias) {
    if (alias == null) {
      return null;
    }
    try {
      return resolveAlias(alias);
    } catch (Exception e) {
      throw new BuilderException("Error resolving class. Cause: " + e, e);
    }
  }
  //*Configuration
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);

JDBC 通過別名解析器解析之后會JdbcTransactionFactory工廠實例。數(shù)據(jù)源的解析與此類似最終得到的是PooledDataSourceFactory工廠實例。
下面我們來看看之前說過的類似${driver}的解析。其實是通過PropertyParser的parse來處理的。下面我們來看個時序圖。
在這里插入圖片描述
這里最核心的就是第五步,我們來看看源碼

  public static String parse(String string, Properties variables) {
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    return parser.parse(string);
  }

  //就是一個map,用相應的value替換key
  private static class VariableTokenHandler implements TokenHandler {
    private Properties variables;

    public VariableTokenHandler(Properties variables) {
      this.variables = variables;
    }

    @Override
    public String handleToken(String content) {
      if (variables != null && variables.containsKey(content)) {
        return variables.getProperty(content);
      }
      return "${" + content + "}";
    }
  }

如上,在VariableTokenHandler 會將${driver} 作為key,其需要被替換的值作為value。傳入GenericTokenParser中。然后通過GenericTokenParser 類的parse進行替換。
至此,我們environments配置就解析完了。

總結

本文主要介紹typeAliases和environments配置的解析。然后還說下${driver} 這種屬性的處理。希望對讀者朋友們有所幫助。

源代碼

https://github.com/XWxiaowei/mybatis




作者:碼農飛哥
微信公眾號:碼農飛哥