Mybatis3.3.x技術內幕(九):Mybatis初始化流程(中)

Mybatis初始化流程,其實就是組裝重量級All-In-One對象Configuration的過程,主要分爲系統環境參數初始化和Mapper映射初始化。java

上一節中,粗略講述了Mybatis初始化的基本步驟,本節,將詳細分析具體的初始化過程當中的細節問題,細節決定成敗。sql


1. Properties variables的做用apache

一般,咱們會單獨配置jdbc.properties文件,保存於variables變量中,而Xml文件內可使用${driver}佔位符,讀取時可動態替換佔位符的值。
編程

String value = PropertyParser.parse(attribute.getNodeValue(), variables);

Mybatis中的PropertyParser類,就是用來動態替換佔位符參數的。
緩存


2. 掃描package網絡

<typeAliases>
	<typeAlias alias="Student" type="com.mybatis3.domain.Student" />
	<typeAlias alias="Teacher" type="com.mybatis3.domain.Teacher" />
	<package name="com.mybatis3.domain" />
</typeAliases>

前兩個typeAlias,很容易理解,那麼<package>元素如何處理呢?
session

  public void registerAliases(String packageName, Class<?> superType){
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    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);
      }
    }
  }

掃描package下全部的Class,並註冊。此時,String alias = type.getSimpleName()。在處理typeHandlers和mappers時,處理package元素的原理也是同樣。
mybatis


3. namespace如何映射Mapper接口app

org.apache.ibatis.builder.xml.XMLMapperBuilder.bindMapperForNamespace()。框架

  // namespace="com.mybatis3.mappers.StudentMapper"
  private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound type is not required
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          configuration.addLoadedResource("namespace:" + namespace);
          configuration.addMapper(boundType);
        }
      }
    }
  }

直接使用Class.forName(),成功找到就註冊,找不到就什麼也不作。

Mybatis中的namespace有兩個功能。

1. 和其名字含義同樣,做爲名稱空間使用。namespace + id,就能找到對應的Sql。

2. 做爲Mapper接口的全限名使用,經過namespace,就能找到對應的Mapper接口(也有稱Dao接口的)。Mybatis推薦的最佳實踐,但並不強制使用。


Mapper接口註冊至Configuration的MapperRegistry mapperRegistry內。

private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

Mapper接口將經過MapperProxyFactory建立動態代理對象。

可參看動態代理之投鞭斷流(自動映射器Mapper的底層實現原理)博文。


4. 一個MappedStatement被緩存了兩個引用的原理及緣由

configuration.addMappedStatement(statement);

調用上面一句話,往Map裏放置一個MappedStatement對象,結果Map中變成兩個元素。

com.mybatis3.mappers.StudentMapper.findAllStudents=org.apache.ibatis.mapping.MappedStatement@add0edd
findAllStudents=org.apache.ibatis.mapping.MappedStatement@add0edd

咱們的問題是,爲何會變成兩個元素?同一個對象,爲何要存有兩個鍵的引用?

其實,在Mybatis中,這些Map,都是StrictMap類型,Mybatis在StrictMap內作了手腳。

protected static class StrictMap<V> extends HashMap<String, V> {

    public V put(String key, V value) {
      if (containsKey(key)) {
        throw new IllegalArgumentException(name + " already contains value for " + key);
      }
      if (key.contains(".")) {
        final String shortKey = getShortName(key);
        // 不存在shortKey鍵值,放進去
        if (super.get(shortKey) == null) {
          super.put(shortKey, value);
        } else {
        // 存在shortKey鍵值,填充佔位對象Ambiguity
          super.put(shortKey, (V) new Ambiguity(shortKey));
        }
      }
      return super.put(key, value);
    }
}

Mybatis重寫了put方法,將id和namespace+id的鍵,都put了進去,指向同一個MappedStatement對象。若是shortKey鍵值存在,就填充爲佔位符對象Ambiguity,屬於覆蓋操做。

這樣作的好處是,方便咱們編程。

Student std  = sqlSession.selectOne("findStudentById", 1);
Student std  = sqlSession.selectOne("com.mybatis3.mappers.StudentMapper.findStudentById", 1);

上面兩句代碼,是等價的,Mybatis不強制咱們必定要加namespace名稱空間,因此,這是存放兩個鍵的良苦用心。

問題:不一樣namespace空間下的id,可否相同呢?(網上的說法是,不一樣名稱空間下的id能夠相同)

明白上述put原理後,就不可貴出結論,namespace名稱空間不一樣,而id相同時,使用namespace+id獲取Sql,徹底能夠正確執行。若是隻用id獲取,那麼,將致使錯誤。

org.apache.ibatis.session.Configuration.StrictMap.get()方法源碼。

    public V get(Object key) {
      V value = super.get(key);
      if (value == null) {
        throw new IllegalArgumentException(name + " does not contain value for " + key);
      }
      if (value instanceof Ambiguity) {
        throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name
            + " (try using the full name including the namespace, or rename one of the entries)");
      }
      return value;
    }

get時,若是獲得的是一個佔位對象Ambiguity,就拋出異常,要求使用full name進行調用。full name就是namespace+id。Ambiguity意爲模糊不清。

解決辦法:

1. 保證shortKey(即id)不重複。(好像有點難度,不推薦)

2. 使用綁定Mapper接口調用方法,由於它老是轉換爲full name調用。(Mybatis最佳實踐,推薦)

3. 直接使用字符串full name調用。(退而求其次的方式,不推薦)


5. 初始化過程當中的mapped和incomplete對象

翻譯爲搞定的和還沒搞定的。這恐怕是Mybatis框架中比較奇葩的設計了,給人不少迷惑,咱們來看看它具體是什麼意思。

<resultMap type="Student" id="StudentResult" extends="Parent">
	<id property="studId" column="stud_id" />
	<result property="name" column="name" />
	<result property="email" column="email" />
	<result property="dob" column="dob" />
</resultMap>
	
<resultMap type="Student" id="Parent">
	<result property="phone" column="phone" />
</resultMap>

Mapper.xml中的不少元素,是能夠指定父元素的,像上面extends="Parent"。然而,Mybatis解析元素時,是按順序解析的,因而先解析的id="StudentResult"的元素,然而該元素繼承自id="Parent"的元素,可是,Parent被配置在下面了,尚未解析到,內存中尚不存在,怎麼辦呢?Mybatis就把id="StudentResult"的元素標記爲incomplete的,而後繼續解析後續元素。等程序把id="Parent"的元素也解析完後,再回過頭來解析id="StudentResult"的元素,就能夠正確繼承父元素的內容。

簡言之就是,你的父元素能夠配置在你的後邊,不限制非得配置在前面。不管你配置在哪兒,Mybatis都能「智能」的獲取到,並正確繼承。

這即是在Configuration對象內,有的叫mapped,有的叫incomplete的緣由。

protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<XMLStatementBuilder>();
protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<CacheRefResolver>();
protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<ResultMapResolver>();
protected final Collection<MethodResolver> incompleteMethods = new LinkedList<MethodResolver>();

org.apache.ibatis.builder.xml.XMLMapperBuilder.parse()方法內,觸發了incomplete的再度解析。

  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }
    // 執行incomplete的地方
    parsePendingResultMaps();
    parsePendingChacheRefs();
    parsePendingStatements();
  }

Pending含義爲待定的,懸而未決的意思。


總結:有了這些細節儲備,閱讀源碼就變得更加駕輕就熟了。


版權提示:文章出自開源中國社區,若對文章感興趣,可關注個人開源中國社區博客(http://my.oschina.net/zudajun)。(通過網絡爬蟲或轉載的文章,常常丟失流程圖、時序圖,格式錯亂等,仍是看原版的比較好)

相關文章
相關標籤/搜索