1. 程式人生 > >Mybaits 原始碼解析 (三)----- Mapper介面底層原理(為什麼Mapper不用寫實現類就能訪問到資料庫?)

Mybaits 原始碼解析 (三)----- Mapper介面底層原理(為什麼Mapper不用寫實現類就能訪問到資料庫?)

上一篇我們講解到mapperElement方法用來解析mapper,我們這篇文章具體來看看mapper.xml的解析過程

mappers配置方式

mappers 標籤下有許多 mapper 標籤,每一個 mapper 標籤中配置的都是一個獨立的對映配置檔案的路徑,配置方式有以下幾種。

介面資訊進行配置

<mappers>
    <mapper class="org.mybatis.mappers.UserMapper"/>
    <mapper class="org.mybatis.mappers.ProductMapper"/>
    <mapper class="org.mybatis.mappers.ManagerMapper"/>
</mappers>

注意:這種方式必須保證介面名(例如UserMapper)和xml名(UserMapper.xml)相同,還必須在同一個包中。因為是通過獲取mapper中的class屬性,拼接上.xml來讀取UserMapper.xml,如果xml檔名不同或者不在同一個包中是無法讀取到xml的。

相對路徑進行配置

<mappers>
    <mapper resource="org/mybatis/mappers/UserMapper.xml"/>
    <mapper resource="org/mybatis/mappers/ProductMapper.xml"/>
    <mapper resource="org/mybatis/mappers/ManagerMapper.xml"/>
</mappers>

注意:這種方式不用保證同介面同包同名。但是要保證xml中的namespase和對應的介面名相同。

絕對路徑進行配置

<mappers>
    <mapper url="file:///var/mappers/UserMapper.xml"/>
    <mapper url="file:///var/mappers/ProductMapper.xml"/>
    <mapper url="file:///var/mappers/ManagerMapper.xml"/>
</mappers>

介面所在包進行配置

<mappers>
    <package name="org.mybatis.mappers"/>
</mappers>

這種方式和第一種方式要求一致,保證介面名(例如UserMapper)和xml名(UserMapper.xml)相同,還必須在同一個包中。

注意:以上所有的配置都要保證xml中的namespase和對應的介面名相同。

我們以packae屬性為例詳細分析一下:

mappers解析入口方法

接上一篇文章最後部分,我們來看看mapperElement方法:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            //包掃描的形式
            if ("package".equals(child.getName())) {
                // 獲取 <package> 節點中的 name 屬性
                String mapperPackage = child.getStringAttribute("name");
                // 從指定包中查詢 所有的 mapper 介面,並根據 mapper 介面解析對映配置
                configuration.addMappers(mapperPackage);
            } else {
                // 獲取 resource/url/class 等屬性
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");

                // resource 不為空,且其他兩者為空,則從指定路徑中載入配置
                if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    // 解析對映檔案
                    mapperParser.parse();
                // url 不為空,且其他兩者為空,則通過 url 載入配置
                } else if (resource == null && url != null && mapperClass == null) {
                    ErrorContext.instance().resource(url);
                    InputStream inputStream = Resources.getUrlAsStream(url);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                    // 解析對映檔案
                    mapperParser.parse();
                // mapperClass 不為空,且其他兩者為空,則通過 mapperClass 解析對映配置
                } else if (resource == null && url == null && mapperClass != null) {
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

在 MyBatis 中,共有四種載入對映檔案或資訊的方式。第一種是從檔案系統中載入對映檔案;第二種是通過 URL 的方式載入和解析對映檔案;第三種是通過 mapper 介面載入對映資訊,對映資訊可以配置在註解中,也可以配置在對映檔案中。最後一種是通過包掃描的方式獲取到某個包下的所有類,並使用第三種方式為每個類解析對映資訊。

我們先看下以packae掃描的形式,看下configuration.addMappers(mapperPackage)方法

public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
}

我們看一下MapperRegistry的addMappers方法:

 1 public void addMappers(String packageName) {
 2     //傳入包名和Object.class型別
 3     this.addMappers(packageName, Object.class);
 4 }
 5 
 6 public void addMappers(String packageName, Class<?> superType) {
 7     ResolverUtil<Class<?>> resolverUtil = new ResolverUtil();
 8     /*
 9      * 查詢包下的父類為 Object.class 的類。
10      * 查詢完成後,查詢結果將會被快取到resolverUtil的內部集合中。上一篇文章我們已經看過這部分的原始碼,不再累述了
11      */ 
12     resolverUtil.find(new IsA(superType), packageName);
13     // 獲取查詢結果
14     Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
15     Iterator i$ = mapperSet.iterator();
16 
17     while(i$.hasNext()) {
18         Class<?> mapperClass = (Class)i$.next();
19         //我們具體看這個方法
20         this.addMapper(mapperClass);
21     }
22 
23 }

其實就是通過 VFS(虛擬檔案系統)獲取指定包下的所有檔案的Class,也就是所有的Mapper介面,然後遍歷每個Mapper介面進行解析,接下來就和第一種配置方式(介面資訊進行配置)一樣的流程了,接下來我們來看看 基於 XML 的對映檔案的解析過程,可以看到先建立一個XMLMapperBuilder,再呼叫其parse()方法:

 1 public void parse() {
 2     // 檢測對映檔案是否已經被解析過
 3     if (!configuration.isResourceLoaded(resource)) {
 4         // 解析 mapper 節點
 5         configurationElement(parser.evalNode("/mapper"));
 6         // 新增資源路徑到“已解析資源集合”中
 7         configuration.addLoadedResource(resource);
 8         // 通過名稱空間繫結 Mapper 介面
 9         bindMapperForNamespace();
10     }
11 
12     parsePendingResultMaps();
13     parsePendingCacheRefs();
14     parsePendingStatements();
15 }

我們重點關注第5行和第9行的邏輯,也就是configurationElement和bindMapperForNamespace方法

解析對映檔案

在 MyBatis 對映檔案中,可以配置多種節點。比如 <cache>,<resultMap>,<sql> 以及 <select | insert | update | delete> 等。下面我們來看一個對映檔案配置示例。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mapper.EmployeeMapper">
    <cache/>
    
    <resultMap id="baseMap" type="entity.Employee">
        <result property="id" column="id" jdbcType="INTEGER"></result>
        <result property="name" column="name" jdbcType="VARCHAR"></result>
    </resultMap>
    
    <sql id="table">
        employee
    </sql>
    
    <select id="getAll" resultMap="baseMap">
        select * from  <include refid="table"/>  WHERE id = #{id}
    </select>
    
    <!-- <insert|update|delete/> -->
</mapper>

接著來看看configurationElement解析mapper.xml中的內容。

 1 private void configurationElement(XNode context) {
 2     try {
 3         // 獲取 mapper 名稱空間,如 mapper.EmployeeMapper
 4         String namespace = context.getStringAttribute("namespace");
 5         if (namespace == null || namespace.equals("")) {
 6             throw new BuilderException("Mapper's namespace cannot be empty");
 7         }
 8 
 9         // 設定名稱空間到 builderAssistant 中
10         builderAssistant.setCurrentNamespace(namespace);
11 
12         // 解析 <cache-ref> 節點
13         cacheRefElement(context.evalNode("cache-ref"));
14 
15         // 解析 <cache> 節點
16         cacheElement(context.evalNode("cache"));
17 
18         // 已廢棄配置,這裡不做分析
19         parameterMapElement(context.evalNodes("/mapper/parameterMap"));
20 
21         // 解析 <resultMap> 節點
22         resultMapElements(context.evalNodes("/mapper/resultMap"));
23 
24         // 解析 <sql> 節點
25         sqlElement(context.evalNodes("/mapper/sql"));
26 
27         // 解析 <select>、<insert>、<update>、<delete> 節點
28         buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
29     } catch (Exception e) {
30         throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
31     }
32 }

接下來我們就對其中關鍵的方法進行詳細分析

解析 cache 節點

MyBatis 提供了一、二級快取,其中一級快取是 SqlSession 級別的,預設為開啟狀態。二級快取配置在對映檔案中,使用者需要顯示配置才能開啟。如下:

<cache/>

也可以使用第三方快取

<cache type="org.mybatis.caches.redis.RedisCache"/>

其中有一些屬性可以選擇

<cache eviction="LRU"  flushInterval="60000"  size="512" readOnly="true"/>
  1. 根據資料的歷史訪問記錄來進行淘汰資料,其核心思想是“如果資料最近被訪問過,那麼將來被訪問的機率也更高”
  2. 快取的容量為 512 個物件引用
  3. 快取每隔60秒重新整理一次
  4. 快取返回的物件是寫安全的,即在外部修改物件不會影響到快取內部儲存物件

下面我們來分析一下快取配置的解析邏輯,如下:

private void cacheElement(XNode context) throws Exception {
    if (context != null) {
        // 獲取type屬性,如果type沒有指定就用預設的PERPETUAL(早已經註冊過的別名的PerpetualCache)
        String type = context.getStringAttribute("type", "PERPETUAL");
        // 根據type從早已經註冊的別名中獲取對應的Class,PERPETUAL對應的Class是PerpetualCache.class
        // 如果我們寫了type屬性,如type="org.mybatis.caches.redis.RedisCache",這裡將會得到RedisCache.class
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        //獲取淘汰方式,預設為LRU(早已經註冊過的別名的LruCache),最近最少使用到的先淘汰
        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 props = context.getChildrenAsProperties();

        // 構建快取物件
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

public <T> Class<T> resolveAlias(String string) {
    try {
        if (string == null) {
            return null;
        } else {
            // 轉換成小寫
            String key = string.toLowerCase(Locale.ENGLISH);
            Class value;
            // 如果沒有設定type屬性,則這裡傳過來的是PERPETUAL,能從別名快取中獲取到PerpetualCache.class
            if (this.TYPE_ALIASES.containsKey(key)) {
                value = (Class)this.TYPE_ALIASES.get(key);
            } else {
                //如果是設定了自定義的type,則在別名快取中是獲取不到的,直接通過類載入,載入自定義的type,如RedisCache.class
                value = Resources.classForName(string);
            }

            return value;
        }
    } catch (ClassNotFoundException var4) {
        throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + var4, var4);
    }
}

快取的構建封裝在 BuilderAssistant 類的 useNewCache 方法中,我們來看看

public Cache useNewCache(Class<? extends Cache> typeClass,
    Class<? extends Cache> evictionClass,Long flushInterval,
    Integer size,boolean readWrite,boolean blocking,Properties props) {

    // 使用建造模式構建快取例項
    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 屬性,即當前使用的快取
    currentCache = cache;
    return cache;
}

上面使用了建造模式構建 Cache 例項,我們跟下去看看。

public Cache build() {
    // 設定預設的快取型別(PerpetualCache)和快取裝飾器(LruCache)
    setDefaultImplementations();

    // 通過反射建立快取
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    // 僅對內建快取 PerpetualCache 應用裝飾器
    if (PerpetualCache.class.equals(cache.getClass())) {
        // 遍歷裝飾器集合,應用裝飾器
        for (Class<? extends Cache> decorator : decorators) {
            // 通過反射建立裝飾器例項
            cache = newCacheDecoratorInstance(decorator, cache);
            // 設定屬性值到快取例項中
            setCacheProperties(cache);
        }
        // 應用標準的裝飾器,比如 LoggingCache、SynchronizedCache
        cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
        // 應用具有日誌功能的快取裝飾器
        cache = new LoggingCache(cache);
    }
    return cache;
}

private void setDefaultImplementations() {
    if (this.implementation == null) {
        //設定預設快取型別為PerpetualCache
        this.implementation = PerpetualCache.class;
        if (this.decorators.isEmpty()) {
            this.decorators.add(LruCache.class);
        }
    }
}

private Cache newBaseCacheInstance(Class<? extends Cache> cacheClass, String id) {
    //獲取構造器
    Constructor cacheConstructor = this.getBaseCacheConstructor(cacheClass);

    try {
        //通過構造器例項化Cache
        return (Cache)cacheConstructor.newInstance(id);
    } catch (Exception var5) {
        throw new CacheException("Could not instantiate cache implementation (" + cacheClass + "). Cause: " + var5, var5);
    }
}

如上就建立好了一個Cache的例項,然後把它新增到Configuration中,並且設定到currentCache屬性中,這個屬性後面還要使用,也就是Cache例項後面還要使用,我們後面再看。

解析 resultMap 節點

resultMap 主要用於對映結果。通過 resultMap 和自動對映,可以讓 MyBatis 幫助我們完成 ResultSet → Object 的對映。下面開始分析 resultMap 配置的解析過程。

private void resultMapElements(List<XNode> list) throws Exception {
    // 遍歷 <resultMap> 節點列表
    for (XNode resultMapNode : list) {
        try {
            // 解析 resultMap 節點
            resultMapElement(resultMapNode);
        } catch (IncompleteElementException e) {
        }
    }
}

private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
    return resultMapElement(resultMapNode, Collections.<ResultMapping>emptyList());
}

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());

    // 獲取 id 和 type 屬性
    String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
    String type = resultMapNode.getStringAttribute("type",
        resultMapNode.getStringAttribute("ofType",
            resultMapNode.getStringAttribute("resultType",
                resultMapNode.getStringAttribute("javaType"))));
    // 獲取 extends 和 autoMapping
    String extend = resultMapNode.getStringAttribute("extends");
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");

    // 獲取 type 屬性對應的型別
    Class<?> typeClass = resolveClass(type);
    Discriminator discriminator = null;
    //建立ResultMapping集合,對應resultMap子節點的id和result節點
    List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
    resultMappings.addAll(additionalResultMappings);

    // 獲取並遍歷 <resultMap> 的子節點列表
    List<XNode> resultChildren = resultMapNode.getChildren();
    for (XNode resultChild : resultChildren) {
        if ("constructor".equals(resultChild.getName())) {
            processConstructorElement(resultChild, typeClass, resultMappings);
        } else if ("discriminator".equals(resultChild.getName())) {
            discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
        } else {

            List<ResultFlag> flags = new ArrayList<ResultFlag>();
            if ("id".equals(resultChild.getName())) {
                // 新增 ID 到 flags 集合中
                flags.add(ResultFlag.ID);
            }
            // 解析 id 和 result 節點,將id或result節點生成相應的 ResultMapping,將ResultMapping新增到resultMappings集合中
            resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
        }
    }
    //建立ResultMapResolver物件
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend,
        discriminator, resultMappings, autoMapping);
    try {
        // 根據前面獲取到的資訊構建 ResultMap 物件
        return resultMapResolver.resolve();
    } catch (IncompleteElementException e) {
        configuration.addIncompleteResultMap(resultMapResolver);
        throw e;
    }
}

解析 id 和 result 節點

在 <resultMap> 節點中,子節點 <id> 和 <result> 都是常規配置,比較常見。我們來看看解析過程

private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
    String property;
    // 根據節點型別獲取 name 或 property 屬性
    if (flags.contains(ResultFlag.CONSTRUCTOR)) {
        property = context.getStringAttribute("name");
    } else {
        property = context.getStringAttribute("property");
    }

    // 獲取其他各種屬性
    String column = context.getStringAttribute("column");
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    String nestedSelect = context.getStringAttribute("select");
    
    /*
     * 解析 resultMap 屬性,該屬性出現在 <association> 和 <collection> 節點中。
     * 若這兩個節點不包含 resultMap 屬性,則呼叫 processNestedResultMappings 方法,遞迴呼叫resultMapElement解析<association> 和 <collection>的巢狀節點,生成resultMap,並返回resultMap.getId();
     * 如果包含resultMap屬性,則直接獲取其屬性值,這個屬性值對應一個resultMap節點
     */
    String nestedResultMap = context.getStringAttribute("resultMap", processNestedResultMappings(context, Collections.<ResultMapping>emptyList()));
    
    String notNullColumn = context.getStringAttribute("notNullColumn");
    String columnPrefix = context.getStringAttribute("columnPrefix");
    String typeHandler = context.getStringAttribute("typeHandler");
    String resultSet = context.getStringAttribute("resultSet");
    String foreignColumn = context.getStringAttribute("foreignColumn");
    boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));

    Class<?> javaTypeClass = resolveClass(javaType);
    Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);

    // 構建 ResultMapping 物件
    return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect,
        nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}

看processNestedResultMappings解析<association> 和 <collection> 節點中的子節點,並返回ResultMap.id

private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings) throws Exception {
    if (("association".equals(context.getName()) || "collection".equals(context.getName()) || "case".equals(context.getName())) && context.getStringAttribute("select") == null) {
        ResultMap resultMap = this.resultMapElement(context, resultMappings);
        return resultMap.getId();
    } else {
        return null;
    }
}

下面以 <association> 節點為例,演示該節點的兩種配置方式,分別如下:

第一種配置方式是通過 resultMap 屬性引用其他的 <resultMap> 節點,配置如下:

<resultMap id="articleResult" type="Article">
    <id property="id" column="id"/>
    <result property="title" column="article_title"/>
    <!-- 引用 authorResult -->
    <association property="article_author" column="article_author_id" javaType="Author" resultMap="authorResult"/>
</resultMap>

<resultMap id="authorResult" type="Author">
    <id property="id" column="author_id"/>
    <result property="name" column="author_name"/>
</resultMap>

第二種配置方式是採取 resultMap 巢狀的方式進行配置,如下:

<resultMap id="articleResult" type="Article">
    <id property="id" column="id"/>
    <result property="title" column="article_title"/>
    <!-- resultMap 巢狀 -->
    <association property="article_author" javaType="Author">
        <id property="id" column="author_id"/>
        <result property="name" column="author_name"/>
    </association>
</resultMap>

第二種配置,<association> 的子節點是一些結果對映配置,這些結果配置最終也會被解析成 ResultMap。

下面分析 ResultMapping 的構建過程。

public ResultMapping buildResultMapping(Class<?> resultType, String property, String column, Class<?> javaType,JdbcType jdbcType, 
    String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix,Class<? extends TypeHandler<?>> typeHandler, 
    List<ResultFlag> flags, String resultSet, String foreignColumn, boolean lazy) {

    // resultType:即 <resultMap type="xxx"/> 中的 type 屬性
    // property:即 <result property="xxx"/> 中的 property 屬性
    Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);

    TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);

    List<ResultMapping> composites = parseCompositeColumnName(column);

    // 通過建造模式構建 ResultMapping
    return new ResultMapping.Builder(configuration, property, column, javaTypeClass)
        .jdbcType(jdbcType)
        .nestedQueryId(applyCurrentNamespace(nestedSelect, true))
        .nestedResultMapId(applyCurrentNamespace(nestedResultMap, true))
        .resultSet(resultSet)
        .typeHandler(typeHandlerInstance)
        .flags(flags == null ? new ArrayList<ResultFlag>() : flags)
        .composites(composites)
        .notNullColumns(parseMultipleColumnNames(notNullColumn))
        .columnPrefix(columnPrefix)
        .foreignColumn(foreignColumn)
        .lazy(lazy)
        .build();
}

private Class<?> resolveResultJavaType(Class<?> resultType, String property, Class<?> javaType) {
    if (javaType == null && property != null) {
        try {
            //獲取ResultMap中的type屬性的元類,如<resultMap id="user" type="java.model.User"/> 中User的元類
            MetaClass metaResultType = MetaClass.forClass(resultType, this.configuration.getReflectorFactory());
            //<result property="name" javaType="String"/>,如果result中沒有設定javaType,則獲取元類屬性對那個的型別
            javaType = metaResultType.getSetterType(property);
        } catch (Exception var5) {
            ;
        }
    }

    if (javaType == null) {
        javaType = Object.class;
    }

    return javaType;
}
    
public ResultMapping build() {
    resultMapping.flags = Collections.unmodifiableList(resultMapping.flags);
    resultMapping.composites = Collections.unmodifiableList(resultMapping.composites);
    resolveTypeHandler();
    validate();
    return resultMapping;
}

我們來看看ResultMapping類

public class ResultMapping {
    private Configuration configuration;
    private String property;
    private String column;
    private Class<?> javaType;
    private JdbcType jdbcType;
    private TypeHandler<?> typeHandler;
    private String nestedResultMapId;
    private String nestedQueryId;
    private Set<String> notNullColumns;
    private String columnPrefix;
    private List<ResultFlag> flags;
    private List<ResultMapping> composites;
    private String resultSet;
    private String foreignColumn;
    private boolean lazy;

    ResultMapping() {
    }
    //略
}

ResultMapping就是和ResultMap中子節點id和result對應

<id column="wi_id" jdbcType="INTEGER"  property="id" />
<result column="warrant_no" jdbcType="String"  jdbcType="CHAR" property="warrantNo" />

ResultMap 物件構建

前面的分析我們知道了<id>,<result> 等節點最終都被解析成了 ResultMapping。並且封裝到了resultMappings集合中,緊接著要做的事情是構建 ResultMap,關鍵程式碼在resultMapResolver.resolve():

public ResultMap resolve() {
    return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
}

public ResultMap addResultMap(
    String id, Class<?> type, String extend, Discriminator discriminator,
    List<ResultMapping> resultMappings, Boolean autoMapping) {
    
    // 為 ResultMap 的 id 和 extend 屬性值拼接名稱空間
    id = applyCurrentNamespace(id, false);
    extend = applyCurrentNamespace(extend, true);

    if (extend != null) {
        if (!configuration.hasResultMap(extend)) {
            throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
        }
        ResultMap resultMap = configuration.getResultMap(extend);
        List<ResultMapping> extendedResultMappings = new ArrayList<ResultMapping>(resultMap.getResultMappings());
        extendedResultMappings.removeAll(resultMappings);
        
        boolean declaresConstructor = false;
        for (ResultMapping resultMapping : resultMappings) {
            if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
                declaresConstructor = true;
                break;
            }
        }
        
        if (declaresConstructor) {
            Iterator<ResultMapping> extendedResultMappingsIter = extendedResultMappings.iterator();
            while (extendedResultMappingsIter.hasNext()) {
                if (extendedResultMappingsIter.next().getFlags().contains(ResultFlag.CONSTRUCTOR)) {
                    extendedResultMappingsIter.remove();
                }
            }
        }
        resultMappings.addAll(extendedResultMappings);
    }

    // 構建 ResultMap
    ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
        .discriminator(discriminator)
        .build();
    // 將建立好的ResultMap加入configuration中
    configuration.addResultMap(resultMap);
    return resultMap;
}

我們先看看ResultMap

public class ResultMap {
    private String id;
    private Class<?> type;
    private List<ResultMapping> resultMappings;
    //用於儲存 <id> 節點對應的 ResultMapping 物件
    private List<ResultMapping> idResultMappings;
    private List<ResultMapping> constructorResultMappings;
    //用於儲存 <id> 和 <result> 節點對應的 ResultMapping 物件
    private List<ResultMapping> propertyResultMappings;
    //用於儲存 所有<id>、<result> 節點 column 屬性
    private Set<String> mappedColumns;
    private Discriminator discriminator;
    private boolean hasNestedResultMaps;
    private boolean hasNestedQueries;
    private Boolean autoMapping;

    private ResultMap() {
    }
    //略
}

再來看看通過建造模式構建 ResultMap 例項

public ResultMap build() {
    if (resultMap.id == null) {
        throw new IllegalArgumentException("ResultMaps must have an id");
    }
    resultMap.mappedColumns = new HashSet<String>();
    resultMap.mappedProperties = new HashSet<String>();
    resultMap.idResultMappings = new ArrayList<ResultMapping>();
    resultMap.constructorResultMappings = new ArrayList<ResultMapping>();
    resultMap.propertyResultMappings = new ArrayList<ResultMapping>();
    final List<String> constructorArgNames = new ArrayList<String>();

    for (ResultMapping resultMapping : resultMap.resultMappings) {
        /*
         * 檢測 <association> 或 <collection> 節點
         * 是否包含 select 和 resultMap 屬性
         */
        resultMap.hasNestedQueries = resultMap.hasNestedQueries || resultMapping.getNestedQueryId() != null;
        resultMap.hasNestedResultMaps =
            resultMap.hasNestedResultMaps || (resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null);

        final String column = resultMapping.getColumn();
        if (column != null) {
            // 將 colum 轉換成大寫,並新增到 mappedColumns 集合中
            resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));
        } else if (resultMapping.isCompositeResult()) {
            for (ResultMapping compositeResultMapping : resultMapping.getComposites()) {
                final String compositeColumn = compositeResultMapping.getColumn();
                if (compositeColumn != null) {
                    resultMap.mappedColumns.add(compositeColumn.toUpperCase(Locale.ENGLISH));
                }
            }
        }

        // 新增屬性 property 到 mappedProperties 集合中
        final String property = resultMapping.getProperty();
        if (property != null) {
            resultMap.mappedProperties.add(property);
        }

        if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
            resultMap.constructorResultMappings.add(resultMapping);
            if (resultMapping.getProperty() != null) {
                constructorArgNames.add(resultMapping.getProperty());
            }
        } else {
            // 新增 resultMapping 到 propertyResultMappings 中
            resultMap.propertyResultMappings.add(resultMapping);
        }

        if (resultMapping.getFlags().contains(ResultFlag.ID)) {
            // 新增 resultMapping 到 idResultMappings 中
            resultMap.idResultMappings.add(resultMapping);
        }
    }
    if (resultMap.idResultMappings.isEmpty()) {
        resultMap.idResultMappings.addAll(resultMap.resultMappings);
    }
    if (!constructorArgNames.isEmpty()) {
        final List<String> actualArgNames = argNamesOfMatchingConstructor(constructorArgNames);
        if (actualArgNames == null) {
            throw new BuilderException("Error in result map '" + resultMap.id
                + "'. Failed to find a constructor in '"
                + resultMap.getType().getName() + "' by arg names " + constructorArgNames
                + ". There might be more info in debug log.");
        }
        Collections.sort(resultMap.constructorResultMappings, new Comparator<ResultMapping>() {
            @Override
            public int compare(ResultMapping o1, ResultMapping o2) {
                int paramIdx1 = actualArgNames.indexOf(o1.getProperty());
                int paramIdx2 = actualArgNames.indexOf(o2.getProperty());
                return paramIdx1 - paramIdx2;
            }
        });
    }

    // 將以下這些集合變為不可修改集合
    resultMap.resultMappings = Collections.unmodifiableList(resultMap.resultMappings);
    resultMap.idResultMappings = Collections.unmodifiableList(resultMap.idResultMappings);
    resultMap.constructorResultMappings = Collections.unmodifiableList(resultMap.constructorResultMappings);
    resultMap.propertyResultMappings = Collections.unmodifiableList(resultMap.propertyResultMappings);
    resultMap.mappedColumns = Collections.unmodifiableSet(resultMap.mappedColumns);
    return resultMap;
}

主要做的事情就是將 ResultMapping 例項及屬性分別儲存到不同的集合中。

解析 sql 節點

<sql> 節點用來定義一些可重用的 SQL 語句片段,比如表名,或表的列名等。在對映檔案中,我們可以通過 <include> 節點引用 <sql> 節點定義的內容。

<sql id="table">
    user
</sql>

<select id="findOne" resultType="Article">
    SELECT * FROM <include refid="table"/> WHERE id = #{id}
</select>

下面分析一下 sql 節點的解析過程,如下:

private void sqlElement(List<XNode> list) throws Exception {
    if (configuration.getDatabaseId() != null) {
        // 呼叫 sqlElement 解析 <sql> 節點
        sqlElement(list, configuration.getDatabaseId());
    }

    // 再次呼叫 sqlElement,不同的是,這次呼叫,該方法的第二個引數為 null
    sqlElement(list, null);
}

private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
    for (XNode context : list) {
        // 獲取 id 和 databaseId 屬性
        String databaseId = context.getStringAttribute("databaseId");
        String id = context.getStringAttribute("id");

        // id = currentNamespace + "." + id
        id = builderAssistant.applyCurrentNamespace(id, false);

        // 檢測當前 databaseId 和 requiredDatabaseId 是否一致
        if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
            // 將 <id, XNode> 鍵值對快取到XMLMapperBuilder物件的 sqlFragments 屬性中,以供後面的sql語句使用
            sqlFragments.put(id, context);
        }
    }
}

解析select|insert|update|delete節點

 <select>、<insert>、<update> 以及 <delete> 等節點統稱為 SQL 語句節點,其解析過程在buildStatementFromContext方法中:

private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        // 呼叫過載方法構建 Statement
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        // 建立 XMLStatementBuilder 建造類
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
            /*
             * 解析sql節點,將其封裝到 Statement 物件中,並將解析結果儲存到 configuration 的 mappedStatements 集合中
             */
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

我們繼續看 statementParser.parseStatementNode();

public void parseStatementNode() {
    // 獲取 id 和 databaseId 屬性
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        return;
    }

    // 獲取各種屬性
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // 通過別名解析 resultType 對應的型別
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    
    // 解析 Statement 型別,預設為 PREPARED
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    
    // 解析 ResultSetType
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    // 獲取節點的名稱,比如 <select> 節點名稱為 select
    String nodeName = context.getNode().getNodeName();
    // 根據節點名稱解析 SqlCommandType
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    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());

    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // 解析 SQL 語句
    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;
    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);
}

我們主要來分析下面幾個重要的方法:

  1. 解析 <include> 節點
  2. 解析 SQL,獲取 SqlSource
  3. 構建 MappedStatement 例項

解析 <include> 節點

先來看一個include的例子

<mapper namespace="java.mybaits.dao.UserMapper">
    <sql id="table">
        user
    </sql>

    <select id="findOne" resultType="User">
        SELECT  * FROM  <include refid="table"/> WHERE id = #{id}
    </select>
</mapper>

<include> 節點的解析邏輯封裝在 applyIncludes 中,該方法的程式碼如下:

public void applyIncludes(Node source) {
    Properties variablesContext = new Properties();
    Properties configurationVariables = configuration.getVariables();
    if (configurationVariables != null) {
        // 將 configurationVariables 中的資料新增到 variablesContext 中
        variablesContext.putAll(configurationVariables);
    }

    // 呼叫過載方法處理 <include> 節點
    applyIncludes(source, variablesContext, false);
}

繼續看 applyIncludes 方法

private void applyIncludes(Node source, final Properties variablesContext, boolean included) {

    // 第一個條件分支
    if (source.getNodeName().equals("include")) {

        //獲取 <sql> 節點。
        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);
        }
        // 將 <select>節點中的 <include> 節點替換為 <sql> 節點
        source.getParentNode().replaceChild(toInclude, source);
        while (toInclude.hasChildNodes()) {
            // 將 <sql> 中的內容插入到 <sql> 節點之前
            toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
        }

        /*
         * 前面已經將 <sql> 節點的內容插入到 dom 中了,
         * 現在不需要 <sql> 節點了,這裡將該節點從 dom 中移除
         */
        toInclude.getParentNode().removeChild(toInclude);

    // 第二個條件分支
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
        if (included && !variablesContext.isEmpty()) {
            NamedNodeMap attributes = source.getAttributes();
            for (int i = 0; i < attributes.getLength(); i++) {
                Node attr = attributes.item(i);
                // 將 source 節點屬性中的佔位符 ${} 替換成具體的屬性值
                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);
        }
        
    // 第三個條件分支
    } else if (included && source.getNodeType() == Node.TEXT_NODE && !variablesContext.isEmpty()) {
        // 將文字(text)節點中的屬性佔位符 ${} 替換成具體的屬性值
        source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
}

我們先來看一下 applyIncludes 方法第一次被呼叫時的狀態,source為<select> 節點,節點型別:ELEMENT_NODE,此時會進入第二個分支,獲取到獲取 <select> 子節點列表,遍歷子節點列表,將子節點作為引數,進行遞迴呼叫applyIncludes ,此時可獲取到的子節點如下:

編號子節點型別描述
1 SELECT * FROM TEXT_NODE 文字節點
2 <include refid="table"/> ELEMENT_NODE 普通節點
3 WHERE id = #{id} TEXT_NODE 文字節點

接下來要做的事情是遍歷列表,然後將子節點作為引數進行遞迴呼叫。第一個子節點呼叫applyIncludes方法,source為 SELECT * FROM 節點,節點型別:TEXT_NODE,進入分支三,沒有${},不會替換,則節點一結束返回,什麼都沒有做。第二個節點呼叫applyIncludes方法,此時source為 <include refid="table"/>節點,節點型別:ELEMENT_NODE,進入分支一,通過refid找到 sql 節點,也就是toInclude節點,然後執行source.getParentNode().replaceChild(toInclude, source);,直接將<include refid="table"/>節點的父節點,也就是<select> 節點中的當前<include >節點替換成 <sql> 節點,然後呼叫toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);,將 <sql> 中的內容插入到 <sql> 節點之前,也就是將user插入到 <sql> 節點之前,現在不需要 <sql> 節點了,最後將該節點從 dom 中移除

建立SqlSource

建立SqlSource在createSqlSource方法中

public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
}

// -☆- XMLScriptBuilder
public SqlSource parseScriptNode() {
    // 解析 SQL 語句節點
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    // 根據 isDynamic 狀態建立不同的 SqlSource
    if (isDynamic) {
        sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
        sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}

繼續跟進parseDynamicTags

/** 該方法用於初始化 nodeHandlerMap 集合,該集合後面會用到 */
private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
}
    
protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<SqlNode>();
    NodeList children = node.getNode().getChildNodes();
    // 遍歷子節點
    for (int i = 0; i < children.getLength(); i++) {
        XNode child = node.newXNode(children.item(i));
        //如果節點是TEXT_NODE型別
        if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
            // 獲取文字內容
            String data = child.getStringBody("");
            TextSqlNode textSqlNode = new TextSqlNode(data);
            // 若文字中包含 ${} 佔位符,會被認為是動態節點
            if (textSqlNode.isDynamic()) {
                contents.add(textSqlNode);
                // 設定 isDynamic 為 true
                isDynamic = true;
            } else {
                // 建立 StaticTextSqlNode
                contents.add(new StaticTextSqlNode(data));
            }

        // child 節點是 ELEMENT_NODE 型別,比如 <if>、<where> 等
        } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
            // 獲取節點名稱,比如 if、where、trim 等
            String nodeName = child.getNode().getNodeName();
            // 根據節點名稱獲取 NodeHandler,也就是上面註冊的nodeHandlerMap
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler == null) {
                throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
            }
            // 處理 child 節點,生成相應的 SqlNode
            handler.handleNode(child, contents);

            // 設定 isDynamic 為 true
            isDynamic = true;
        }
    }
    return new MixedSqlNode(contents);
}

對於if、trim、where等這些動態節點,是通過對應的handler來解析的,如下

handler.handleNode(child, contents);

該程式碼用於處理動態 SQL 節點,並生成相應的 SqlNode。下面來簡單分析一下 WhereHandler 的程式碼。

/** 定義在 XMLScriptBuilder 中 */
private class WhereHandler implements NodeHandler {

    public WhereHandler() {
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 呼叫 parseDynamicTags 解析 <where> 節點
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        // 建立 WhereSqlNode
        WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
        // 新增到 targetContents
        targetContents.add(where);
    }
}

我們已經將 XML 配置解析了 SqlSource,下面我們看看MappedStatement的構建。

構建MappedStatement

SQL 語句節點可以定義很多屬性,這些屬性和屬性值最終儲存在 MappedStatement 中。

public MappedStatement addMappedStatement(
    String id, SqlSource sqlSource, StatementType statementType, 
    SqlCommandType sqlCommandType,Integer fetchSize, Integer timeout, 
    String parameterMap, Class<?> parameterType,String resultMap, 
    Class<?> resultType, ResultSetType resultSetType, boolean flushCache,
    boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, 
    String keyProperty,String keyColumn, String databaseId, 
    LanguageDriver lang, String resultSets) {

    if (unresolvedCacheRef) {
        throw new IncompleteElementException("Cache-ref not yet resolved");
    }
  // 拼接上名稱空間,如 <select id="findOne" resultType="User">,則id=java.mybaits.dao.UserMapper.findOne
    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    // 建立建造器,設定各種屬性
    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))
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .resultSetType(resultSetType).useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);//這裡用到了前面解析<cache>節點時建立的Cache物件,設定到MappedStatement物件裡面的cache屬性中

    // 獲取或建立 ParameterMap
    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
        statementBuilder.parameterMap(statementParameterMap);
    }

    // 構建 MappedStatement
    MappedStatement statement = statementBuilder.build();
    // 新增 MappedStatement 到 configuration 的 mappedStatements 集合中
    // 通過UserMapper代理物件呼叫findOne方法時,就可以拼接UserMapper介面名java.mybaits.dao.UserMapper和findOne方法找到id=java.mybaits.dao.UserMapper的MappedStatement,然後執行對應的sql語句
    configuration.addMappedStatement(statement);
    return statement;
}

這裡我們要注意,MappedStatement物件中有一個cache屬性,將前面解析<cache>節點時建立的Cache物件,設定到MappedStatement物件裡面的cache屬性中,以備後面二級快取使用,我們後面專門來講這一塊。

Mapper 介面繫結

對映檔案解析完成後,我們需要通過名稱空間將繫結 mapper 介面,看看具體繫結的啥

private void bindMapperForNamespace() {
    // 獲取對映檔案的名稱空間
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
        Class<?> boundType = null;
        try {
            // 根據名稱空間解析 mapper 型別
            boundType = Resources.classForName(namespace);
        } catch (ClassNotFoundException e) {
        }
        if (boundType != null) {
            // 檢測當前 mapper 類是否被繫結過
            if (!configuration.hasMapper(boundType)) {
                configuration.addLoadedResource("namespace:" + namespace);
                // 繫結 mapper 類
                configuration.addMapper(boundType);
            }
        }
    }
}

// Configuration
public <T> void addMapper(Class<T> type) {
    // 通過 MapperRegistry 繫結 mapper 類
    mapperRegistry.addMapper(type);
}

// MapperRegistry
public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
        if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
        }
        boolean loadCompleted = false;
        try {
            /*
             * 將 type 和 MapperProxyFactory 進行繫結,MapperProxyFactory 可為 mapper 介面生成代理類
             */
            knownMappers.put(type, new MapperProxyFactory<T>(type));
            
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            // 解析註解中的資訊
            parser.parse();
            loadCompleted = true;
        } finally {
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}

其實就是獲取當前對映檔案的名稱空間,並獲取其Class,也就是獲取每個Mapper介面,然後為每個Mapper介面建立一個代理類工廠,new MapperProxyFactory<T>(type),並放進 knownMappers 這個HashMap中,我們來看看這個MapperProxyFactory

public class MapperProxyFactory<T> {
    //存放Mapper介面Class
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap();

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public Class<T> getMapperInterface() {
        return this.mapperInterface;
    }

    public Map<Method, MapperMethod> getMethodCache() {
        return this.methodCache;
    }

    protected T newInstance(MapperProxy<T> mapperProxy) {
        //生成mapperInterface的代理類
        return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
    }

    public T newInstance(SqlSession sqlSession) {
        MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
        return this.newInstance(mapperProxy);
    }
}

這一塊我們後面文章再來看是如何呼叫的。