Mybatis源碼解析-6.Mapper映射文件配置解析

6.XML文件格式的mapper標籤解析

上一節已經知道,對於XML文件中mapper標籤的解析都是通過XMLMapperBuilder進行處理的。

接下來讓我們首先對XMLMapperBuilder進行分析,然後再詳細考察mapper標籤的解析邏輯。

XMLMapperBuilder

XMLMapperBuilder,顧名思義,該工具類是用於解析mapper標籤的,這裏我們主要分析XMLMapperBuilder的四個屬性,便於下面分析具體的業務邏輯。XMLMapperBuilder擁有如下四個屬性:

  1. XPathParser parser:XML文件解析器,該對象是XML文件解析的工具類。
  2. MapperBuilderAssistant builderAssistant:Mapper構建協助器,由於Mybatis中的很多對象都比較大,屬性比較多,而解析結果本身就是一個對象,因此Mybatis將解析結果的構建代碼進行封裝,封裝到這個構建協助器裏,讓代碼更加簡潔。除此之外,這裏還存儲了當前的命名空間,方便獲取。
  3. Map<String, XNode> sqlFragments :Sql段,保存<mapper>標籤的<sql>子標籤定義的sql語句,這裏我們先不考慮該對象含義。
  4. String resource:要解析的資源的名稱

開始解析mapper標籤

瞭解了XMLMapperBuilder的各個屬性的含義,就可以開始解析mapper標籤了,否則我們可能在解析到一半就不知道哪個東西是做什麼的了。

根據XMLConfigBuildermapperElement(XNode)方法中如下代碼:

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();

我們知道,mapper標籤的解析是由XMLMapperBuilderparse()方法完成的。讓我們考察該方法代碼:

public void parse() {
    // 如果資源沒有被解析過,則需要進行解析
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }
    // 解析未完成解析的ResultMap、CacheRef和Statement
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

對於該方法的前半部分肯定大家都沒有意見,如果資源沒有被解析,就開始解析,這是一個正常的需求,而後半部分,則就無法理解了,爲什麼這裏無論如何都要執行呢?事實上,並不是解析了一個mapper配置文件就可以獲取到所有的ResultMapCacheRefStatements的,所以再每次解析完一個mapper配置文件都會進行解析一遍上面三個資源,而且,對於具有Java配置的Mybatis,可能還需要解析完Java配置後,再去解析上面的三個資源。所以纔會出現後半部分。

讓我們先討論前半部分的解析mapper標籤的部分。其中:

  1. configurationElement(parser.evalNode("/mapper")) 這行代碼負責主要解析mapper配置,並創建一個MappedStatement放入到Configuration類型的對象中。
  2. configuration.addLoadedResource(resource); 這行代碼負責記錄,該配置文件已經被解析完畢
  3. bindMapperForNamespace(); 最後的一行代碼負責將Java的Mapper對象與命名空間(即XML配置文件)聯繫起來。

解析mapper標籤

下面讓我們考察一下configurationElement(XNode)方法。該方法是解析XML映射文件的主要邏輯。代碼如下:

private void configurationElement(XNode context) {
    try {
      // 獲取命名空間名稱
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      // 存儲當前命名空間
      builderAssistant.setCurrentNamespace(namespace);
      // 解析另一命名空間的緩存配置
      cacheRefElement(context.evalNode("cache-ref"));
      // 解析當前命名空間是否開啓緩存
      cacheElement(context.evalNode("cache"));
      // 解析parameterMap標籤
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // 解析resultMap標籤
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 解析sql標籤
      sqlElement(context.evalNodes("/mapper/sql"));
      // 解析select|insert|update|delete標籤
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
}

解析cache-ref標籤

首先處理<cache-ref>標籤,該標籤是引用其他緩存的標籤,Mybatis對該標籤描述如下:

對某一命名空間的語句,只會使用該命名空間的緩存進行緩存或刷新。 但你可能會想要在多個命名空間中共享相同的緩存配置和實例。要實現這種需求,你可以使用 cache-ref 元素來引用另一個緩存。

該屬性的解析由cacheRefElement(XNode)方法處理,其實代碼很簡單:

private void cacheRefElement(XNode context) {
    if (context != null) {
      // 在Configuration的緩存引用註冊表中添加一條
      // 當前命名空間:要引用的命名空間
      // 用於標識命名空間緩存之間的引用關係
      configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
      // 將builderAssistant和要引用的命名空間緩存封裝入一個CacheRefResolver去解析緩存
      // 解析成功後放到builderAssistant備用
      CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
      try {
        // 嘗試解析緩存
        // 解析失敗則放入到Configuration對象的incompleteCacheRefs註冊表中
        cacheRefResolver.resolveCacheRef();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteCacheRef(cacheRefResolver);
      }
    }
}

事實證明,在Mybatis中,儲存命名空間使用緩存關係的是一張名爲cacheRefMap格式爲<String,String>的註冊表,其中key和value都是命名空間的名稱。因此此處,是將<當前命名空間,引用的緩存的命名空間>放入到註冊表中。然後再根據緩存命名空間解析使用的緩存。

當然對於解析失敗的緩存,Mybatis將解析失敗,即當前未生成的緩存,就將其放入到incompleteCacheRefs註冊表中,等待進一步解析,這也就是mapper標籤解析中後三個方法的意義。

解析cache標籤

<cache>標籤用於定義當前命名空間使用的緩存,該標籤的解析是由cacheElement(XNode)方法完成的。解析方法相對簡單:

private void cacheElement(XNode context) {
    if (context != null) {
      // 獲取緩存類型
      // 默認情況下是永久緩存,沒有過期時間
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      // 解析過期策略,默認是LRU,最近最少使用
      String eviction = context.getStringAttribute("eviction", "LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      // 設置刷新間隔
      Long flushInterval = context.getLongAttribute("flushInterval");
      // 設置緩存大小
      Integer size = context.getIntAttribute("size");
      // 設置緩存是否只讀
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      boolean blocking = context.getBooleanAttribute("blocking", false);
      // 將所有屬性按照Properties讀取
      Properties props = context.getChildrenAsProperties();
      // 將讀取到的屬性應用到緩存中,構建緩存
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

之前說過,MapperBuilderAssistant的一大功能就是創建複雜對象並註冊到Configuration註冊表中,這裏就是通過讀取到的屬性建立一個緩存,然後註冊到Configuration對象中。查看MapperBuilderAssistant.useNewCache(...)方法:

public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    // 根據配置構建Cache
    // 默認狀態是永久緩存,使用LRU算法
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    // 將緩存註冊到Configuration中
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
}

那麼在Mybatis中,緩存是如何存儲的呢?考察如下代碼:

configuration.addCache(cache);

public void addCache(Cache cache) {
    caches.put(cache.getId(), cache);
}

可以看到,緩存在Configuration對象中也是存放在一個註冊表中,該註冊表叫做caches,格式是<String, Cache>,其中key是命名空間,value是緩存實例。

解析parameterMap標籤

<parameterMap>標籤用於聲明方法參數類型,解析該標籤使用的是parameterMapElement(List<XNode>)方法,考察該方法:

private void parameterMapElement(List<XNode> list) {
    // 遍歷所有parameterMap標籤
    for (XNode parameterMapNode : list) {
      // 獲取parameterMap的id
      String id = parameterMapNode.getStringAttribute("id");
      // 獲取parameterMap代表的Class類型
      String type = parameterMapNode.getStringAttribute("type");
      // 將type解析爲Class對象
      Class<?> parameterClass = resolveClass(type);
      // 獲取所有parameter子標籤
      List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter");
      // 創建列表存儲子標籤內容
      List<ParameterMapping> parameterMappings = new ArrayList<>();
      // 遍歷所有子標籤
      for (XNode parameterNode : parameterNodes) {
        // 獲取屬性名
        String property = parameterNode.getStringAttribute("property");
        // 獲取屬性類型
        String javaType = parameterNode.getStringAttribute("javaType");
        // 獲取屬性代表的JDBC類型
        String jdbcType = parameterNode.getStringAttribute("jdbcType");
        // 獲取resultMap屬性,具體使用到的情況請看Mybatis文檔
        String resultMap = parameterNode.getStringAttribute("resultMap");
        // 獲取mode屬性
        String mode = parameterNode.getStringAttribute("mode");
        // 獲取typeHandler屬性
        String typeHandler = parameterNode.getStringAttribute("typeHandler");
        // 獲取numericScale屬性,該屬性表示小數保留的位數
        Integer numericScale = parameterNode.getIntAttribute("numericScale");
        ParameterMode modeEnum = resolveParameterMode(mode);
        Class<?> javaTypeClass = resolveClass(javaType);
        JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
        Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
        // 將子標籤構造成一個ParameterMapping
        ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);
        // 並放入一個列表中
        parameterMappings.add(parameterMapping);
      }
      // 最後將結果列表註冊到`Configuration`對象的`parameterMaps`屬性中
      builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
    }
}

仔細考察builderAssistant.addParameterMap(id, parameterClass, parameterMappings);可以發現,Configuration對象使用parameterMaps註冊表存儲各個ParameterMap,格式爲<String, ParameterMap>。其中key是id,parameterMap的id是命名空間 + id字段,value就是parameterMap對象。

解析resultMap標籤

<resultMap>標籤用於表示Mybatis數據庫操作的結果集,是Mybatis引以爲豪的一部分。該標籤功能相當強大,在一般的結果映射上增添了許多功能。ResultMap 的設計思想是,對簡單的語句做到零配置,對於複雜一點的語句,只需要描述語句之間的關係就行了。

解析<resultMap>標籤的方法是resultMapElements(List<XNode>),該方法只是遍歷所有的<resultMap>標籤對其進行解析罷了:

private void resultMapElements(List<XNode> list) throws Exception {
    for (XNode resultMapNode : list) {
      try {
        resultMapElement(resultMapNode);
      } catch (IncompleteElementException e) {
        // ignore, it will be retried
      }
    }
  }

真正的解析方法其實在resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType)中,因爲resultMapElement(resultMapNode);就是調用的該方法,我們直接考察該方法:

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    // 獲取resultMap的id
    String id = resultMapNode.getStringAttribute("id",
        resultMapNode.getValueBasedIdentifier());
    // 獲取resultMap的java類型
    // 獲取順序是type->ofType->resultType->javaType
    String type = resultMapNode.getStringAttribute("type",
        resultMapNode.getStringAttribute("ofType",
            resultMapNode.getStringAttribute("resultType",
                resultMapNode.getStringAttribute("javaType"))));
    // 獲取extends屬性,
    String extend = resultMapNode.getStringAttribute("extends");
    // 獲取autoMapping屬性
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    // 通過獲取到的Java類型獲取對應的Class對象
    Class<?> typeClass = resolveClass(type);
    Discriminator discriminator = null;
    List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
    resultMappings.addAll(additionalResultMappings);
    // 遍歷resultMap標籤的所有子標籤
    // resultMap的子標籤共有4種:
    // 1. constructor:用於聲明創建結果集的構造器
    // 2. discriminator:配置鑑別器
    // 3. id:用於標記字段是id
    // 4. result:用於標記屬性與數據庫字段的對應關係
    List<XNode> resultChildren = resultMapNode.getChildren();
    for (XNode resultChild : resultChildren) {
      if ("constructor".equals(resultChild.getName())) {
        // connstuctor標籤的解析
        processConstructorElement(resultChild, typeClass, resultMappings);
      } else if ("discriminator".equals(resultChild.getName())) {
        // discriminator 標籤的解析
        discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
      } else {
        // id 標籤的解析
        List<ResultFlag> flags = new ArrayList<ResultFlag>();
        if ("id".equals(resultChild.getName())) {
          flags.add(ResultFlag.ID);
        }
        // result標籤的解析
        resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
      }
    }
    // 通過ResultMapResolver將解析結果添加到Configuration的resultMaps註冊表中
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
      return resultMapResolver.resolve();
    } catch (IncompleteElementException  e) {
      configuration.addIncompleteResultMap(resultMapResolver);
      throw e;
    }
}

事實上,Mybatis中的<resultMap>標籤最後會變成一個ResultMap對象。我們之前一共談到了如下幾個屬性,他們與ResultMap對象的屬性對應關係如下:

  1. id:ResultMap.id
  2. type(Java類型):ResultMap.type
  3. result子標籤集合:ResultMap.resultMappings
  4. id子標籤:ResultMap.idResultMappings
  5. 構造器子標籤:ResultMap.constructorResultMappings
  6. 鑑別器子標籤:ResultMap.discriminator
  7. autoMapping屬性:ResultMap.autoMapping

需要注意,各個XXXResultMappings屬性都是ResultMapping對象列表ResultMapping主要用來存儲java對象屬性與數據庫字段之間的對應關係,例如:

 <result property="doorCount" column="door_count" />

上面的xml配置就會生成一個ResultMapping對象。我們根據這一段xml文件配置查看一下ResultMapping到底如何進行存儲的?ResultMapping對象具有如下幾個屬性,分別代表的意義列在註釋上:

  // 保存該ResultMap的Configuration對象
  private Configuration configuration;
  // 當前標籤所代表的屬性,上面例子中則是doorCount
  private String property;
  // 當前標籤所配置的數據庫列名,上面例子中則是door_count
  private String column;
  // 當前字段在Java中的類型
  private Class<?> javaType;
  // 當前字段在數據庫中的Jdbc類型
  private JdbcType jdbcType;
  // 處理該類型字段的類型處理器
  private TypeHandler<?> typeHandler;
  // 對於Mybatis來說經常會有如下的情況
  // <collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
  // 如果是這種情況,就會將resultMap的id填在此處
  private String nestedResultMapId;
  // 對於Mybatis的resultMap標籤會有如下情況
  // <collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
  // 如果是這種情況,就會把select中指定的查詢語句的id填在此處
  private String nestedQueryId;
  private Set<String> notNullColumns;
  // 列前綴
  private String columnPrefix;
  // 當前字段的標識,例如該字段是ID
  private List<ResultFlag> flags;
  // 對於Mybatis的resultMap標籤會有如下情況:
  // <collection property="posts" ofType="domain.blog.Post">
  //   <id property="id" column="post_id"/>
  //   <result property="subject" column="post_subject"/>
  //   <result property="body" column="post_body"/>
  // </collection>
  // 這樣就會出現ResultMapping的嵌套關係,而這裏就用來存儲這一嵌套關係
  private List<ResultMapping> composites;
  private String resultSet;
  // 外鍵
  private String foreignColumn;
  // 該字段是否需要懶加載
  private boolean lazy;

通過上面的解析,我們已經瞭解了<resultMap>標籤在Mybatis配置對象中保存的結構,其實每個<resultMap>標籤就是一個ResultMap對象,這個對象中存儲了對象屬性與數據庫字段的關係,並且存儲了獲取這些對象使用的查詢sqlId,就好像一個命令模式,提供了輸出格式和操作過程,由執行器執行具體的邏輯(SQL查詢語句),然後拼接返回結果。

解析sql標籤

mapper映射文件的配置中,我們經常用到如下配置:

<sql id="someinclude">
  from
    <include refid="${include_target}"/>
</sql>

這種引用某個SQL片段的情況。<sql>標籤就是爲了防止重複書寫相同配置而存在的。該標籤由XMLMapperBuilder.sqlElement(List<XNode>)方法進行解析。下面我們考察該方法:

private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
    for (XNode context : list) {
      // 獲取databaseId屬性
      String databaseId = context.getStringAttribute("databaseId");
      // 獲取id屬性
      String id = context.getStringAttribute("id");
      // 將id拼接上命名空間,構建成完整的id
      id = builderAssistant.applyCurrentNamespace(id, false);
      // 如果當前正在使用的數據庫ID與獲取到的SQL上的ID相同的話
      // 那麼引用這個sql標籤
      // 否則不使用它
      if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
        sqlFragments.put(id, context);
      }
  }
}

注意,此處的應用是將XNode節點放入sqlFragments屬性中,就是我們之前賣關子的屬性。真正解析該標籤的過程是在真正解析操作數據庫的sql語句的時候。話不多說,讓我們考察那些SQL語句吧。

解析語句標籤

真正操作數據庫,執行數據庫操作的SQL是由<insert>selectupdatedelete標籤聲明的,這四個標籤分別聲明瞭插入、查找、更新、刪除操作。事實上,對於mybatis來說,數據庫操作只有兩種:

  1. 查找:select
  2. 更新:包括insert、update、delete

處理上述四個標籤解析邏輯的是XMLMapperBuilder.buildStatementFromContext(List<XNode>)方法。該方法傳入所有的數據庫操作SQL標籤的XML節點,源碼如下:

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    // 遍歷所有的insert、update、delete、select標籤
    for (XNode context : list) {
      // 爲每個標籤創建XMLStatementBuilder
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        // 通過XMLStatementBuilder進行解析
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
}

之前已經說過,在Mybatis的BaseBuilder類有多個子類,其中除了XMLConfigBuilder類以外,其餘的類都是用於解析<mapper>標籤的,這裏的XMLStatementBuilder就是用來解析<mapper>標籤中聲明的持久化操作的。具體的解析邏輯在XMLStatementBuilder.parseStatementNode()方法中。注意在創建XMLStatementBuilder傳入瞭如下參數:

  1. configuration:Mybatis配置存儲的配置對象
  2. builderAssistant:配置解析的協助器,用於簡化各種複雜的構建操作,並將構建的結果對象添加到configuration對象中
  3. context:要解析的XML節點
  4. requiredDatabaseId:mybatis配置中指定的databaseId,由於之前已經提到,<sql>標籤只有在自己聲明的databaseId與Mybatis配置的databaseId相同的情況下才會使用。而sql標籤內容的解析在各個使用了include標籤的地方進行解析。

明確了上面的內容之後,我們就可以開始分析解析操作數據的sql的真正邏輯了,即XMLStatementBuilder.parseStatementNode()方法,源碼如下:

public void parseStatementNode() {
    // 獲取SQL語句ID
    String id = context.getStringAttribute("id");
    // 獲取SQL語句適應的數據庫
    String databaseId = context.getStringAttribute("databaseId");
    // 如果當前sql指定的數據庫與Mybatis中配置的不同,則不進行解析
    // 需要注意,這裏還做了一個判重操作,如果有相同id的語句已經解析過,並且其databaseId不爲null,那麼就跳過本次解析
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    // 獲取fetchSize屬性
    Integer fetchSize = context.getIntAttribute("fetchSize");
    // 獲取timeout屬性
    Integer timeout = context.getIntAttribute("timeout");
    // 獲取parameterMap屬性
    String parameterMap = context.getStringAttribute("parameterMap");
    // 獲取parameterType屬性
    String parameterType = context.getStringAttribute("parameterType");
    // 將parameterType解析成Java的Class對象
    Class<?> parameterTypeClass = resolveClass(parameterType);
    // 獲取resultMap屬性
    String resultMap = context.getStringAttribute("resultMap");
    // 獲取resultType屬性
    String resultType = context.getStringAttribute("resultType");
    // 獲取語言屬性
    String lang = context.getStringAttribute("lang");
    // 查找對應的語言驅動
    LanguageDriver langDriver = getLanguageDriver(lang);
    // 將resultType屬性解析爲對應的Java的Class對象
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    // 判斷SQL是否是查詢語句
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    // 判斷是否刷新緩存
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    // 判斷是否使用緩存
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // 解析include標籤
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    // 處理自動生成id
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }
    // 將解析結果構建成一個MappedStatement放入到configuration對象的mappedStatements註冊表中
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

其實上述解析操作大致可以分爲5步:

  1. 判斷databaseId與當前配置是否匹配,並判斷該id的語句是否被解析過,這就是問什麼要優先獲取id的原因
  2. 獲取標籤基本信息
  3. 處理include標籤
  4. 處理自動生成key的配置
  5. 構建MappedStatement放入到configuration對象中

其中第一步和第二步都比較簡單,這裏我們主要介紹後面三個步驟。首先是處理include標籤。

include標籤的處理

<include>標籤就是用來引用之前在sql標籤中聲明的sql的。上面的代碼中處理include標籤的是如下幾行:

XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());

可以看到對於<include>標籤的解析交給了XMLIncludeTransformer這一解析器,該解析器與別的解析器有一個很大的不同,就是別的解析器都是解析標籤,然後將標籤的內容封裝爲一個Java對象,放到Configuration對象中,而這個解析器則是,修改XML解析對象,然後再次交給XMLStatementsBuilder進行解析。瞭解了這一點之後,我們可以考察一下XMLIncludeTransformer.applyIncludes(Node source, final Properties variablesContext, boolean included)方法的源碼:

private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
    // 情況1
    // 如果傳入的節點是<include>節點
    // 那麼獲取refid屬性,以及<include>中聲明的參數
    // 根據這些去查找對應的sql段
    // 如果sql段中有<include>標籤,則遞歸處理
    // 否則將include標籤替換成sql標籤
    // 然後再將sql標籤中所有的內容替換到sql標籤所在的位置
    if (source.getNodeName().equals("include")) {
      Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
      Properties toIncludeContext = getVariablesContext(source, variablesContext);
      applyIncludes(toInclude, toIncludeContext, true);
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      source.getParentNode().replaceChild(toInclude, source);
      while (toInclude.hasChildNodes()) {
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      toInclude.getParentNode().removeChild(toInclude);
      // 情況2
      // 如果傳入的標籤是一般的非<include>標籤
      // 則應用屬性表中的屬性到標籤中,然後遞歸初期傳入標籤的子標籤
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
      if (included && !variablesContext.isEmpty()) {
        // replace variables in attribute values
        NamedNodeMap attributes = source.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
          Node attr = attributes.item(i);
          attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
        }
      }
      NodeList children = source.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        applyIncludes(children.item(i), variablesContext, included);
      }
      // 情況3
      // 如果傳入的標籤是文本標籤
      // 那麼應用屬性表中的屬性
    } else if (included && source.getNodeType() == Node.TEXT_NODE
        && !variablesContext.isEmpty()) {
      // replace variables in text node
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
}

處理<include>標籤使用了遞歸的方式進行處理。這裏我們舉例說明會比較形象,考慮下面的例子:

<sql id="sometable">
  ${prefix}Table
</sql>

<sql id="someinclude">
  from
    <include refid="${include_target}"/>
</sql>

<select id="select" resultType="map">
  select
    field1, field2, field3
  <include refid="someinclude">
    <property name="prefix" value="Some"/>
    <property name="include_target" value="sometable"/>
  </include>
</select>

一般情況下,我們解析id爲select<select>標籤,這裏包含了一個<include>標籤,實際上會將該<select>標籤傳入到applyIncludes(Node source, final Properties variablesContext, boolean included),其中source就是該<select>標籤內容,variablesContext<include>標籤中的屬性表,最後的included表示是不是include標籤內的內容。

這裏首先會調用情況2,解析<select>標籤,獲取所有該標籤的子標籤進行遍歷處理<include>標籤,事實上,該標籤只有兩個標籤,第一個是select field1, field2, field3,第二個是include標籤,這裏第一個標籤交給情況3處理,<include>標籤交給情況1處理。

需要注意的是,可以發現,Mybatis並沒有進行<sql>標籤自身的循環依賴問題處理。實際上如果出現了此問題,Mybatis只能是在解析時無限遞歸導致stackOverFlow,並不會給出任何提示,不像是Spring還會給出出現了循環依賴問題。

例如,你寫的sql標籤是這樣的:

<sql id="id">
  c.id
  <include refid="id"></include>
</sql>

這種情況就會導致<include>標籤的解析出現無限的遞歸,因此,出現stackOverFlow的情況。

selectKey標籤的處理

Mybatis的<selectKey>標籤用於處理自動生成主鍵的問題。有些數據庫或者JDBC驅動不支持自動生成主鍵,因此只能自己創建自定義主鍵。但是頻繁調用setter方法設置主鍵又不好,因此就有了selectKey這一標籤。Mybatis中給出的<selectKey>標籤的使用方法如下:

<insert id="insertAuthor">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
  </selectKey>
  insert into Author
    (id, username, password, email,bio, favourite_section)
  values
    (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>

簡單介紹一下<selectKey>子標籤的屬性:

  1. keyProperty:selectKey 語句結果應該被設置到的目標屬性。如果生成列不止一個,可以用逗號分隔多個屬性名稱。
  2. keyColumn:返回結果集中生成列屬性的列名。如果生成列不止一個,可以用逗號分隔多個屬性名稱。
  3. resultType:結果的類型。通常 MyBatis 可以推斷出來,但是爲了更加準確,寫上也不會有什麼問題。MyBatis 允許將任何簡單類型用作主鍵的類型,包括字符串。如果生成列不止一個,則可以使用包含期望屬性的 Object 或 Map。
  4. order:可以設置爲 BEFORE 或 AFTER。如果設置爲 BEFORE,那麼它首先會生成主鍵,設置 keyProperty 再執行插入語句。如果設置爲 AFTER,那麼先執行插入語句,然後是 selectKey 中的語句 - 這和 Oracle 數據庫的行爲相似,在插入語句內部可能有嵌入索引調用。
  5. statementType:和前面一樣,MyBatis 支持 STATEMENT,PREPARED 和 CALLABLE 類型的映射語句,分別代表 Statement, PreparedStatement 和 CallableStatement 類型。

處理<selectKey>標籤的是XMLStatementBuilder.processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver)方法。考察該方法源碼如下:

private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
    List<XNode> selectKeyNodes = context.evalNodes("selectKey");
    // 處理<selectKey>標籤
    if (configuration.getDatabaseId() != null) {
      parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
    }
    parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
    // 移除<selectKey>標籤
    removeSelectKeyNodes(selectKeyNodes);
}

通過剛纔給出的例子我們可以看到,<selectKey>標籤中的內容僅用於查詢一個值,該值用於作爲<insert>操作的主鍵,因此真正執行插入操作時是不需要這條sql的,因此需要在解析之後把它清除掉。

parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);

這兩行代碼負責解析<selectKey>標籤,而後面的removeSelectKeyNodes(selectKeyNodes)負責將<selectKey>標籤移除。

下面我們考察解析操作中佔主要地位的XMLStatementBuilder.parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId)方法。該方法其實僅僅是讀取所有的<selectKey>標籤然後進行解析,代碼如下:

private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
    // 遍歷所有的`<selectKey>`標籤,對每個標籤進行解析
    for (XNode nodeToHandle : list) {
      String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
      String databaseId = nodeToHandle.getStringAttribute("databaseId");
      if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
        parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
      }
    }
}

可以看到,對於每個<selectKey>標籤來說,他的默認id是其父id+!selectKey。例如上面的例子中,<selectKey>的id就是insertAuthor!selectKey。不過實際上<selectKey>就是一個查詢語句,也就相當於<select>標籤,Mybatis確實也是這樣處理的。我們都知道對於<select>標籤來說,最後都會變成一個MappedStatement。對於<selectKey>標籤也是,該MappedStatement的id就是我們上面生成的那個id。真正構建MappedStatement的方法就是parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);這行代碼。也就是XMLStatementBuilder.parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId)方法。源碼如下 :

private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
    // 獲取返回類型
    String resultType = nodeToHandle.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    // 獲取statementType
    StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    // 獲取Java屬性名稱
    String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
    //  獲取數據庫屬性名稱
    String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
    // 判斷是在插入操作之前調用還是之後調用
    boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

    //defaults
    boolean useCache = false;
    boolean resultOrdered = false;
    KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
    Integer fetchSize = null;
    Integer timeout = null;
    boolean flushCache = false;
    String parameterMap = null;
    String resultMap = null;
    ResultSetType resultSetTypeEnum = null;

    SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
    SqlCommandType sqlCommandType = SqlCommandType.SELECT;
    // 創建一個MappedStatement保存該查詢操作
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);

    id = builderAssistant.applyCurrentNamespace(id, false);

    MappedStatement keyStatement = configuration.getMappedStatement(id, false);
    // 使用該查詢Sql創建一個KeyGenerator
    // 並將該查詢sql與keyGenerator簡歷聯繫
    configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}

事實證明,通常情況下,我們很少使用<selectKey>標籤顯式指定id,更習慣的是使用數據庫的自增id,這種時候,我們只需要在<insert>useGeneratedKeys屬性置爲true。這時候使用的就是默認的Jdbc3KeyGenerator

至於各種KeyGenerator的具體執行流程,我們在後面講解Mybatis執行流程時將會討論該問題。

MappedStatement的結構

執行了上面的所有操作,我們就已經做好了充足的準備構建一個新的MappedStatement用來表示一個操作的SQL語句。那麼MappedStatement到底是怎麼和我們剛纔獲取到的數據對應上的呢?下面讓我們考慮MappedStatement的屬性以及其業務含義。

  // 該MappedStatement所對應的mapper.xml
  private String resource;
  // 存儲該MappedStatement的Configuration對象
  private Configuration configuration;
  // 該MappedStatement的id
  private String id;
  // 該MappedStatement獲取結果的默認長度
  private Integer fetchSize;
  // 該MappedStatement代表的SQL執行的超時時間
  private Integer timeout;
  // 該MappedStatement的Statement類型
  private StatementType statementType;
  // 該MappedStatement代表的SQL的返回集合類型
  private ResultSetType resultSetType;
  // 該MappedStatement的Sql元數據
  private SqlSource sqlSource;
  // 該MappedStatement所使用的緩存
  private Cache cache;
  // 該MappedStatement所使用的參數列表
  private ParameterMap parameterMap;
  // 該MappedStatement所使用的結果集列表
  private List<ResultMap> resultMaps;
  // 執行該MappedStatement所代表的操作時是否要刷新緩存
  private boolean flushCacheRequired;
  // 執行該MappedStatement所代表的操作時是否要使用緩存
  private boolean useCache;
  private boolean resultOrdered;
  // 該MappedStatement所代表SQL的類型(UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;)
  private SqlCommandType sqlCommandType;
  // 該MappedStatement的對應key生成器
  private KeyGenerator keyGenerator;
  // 該MappedStatement對應的key生成器使用的屬性
  private String[] keyProperties;
  // 該MappedStatement對應的key生成器對應的數據庫列
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  // 數據庫類型
  private String databaseId;
  // statementLog
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;

可以看到MappedStatement包含了一條SQL執行的所有格式上的數據,只缺少查詢參數這類內容數據,這就像一個命令模式,只需要傳入命令格式和對應的內容就可以執行。

這裏除了對MappedStatement結構進行講解以外,還要特別講解一下我們的如下方法:

builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);

在我們分析了那麼多的Mybatis配置解析源碼都沒有仔細提這個builderAssistant,即我們的構建協助器,但是現在我們不得不說一下該類的addMappedStatement(XXXX)方法,因爲這涉及到我們的Mapper到底使用哪個緩存,下面我們考察這個方法的部分代碼:

public MappedStatement addMappedStatement(...) {
    ...

    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache); // 重點

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
  }

可以看到,最後Mapper真正使用的緩存就是這個currentCache。更改這個屬性的代碼就在解析<cache><cache-ref>標籤中。

瞭解了最主要的MappedStatement,我們其實就可以開始分析Mybatis的執行流程了。但是,MappedStatement到底是怎麼和對應的Class對象聯繫起來的呢 ?

標識資源已經解析

讓我們現在再回歸XMLMapperBuilderparse()方法上,再次給出該方法源碼:

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

我們之前討論的那麼多都是configurationElement(parser.evalNode("/mapper"));這一行代碼。接下來我們會繼續討論接下來的代碼。這裏我們討論的是configuration.addLoadedResource(resource);

這行代碼負責將已解析的資源添加到一個註冊表中(Configuration的loadedResources屬性中),避免重複解析。這一點功能很明確。但是,除此之外還有另一個功能,實際上Mybatis並不是所有的配置都是XML文件書寫的,有一部分配置是在Java類上的。因此這部分配置也要進行解析。但是解析之後可能有XML配置與Java配置兩者互補的地方。因此就需要避免重複解析,直接調用後面的三個parseXXX方法就好了。而剛纔說到的解析不全的地方,相比通過閱讀上面一節已經很清楚了,就被保存在Configuration對象的incompleteXXX對象中。

將XML文件綁定到對應的Java Class對象上

最後讓我們講解一下XML文件是如何和Java對象結合的,使用過Mybatis的讀者都知道,mapper.xmlnamespace屬性的值就是Java類的權限定類名,那這一步對應是怎麼做的呢?

我們仍然查看XMLMapperBuilderparse()方法上,方法源碼如下:

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

請注意bindMapperForNamespace();這行代碼,方法名已經很明確了,這就是將Mapper Class對象和命名空間整合的地方,讓我們考察該方法的源碼:

private void bindMapperForNamespace() {
    // 獲取當前的命名空間
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        // 通過命名空間獲取對應的Class對象
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound type is not required
      }
      // 如果獲取到的Class對象不爲空
      // 查找有沒有爲其分配MappedStatement
      // 如果沒有則添加解析記錄
      // 並且將Class對象與XML文件聯繫起來(即將Class對象添加到Configuration的knownMappers註冊表中)
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          // Spring may not know the real resource name so we set a flag
          // to prevent loading again this resource from the mapper interface
          // look at MapperAnnotationBuilder#loadXmlResource
          configuration.addLoadedResource("namespace:" + namespace);
          configuration.addMapper(boundType);
        }
      }
    }
  }

通過上面的代碼可以看到,將MappedStatement聯繫到Class對象,僅僅是確保Class對象已經解析過,然後將Class對象放入到Configuration的knownMappers註冊表中。如果沒解析過得話,那就開始解析Java的Class對象,可以考察configuration.addMapper(boundType);方法:

public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
}

可以看到,這就是接下來要講解的基於Java註解配置的Mapper對象的解析方法。所以在解析XML文件配置後緊接着就會解析Java的Class對象。除此之外,上述代碼還有如下部分需要注意:

// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);

說實話,個人認爲,這是一個極其智障的註釋,但是是一個良好的設計,我們知道Mybatis的每一個Mapper對應一個命名空間,由於Mybatis防止資源重複解析是通過Configuration的loadedResources註冊表完成的,該註冊表是一個String的Set,如果該註冊表存入的是資源名稱,那麼就會出現很大的問題。命名空間就將具體的文件與配置進行了解耦。

既然已經提到了基於註解的Mapper標籤解,那麼讓我們進入下一節:《7.Java註解方式配置的mapper標籤的解析》。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章