MyBatis 源碼分析 - 配置文件解析過程


* 本文速覽

由於本篇文章篇幅比較大,所以這里拿出一節對本文進行快速概括。本篇文章對 MyBatis 配置文件中常用配置的解析過程進行了較為詳細的介紹和分析,包括但不限於settingstypeAliasestypeHandlers等,本文的篇幅也主要在對這三個配置解析過程的分析上。下面,我們來一起看一下本篇文章的目錄結構。

從目錄上可以看出,2.3節、2.5節和2.8節的內容比較多。其中2.3節是關於settings配置解析過程的分析,除了對常規的 XML 解析過程分析,本節額外的分析了元信息類MetaClass源碼的邏輯。2.5節則是詳細分析了別名注冊的過程,包含自動注冊和手動注冊別名等兩種方式。2.8節則是詳細介紹了類型處理器的注冊過程,類型注冊邏輯是封裝在TypeHandlerRegistry類中的各個register重載方法中。由於重載方法比較多,且互為調用,調用關系比較復雜。為此,我專門畫了一張方法調用關系圖。這張圖在分析類類型處理器注冊那一塊的源碼時,會很有用。

本文的2.9節主要用於分析 SQL 映射文件的解析過程。由於 SQL 映射文件解析的過程也很復雜,所以這里把2.9節獨立成文,后續會進行更新。至於其他的章節,沒什么太復雜的東西,就不一一敘述了。

以上就是 MyBatis 配置文件解析過程的速覽,如果大家對以上所說內容比較熟悉了,那就不用往下看了。如果不了解,或是有興趣的話,不妨閱讀一下。本篇文章行文較長,除了對常規的 XML 解析過程進行分析,還額外分析了一些源碼。如果能掌握本文所分析內容,我相信可以對 MyBatis 有更深入的了解。好了,其他的就不多說了,進入正題吧。

1.簡介

在上一篇文章中,我介紹了 MyBatis 的一些基礎知識,用於為本文及后續的源碼分析文章進行鋪墊。經過前面的鋪墊,我覺得是時候后分析一下 MyBatis 源碼了。在本篇文章中,我將從 MyBatis 解析配置文件的過程着手進行分析。並會在分析的過程中,向大家介紹一些配置的使用方式和用途。MyBatis 的配置比較豐富,很難在一篇文章中把所有配置的解析過程分析完。所以關於配置文件的解析,這里會分兩篇文章進行講解。本篇文章將會分析諸如settingstypeAliases以及typeHandlers等標簽的解析過程。下一篇文章則會重點介紹 SQL 映射文件的解析過程。本系列文章所分析的源碼版本為3.4.6,是 MyBatis 最新的版本。好了,其他的就不多說了,下面進入源碼分析階段。

2.配置文件解析過程分析

2.1 配置文件解析入口

在單獨使用 MyBatis 時,第一步要做的事情就是根據配置文件構建SqlSessionFactory對象。相關代碼如下:

String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

首先,我們使用 MyBatis 提供的工具類 Resources 加載配置文件,得到一個輸入流。然后再通過 SqlSessionFactoryBuilder 對象的build方法構建 SqlSessionFactory 對象。所以這里的 build 方法是我們分析配置文件解析過程的入口方法。那下面我們來看一下這個方法的代碼:

// -☆- SqlSessionFactoryBuilder
public SqlSessionFactory build(InputStream inputStream) {
    // 調用重載方法
    return build(inputStream, null, null);
}

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        // 創建配置文件解析器
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        // 調用 parse 方法解析配置文件,生成 Configuration 對象
        return build(parser.parse());
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
        ErrorContext.instance().reset();
        try {
        inputStream.close();
        } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
        }
    }
}

public SqlSessionFactory build(Configuration config) {
    // 創建 DefaultSqlSessionFactory
    return new DefaultSqlSessionFactory(config);
}

從上面的代碼中,我們大致可以猜出 MyBatis 配置文件是通過XMLConfigBuilder進行解析的。不過目前這里還沒有非常明確的解析邏輯,所以我們繼續往下看。這次來看一下 XMLConfigBuilder 的parse方法,如下:

// -☆- XMLConfigBuilder
public Configuration parse() {
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // 解析配置
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

到這里大家可以看到一些端倪了,注意一個 xpath 表達式 - /configuration。這個表達式代表的是 MyBatis 的<configuration/>標簽,這里選中這個標簽,並傳遞給parseConfiguration方法。我們繼續跟下去。

private void parseConfiguration(XNode root) {
    try {
        // 解析 properties 配置
        propertiesElement(root.evalNode("properties"));

        // 解析 settings 配置,並將其轉換為 Properties 對象
        Properties settings = settingsAsProperties(root.evalNode("settings"));

        // 加載 vfs
        loadCustomVfs(settings);

        // 解析 typeAliases 配置
        typeAliasesElement(root.evalNode("typeAliases"));

        // 解析 plugins 配置
        pluginElement(root.evalNode("plugins"));

        // 解析 objectFactory 配置
        objectFactoryElement(root.evalNode("objectFactory"));

        // 解析 objectWrapperFactory 配置
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));

        // 解析 reflectorFactory 配置
        reflectorFactoryElement(root.evalNode("reflectorFactory"));

        // settings 中的信息設置到 Configuration 對象中
        settingsElement(settings);

        // 解析 environments 配置
        environmentsElement(root.evalNode("environments"));

        // 解析 databaseIdProvider,獲取並設置 databaseId 到 Configuration 對象
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));

        // 解析 typeHandlers 配置
        typeHandlerElement(root.evalNode("typeHandlers"));

        // 解析 mappers 配置
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

到此,一個 MyBatis 的解析過程就出來了,每個配置的解析邏輯都封裝在了相應的方法中。在下面分析過程中,我不打算按照方法調用的順序進行分析,我會適當進行一定的調整。同時,MyBatis 中配置較多,對於一些不常用的配置,這里會略過。那下面我們開始進行分析吧。

2.2 解析 properties 配置

解析properties節點是由propertiesElement這個方法完成的,該方法的邏輯比較簡單。在分析方法源碼前,先來看一下 properties 節點的配置內容。如下:

<properties resource="jdbc.properties">
    <property name="jdbc.username" value="coolblog"/>
    <property name="hello" value="world"/>
</properties>

在上面的配置中,我為 properties 節點配置了一個 resource 屬性,以及兩個子節點。下面我們參照上面的配置,來分析一下 propertiesElement 的邏輯。相關分析如下。

// -☆- XMLConfigBuilder
private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
        // 解析 propertis 的子節點,並將這些節點內容轉換為屬性對象 Properties
        Properties defaults = context.getChildrenAsProperties();
        // 獲取 propertis 節點中的 resource 和 url 屬性值
        String resource = context.getStringAttribute("resource");
        String url = context.getStringAttribute("url");

        // 兩者都不用空,則拋出異常
        if (resource != null && url != null) {
            throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
        }
        if (resource != null) {
            // 從文件系統中加載並解析屬性文件
            defaults.putAll(Resources.getResourceAsProperties(resource));
        } else if (url != null) {
            // 通過 url 加載並解析屬性文件
            defaults.putAll(Resources.getUrlAsProperties(url));
        }
        Properties vars = configuration.getVariables();
        if (vars != null) {
            defaults.putAll(vars);
        }
        parser.setVariables(defaults);
        // 將屬性值設置到 configuration 中
        configuration.setVariables(defaults);
    }
}

public Properties getChildrenAsProperties() {
    Properties properties = new Properties();
    // 獲取並遍歷子節點
    for (XNode child : getChildren()) {
        // 獲取 property 節點的 name 和 value 屬性
        String name = child.getStringAttribute("name");
        String value = child.getStringAttribute("value");
        if (name != null && value != null) {
            // 設置屬性到屬性對象中
            properties.setProperty(name, value);
        }
    }
    return properties;
}

// -☆- XNode
public List<XNode> getChildren() {
    List<XNode> children = new ArrayList<XNode>();
    // 獲取子節點列表
    NodeList nodeList = node.getChildNodes();
    if (nodeList != null) {
        for (int i = 0, n = nodeList.getLength(); i < n; i++) {
            Node node = nodeList.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                // 將節點對象封裝到 XNode 中,並將 XNode 對象放入 children 列表中
                children.add(new XNode(xpathParser, node, variables));
            }
        }
    }
    return children;
}

上面是 properties 節點解析的主要過程,不是很復雜。主要包含三個步驟,一是解析 properties 節點的子節點,並將解析結果設置到 Properties 對象中。二是從文件系統或通過網絡讀取屬性配置,這取決於 properties 節點的 resource 和 url 是否為空。第二步對應的代碼比較簡單,這里就不分析了。有興趣的話,大家可以自己去看看。最后一步則是將解析出的屬性對象設置到 XPathParser 和 Configuration 對象中。

需要注意的是,propertiesElement 方法是先解析 properties 節點的子節點內容,后再從文件系統或者網絡讀取屬性配置,並將所有的屬性及屬性值都放入到 defaults 屬性對象中。這就會存在同名屬性覆蓋的問題,也就是從文件系統,或者網絡上讀取到的屬性及屬性值會覆蓋掉 properties 子節點中同名的屬性和及值。比如上面配置中的jdbc.properties內容如下:

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/myblog?...
jdbc.username=root
jdbc.password=1234

與 properties 子節點內容合並后,結果如下:

如上,原jdbc.username值為coolblog,現在被覆蓋為了root。同名屬性覆蓋的問題需要大家注意一下,其他的就沒什么了,繼續往下分析。

2.3 解析 settings 配置

2.3.1 settings 節點的解析過程

settings 相關配置是 MyBatis 中非常重要的配置,這些配置用於調整 MyBatis 運行時的行為。settings 配置繁多,在對這些配置不熟悉的情況下,保持默認配置即可。關於 settings 相關配置,MyBatis 官網上進行了比較詳細的描述,大家可以去了解一下。在本節中,暫時還用不到這些配置,所以即使不了解這些配置也沒什么關系。下面先來看一個比較簡單的配置,如下:

<settings>
    <setting name="cacheEnabled" value="true"/>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="autoMappingBehavior" value="PARTIAL"/>
</settings>

接下來,對照上面的配置,來分析源碼。如下:

// -☆- XMLConfigBuilder
private Properties settingsAsProperties(XNode context) {
    if (context == null) {
        return new Properties();
    }
    // 獲取 settings 子節點中的內容,getChildrenAsProperties 方法前面已分析過,這里不再贅述
    Properties props = context.getChildrenAsProperties();

    // 創建 Configuration 類的“元信息”對象
    MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
    for (Object key : props.keySet()) {
        // 檢測 Configuration 中是否存在相關屬性,不存在則拋出異常
        if (!metaConfig.hasSetter(String.valueOf(key))) {
            throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
        }
    }
    return props;
}

如上,settingsAsProperties 方法看起來並不復雜,不過這是一個假象。在上面的代碼中出現了一個陌生的類MetaClass,這個類是用來做什么的呢?答案是用來解析目標類的一些元信息,比如類的成員變量,getter/setter 方法等。關於這個類的邏輯,待會我會詳細解析。接下來,簡單總結一下上面代碼的邏輯。如下:

  1. 解析 settings 子節點的內容,並將解析結果轉成 Properties 對象
  2. 為 Configuration 創建元信息對象
  3. 通過 MetaClass 檢測 Configuration 中是否存在某個屬性的 setter 方法,不存在則拋異常
  4. 若通過 MetaClass 的檢測,則返回 Properties 對象,方法邏輯結束

下面,我們來重點關注一下第2步和第3步的流程。這兩步流程對應的代碼較為復雜,需要一點耐心閱讀。好了,下面開始分析。

2.3.2 元信息對象創建過程

元信息類MetaClass的構造方法為私有類型,所以不能直接創建,必須使用其提供的forClass方法進行創建。它的創建邏輯如下:

public class MetaClass {
    private final ReflectorFactory reflectorFactory;
    private final Reflector reflector;

    private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) {
        this.reflectorFactory = reflectorFactory;
        // 根據類型創建 Reflector
        this.reflector = reflectorFactory.findForClass(type);
    }

    public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) {
        // 調用構造方法
        return new MetaClass(type, reflectorFactory);
    }

    // 省略其他方法
}

上面的代碼看起來很簡單,不過這只是冰山一角。上面代碼出現了兩個新的類ReflectorFactoryReflector,MetaClass 通過引入這些新類幫助它完成功能。下面我們看一下hasSetter方法的源碼就知道是怎么回事了。

// -☆- MetaClass
public boolean hasSetter(String name) {
    // 屬性分詞器,用於解析屬性名
    PropertyTokenizer prop = new PropertyTokenizer(name);
    // hasNext 返回 true,則表明 name 是一個復合屬性,后面會進行分析
    if (prop.hasNext()) {
        // 調用 reflector 的 hasSetter 方法
        if (reflector.hasSetter(prop.getName())) {
            // 為屬性創建創建 MetaClass
            MetaClass metaProp = metaClassForProperty(prop.getName());
            // 再次調用 hasSetter
            return metaProp.hasSetter(prop.getChildren());
        } else {
            return false;
        }
    } else {
        // 調用 reflector 的 hasSetter 方法
        return reflector.hasSetter(prop.getName());
    }
}

從上面的代碼中,我們可以看出 MetaClass 中的 hasSetter 方法最終調用了 Reflector 的 hasSetter 方法。關於 Reflector 的 hasSetter 方法,這里先不分析,Reflector 這個類的邏輯較為復雜,本節會在隨后進行詳細說明。下面來簡單介紹一下上面代碼中出現的幾個類:

  1. ReflectorFactory -> 顧名思義,Reflector 的工廠類,兼有緩存 Reflector 對象的功能
  2. Reflector -> 反射器,用於解析和存儲目標類中的元信息
  3. PropertyTokenizer -> 屬性名分詞器,用於處理較為復雜的屬性名

上面的描述比較簡單,僅從上面的描述中,還不能讓大家有更深入的理解。所以下面單獨分析一下這幾個類的邏輯,首先是ReflectorFactory。ReflectorFactory 是一個接口,MyBatis 中目前只有一個實現類DefaultReflectorFactory,它的分析如下:

2.3.2.1 DefaultReflectorFactory 源碼分析

DefaultReflectorFactory 用於創建 Reflector,同時兼有緩存的功能,它的源碼如下。

public class DefaultReflectorFactory implements ReflectorFactory {

    private boolean classCacheEnabled = true;
    /** 目標類和反射器映射緩存 */
    private final ConcurrentMap<Class<?>, Reflector> reflectorMap = new ConcurrentHashMap<Class<?>, Reflector>();

    // 省略部分代碼

    @Override
    public Reflector findForClass(Class<?> type) {
        // classCacheEnabled 默認為 true
        if (classCacheEnabled) {
            // 從緩存中獲取 Reflector 對象
            Reflector cached = reflectorMap.get(type);
            // 緩存為空,則創建一個新的 Reflector 實例,並放入緩存中
            if (cached == null) {
                cached = new Reflector(type);
                // 將 <type, cached> 映射緩存到 map 中,方便下次取用
                reflectorMap.put(type, cached);
            }
            return cached;
        } else {
            // 創建一個新的 Reflector 實例
            return new Reflector(type);
        }
    }
}

如上,DefaultReflectorFactory 的findForClass方法邏輯不是很復雜,包含兩個訪存操作,和一個對象創建操作。代碼注釋的比較清楚了,就不多說了。接下來,來分析一下反射器 Reflector。

2.3.2.2 Reflector 源碼分析

本小節,我們來看一下 Reflector 的源碼。Reflector 這個類的用途主要是是通過反射獲取目標類的 getter 方法及其返回值類型,setter 方法及其參數值類型等元信息。並將獲取到的元信息緩存到相應的集合中,供后續使用。Reflector 本身代碼比較多,這里不能一一分析。本小節,我將會分析三部分邏輯,分別如下:

  1. Reflector 構造方法及成員變量分析
  2. getter 方法解析過程
  3. setter 方法解析過程

下面我們按照這個步驟進行分析,先來分析 Reflector 構造方法。

● Reflector 構造方法及成員變量分析

Reflector 構造方法中包含了很多初始化邏輯,目標類的元信息解析過程也是在構造方法中完成的,這些元信息最終會被保存到 Reflector 的成員變量中。下面我們先來看看 Reflector 的構造方法和相關的成員變量定義,代碼如下:

public class Reflector {

    private final Class<?> type;
    private final String[] readablePropertyNames;
    private final String[] writeablePropertyNames;
    private final Map<String, Invoker> setMethods = new HashMap<String, Invoker>();
    private final Map<String, Invoker> getMethods = new HashMap<String, Invoker>();
    private final Map<String, Class<?>> setTypes = new HashMap<String, Class<?>>();
    private final Map<String, Class<?>> getTypes = new HashMap<String, Class<?>>();
    private Constructor<?> defaultConstructor;

    private Map<String, String> caseInsensitivePropertyMap = new HashMap<String, String>();

    public Reflector(Class<?> clazz) {
        type = clazz;
        // 解析目標類的默認構造方法,並賦值給 defaultConstructor 變量
        addDefaultConstructor(clazz);

        // 解析 getter 方法,並將解析結果放入 getMethods 中
        addGetMethods(clazz);

        // 解析 setter 方法,並將解析結果放入 setMethods 中
        addSetMethods(clazz);

        // 解析屬性字段,並將解析結果添加到 setMethods 或 getMethods 中
        addFields(clazz);

        // 從 getMethods 映射中獲取可讀屬性名數組
        readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]);

        // 從 setMethods 映射中獲取可寫屬性名數組
        writeablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]);

        // 將所有屬性名的大寫形式作為鍵,屬性名作為值,存入到 caseInsensitivePropertyMap 中
        for (String propName : readablePropertyNames) {
            caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
        }
        for (String propName : writeablePropertyNames) {
            caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
        }
    }

    // 省略其他方法
}

如上,Reflector 的構造方法看起來略為復雜,不過好在一些比較復雜的邏輯都封裝在了相應的方法中,這樣整體的邏輯就比較清晰了。Reflector 構造方法所做的事情均已進行了注釋,大家對照着注釋先看一下。相關方法的細節待會會進行分析。看完構造方法,下面我來通過表格的形式,列舉一下 Reflector 部分成員變量的用途。如下:

變量名 類型 用途
readablePropertyNames String[] 可讀屬性名稱數組,用於保存 getter 方法對應的屬性名稱
writeablePropertyNames String[] 可寫屬性名稱數組,用於保存 setter 方法對應的屬性名稱
setMethods Map<String, Invoker>  用於保存屬性名稱到 Invoke 的映射。setter 方法會被封裝到 MethodInvoker 對象中,Invoke 實現類比較簡單,大家自行分析
getMethods Map<String, Invoker>  用於保存屬性名稱到 Invoke 的映射。同上,getter 方法也會被封裝到 MethodInvoker 對象中
setTypes Map<String, Class<?>> 用於保存 setter 對應的屬性名與參數類型的映射
getTypes Map<String, Class<?>> 用於保存 getter 對應的屬性名與返回值類型的映射
caseInsensitivePropertyMap Map<String, String>  用於保存大寫屬性名與屬性名之間的映射,比如 <NAME, name>

上面列舉了一些集合變量,這些變量用於緩存各種原信息。關於這些變量,這里描述的不太好懂,主要是不太好解釋。要想了解這些變量更多的細節,還是要深入到源碼中。所以我們成熱打鐵,繼續往下分析。

● getter 方法解析過程

getter 方法解析的邏輯被封裝在了addGetMethods方法中,這個方法除了會解析形如getXXX的方法,同時也會解析isXXX方法。該方法的源碼分析如下:

private void addGetMethods(Class<?> cls) {
    Map<String, List<Method>> conflictingGetters = new HashMap<String, List<Method>>();
    // 獲取當前類,接口,以及父類中的方法。該方法邏輯不是很復雜,這里就不展開了
    Method[] methods = getClassMethods(cls);
    for (Method method : methods) {
        // getter 方法不應該有參數,若存在參數,則忽略當前方法
        if (method.getParameterTypes().length > 0) {
            continue;
        }
        String name = method.getName();
        // 過濾出以 get 或 is 開頭的方法
        if ((name.startsWith("get") && name.length() > 3)
            || (name.startsWith("is") && name.length() > 2)) {
            // 將 getXXX 或 isXXX 等方法名轉成相應的屬性,比如 getName -> name
            name = PropertyNamer.methodToProperty(name);
            /*
             * 將沖突的方法添加到 conflictingGetters 中。考慮這樣一種情況:
             * 
             * getTitle 和 isTitle 兩個方法經過 methodToProperty 處理,
             * 均得到 name = title,這會導致沖突。
             *
             * 對於沖突的方法,這里先統一起存起來,后續再解決沖突
             */
            addMethodConflict(conflictingGetters, name, method);
        }
    }

    // 解決 getter 沖突
    resolveGetterConflicts(conflictingGetters);
}

如上,addGetMethods 方法的執行流程如下:

  1. 獲取當前類,接口,以及父類中的方法
  2. 遍歷上一步獲取的方法數組,並過濾出以getis開頭的方法
  3. 將方法名轉換成相應的屬性名
  4. 將屬性名和方法對象添加到沖突集合中
  5. 解決沖突

在上面的執行流程中,前三步比較簡單,大家自行分析吧。第4步也不復雜,下面我會把源碼貼出來,大家看一下就能懂。在這幾步中,第5步邏輯比較復雜,這一步邏輯我們重點關注一下。下面繼續看源碼吧。

/** 添加屬性名和方法對象到沖突集合中 */
private void addMethodConflict(Map<String, List<Method>> conflictingMethods, String name, Method method) {
    List<Method> list = conflictingMethods.get(name);
    if (list == null) {
        list = new ArrayList<Method>();
        conflictingMethods.put(name, list);
    }
    list.add(method);
}
    
/** 解決沖突 */
private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters) {
    for (Entry<String, List<Method>> entry : conflictingGetters.entrySet()) {
        Method winner = null;
        String propName = entry.getKey();
        for (Method candidate : entry.getValue()) {
            if (winner == null) {
                winner = candidate;
                continue;
            }
            // 獲取返回值類型
            Class<?> winnerType = winner.getReturnType();
            Class<?> candidateType = candidate.getReturnType();

            /* 
             * 兩個方法的返回值類型一致,若兩個方法返回值類型均為 boolean,則選取 isXXX 方法
             * 為 winner。否則無法決定哪個方法更為合適,只能拋出異常
             */
            if (candidateType.equals(winnerType)) {
                if (!boolean.class.equals(candidateType)) {
                    throw new ReflectionException(
                        "Illegal overloaded getter method with ambiguous type for property "
                            + propName + " in class " + winner.getDeclaringClass()
                            + ". This breaks the JavaBeans specification and can cause unpredictable results.");

                /*
                 * 如果方法返回值類型為 boolean,且方法名以 "is" 開頭,
                 * 則認為候選方法 candidate 更為合適
                 */
                } else if (candidate.getName().startsWith("is")) {
                    winner = candidate;
                }

            /*
             * winnerType 是 candidateType 的子類,類型上更為具體,
             * 則認為當前的 winner 仍是合適的,無需做什么事情
             */
            } else if (candidateType.isAssignableFrom(winnerType)) {

            /*
             * candidateType 是 winnerType 的子類,此時認為 candidate 方法更為合適,
             * 故將 winner 更新為 candidate
             */
            } else if (winnerType.isAssignableFrom(candidateType)) {
                winner = candidate;
            } else {
                throw new ReflectionException(
                    "Illegal overloaded getter method with ambiguous type for property "
                        + propName + " in class " + winner.getDeclaringClass()
                        + ". This breaks the JavaBeans specification and can cause unpredictable results.");
            }
        }

        // 將篩選出的方法添加到 getMethods 中,並將方法返回值添加到 getTypes 中
        addGetMethod(propName, winner);
    }
}

private void addGetMethod(String name, Method method) {
    if (isValidPropertyName(name)) {
        getMethods.put(name, new MethodInvoker(method));
        // 解析返回值類型
        Type returnType = TypeParameterResolver.resolveReturnType(method, type);
        // 將返回值類型由 Type 轉為 Class,並將轉換后的結果緩存到 setTypes 中
        getTypes.put(name, typeToClass(returnType));
    }
}

以上就是解除沖突的過程,代碼有點長,不太容易看懂。這里大家只要記住解決沖突的規則即可理解上面代碼的邏輯。相關規則如下:

  1. 沖突方法的返回值類型具有繼承關系,子類返回值對應的方法被認為是更合適的選擇
  2. 沖突方法的返回值類型相同,如果返回值類型為boolean,那么以is開頭的方法則是更合適的方法
  3. 沖突方法的返回值類型相同,但返回值類型非boolean,此時出現歧義,拋出異常
  4. 沖突方法的返回值類型不相關,無法確定哪個是更好的選擇,此時直接拋異常

分析完 getter 方法的解析過程,下面繼續分析 setter 方法的解析過程。

● setter 方法解析過程

與 getter 方法解析過程相比,setter 方法的解析過程與此有一定的區別。主要體現在沖突出現的原因,以及沖突的解決方法上。那下面,我們深入源碼來找出兩者之間的區別。

private void addSetMethods(Class<?> cls) {
    Map<String, List<Method>> conflictingSetters = new HashMap<String, List<Method>>();
    // 獲取當前類,接口,以及父類中的方法。該方法邏輯不是很復雜,這里就不展開了
    Method[] methods = getClassMethods(cls);
    for (Method method : methods) {
        String name = method.getName();
        // 過濾出 setter 方法,且方法僅有一個參數
        if (name.startsWith("set") && name.length() > 3) {
            if (method.getParameterTypes().length == 1) {
                name = PropertyNamer.methodToProperty(name);
                /*
                 * setter 方法發生沖突原因是:可能存在重載情況,比如:
                 *     void setSex(int sex);
                 *     void setSex(SexEnum sex);
                 */
                addMethodConflict(conflictingSetters, name, method);
            }
        }
    }
    // 解決 setter 沖突
    resolveSetterConflicts(conflictingSetters);
}

從上面的代碼和注釋中,我們可知道 setter 方法之間出現沖突的原因。即方法存在重載,方法重載導致methodToProperty方法解析出的屬性名完全一致。而 getter 方法之間出現沖突的原因是getXXXisXXX對應的屬性名一致。既然沖突發生了,要進行調停,那接下來繼續來看看調停沖突的邏輯。

private void resolveSetterConflicts(Map<String, List<Method>> conflictingSetters) {
    for (String propName : conflictingSetters.keySet()) {
        List<Method> setters = conflictingSetters.get(propName);
        /*
         * 獲取 getter 方法的返回值類型,由於 getter 方法不存在重載的情況,
         * 所以可以用它的返回值類型反推哪個 setter 的更為合適
         */
        Class<?> getterType = getTypes.get(propName);
        Method match = null;
        ReflectionException exception = null;
        for (Method setter : setters) {
            // 獲取參數類型
            Class<?> paramType = setter.getParameterTypes()[0];
            if (paramType.equals(getterType)) {
                // 參數類型和返回類型一致,則認為是最好的選擇,並結束循環
                match = setter;
                break;
            }
            if (exception == null) {
                try {
                    // 選擇一個更為合適的方法
                    match = pickBetterSetter(match, setter, propName);
                } catch (ReflectionException e) {
                    match = null;
                    exception = e;
                }
            }
        }
        // 若 match 為空,表示沒找到更為合適的方法,此時拋出異常
        if (match == null) {
            throw exception;
        } else {
            // 將篩選出的方法放入 setMethods 中,並將方法參數值添加到 setTypes 中
            addSetMethod(propName, match);
        }
    }
}

/** 從兩個 setter 方法中選擇一個更為合適方法 */
private Method pickBetterSetter(Method setter1, Method setter2, String property) {
    if (setter1 == null) {
        return setter2;
    }
    Class<?> paramType1 = setter1.getParameterTypes()[0];
    Class<?> paramType2 = setter2.getParameterTypes()[0];

    // 如果參數2可賦值給參數1,即參數2是參數1的子類,則認為參數2對應的 setter 方法更為合適
    if (paramType1.isAssignableFrom(paramType2)) {
        return setter2;
        
    // 這里和上面情況相反
    } else if (paramType2.isAssignableFrom(paramType1)) {
        return setter1;
    }
    
    // 兩種參數類型不相關,這里拋出異常
    throw new ReflectionException("Ambiguous setters defined for property '" + property + "' in class '"
        + setter2.getDeclaringClass() + "' with types '" + paramType1.getName() + "' and '"
        + paramType2.getName() + "'.");
}

private void addSetMethod(String name, Method method) {
    if (isValidPropertyName(name)) {
        setMethods.put(name, new MethodInvoker(method));
        // 解析參數類型列表
        Type[] paramTypes = TypeParameterResolver.resolveParamTypes(method, type);
        // 將參數類型由 Type 轉為 Class,並將轉換后的結果緩存到 setTypes
        setTypes.put(name, typeToClass(paramTypes[0]));
    }
}

關於 setter 方法沖突的解析規則,這里也總結一下吧。如下:

  1. 沖突方法的參數類型與 getter 的返回類型一致,則認為是最好的選擇
  2. 沖突方法的參數類型具有繼承關系,子類參數對應的方法被認為是更合適的選擇
  3. 沖突方法的參數類型不相關,無法確定哪個是更好的選擇,此時直接拋異常

到此關於 setter 方法的解析過程就說完了。我在前面說過 MetaClass 的hasSetter最終調用了 Refactor 的hasSetter方法,那么現在是時候分析 Refactor 的hasSetter方法了。代碼如下如下:

public boolean hasSetter(String propertyName) {
    return setMethods.keySet().contains(propertyName);
}

代碼如上,就兩行,很簡單,就不多說了。

2.3.2.3 PropertyTokenizer 源碼分析

對於較為復雜的屬性,需要進行進一步解析才能使用。那什么樣的屬性是復雜屬性呢?來看個測試代碼就知道了。

public class MetaClassTest {

    private class Author {
        private Integer id;
        private String name;
        private Integer age;
        /** 一個作者對應多篇文章 */
        private Article[] articles;

        // 省略 getter/setter
    }

    private class Article {
        private Integer id;
        private String title;
        private String content;
        /** 一篇文章對應一個作者 */
        private Author author;

        // 省略 getter/setter
    }

    @Test
    public void testHasSetter() {
        // 為 Author 創建元信息對象
        MetaClass authorMeta = MetaClass.forClass(Author.class, new DefaultReflectorFactory());
        System.out.println("------------☆ Author ☆------------");
        System.out.println("id -> " + authorMeta.hasSetter("id"));
        System.out.println("name -> " + authorMeta.hasSetter("name"));
        System.out.println("age -> " + authorMeta.hasSetter("age"));
        // 檢測 Author 中是否包含 Article[] 的 setter
        System.out.println("articles -> " + authorMeta.hasSetter("articles"));
        System.out.println("articles[] -> " + authorMeta.hasSetter("articles[]"));
        System.out.println("title -> " + authorMeta.hasSetter("title"));

        // 為 Article 創建元信息對象
        MetaClass articleMeta = MetaClass.forClass(Article.class, new DefaultReflectorFactory());
        System.out.println("\n------------☆ Article ☆------------");
        System.out.println("id -> " + articleMeta.hasSetter("id"));
        System.out.println("title -> " + articleMeta.hasSetter("title"));
        System.out.println("content -> " + articleMeta.hasSetter("content"));
        // 下面兩個均為復雜屬性,分別檢測 Article 類中的 Author 類是否包含 id 和 name 的 setter 方法
        System.out.println("author.id -> " + articleMeta.hasSetter("author.id"));
        System.out.println("author.name -> " + articleMeta.hasSetter("author.name"));
    }
}

如上,Article類中包含了一個Author引用。然后我們調用 articleMeta 的 hasSetter 檢測author.idauthor.name屬性是否存在,我們的期望結果為 true。測試結果如下:

如上,標記⑤處的輸出均為 true,我們的預期達到了。標記②處檢測 Article 數組的是否存在 setter 方法,結果也均為 true。這說明 PropertyTokenizer 對數組和復合屬性均進行了處理。那它是如何處理的呢?答案如下:

public class PropertyTokenizer implements Iterator<PropertyTokenizer> {

    private String name;
    private final String indexedName;
    private String index;
    private final String children;

    public PropertyTokenizer(String fullname) {
        // 檢測傳入的參數中是否包含字符 '.'
        int delim = fullname.indexOf('.');
        if (delim > -1) {
            /*
             * 以點位為界,進行分割。比如:
             *    fullname = www.coolblog.xyz
             *
             * 以第一個點為分界符:
             *    name = www
             *    children = coolblog.xyz
             */ 
            name = fullname.substring(0, delim);
            children = fullname.substring(delim + 1);
        } else {
            // fullname 中不存在字符 '.'
            name = fullname;
            children = null;
        }
        indexedName = name;
        // 檢測傳入的參數中是否包含字符 '['
        delim = name.indexOf('[');
        if (delim > -1) {
            /*
             * 獲取中括號里的內容,比如:
             *   1. 對於數組或List集合:[] 中的內容為數組下標,
             *      比如 fullname = articles[1],index = 1
             *   2. 對於Map:[] 中的內容為鍵,
             *      比如 fullname = xxxMap[keyName],index = keyName
             *
             * 關於 index 屬性的用法,可以參考 BaseWrapper 的 getCollectionValue 方法
             */
            index = name.substring(delim + 1, name.length() - 1);

            // 獲取分解符前面的內容,比如 fullname = articles[1],name = articles
            name = name.substring(0, delim);
        }
    }

    // 省略 getter

    @Override
    public boolean hasNext() {
        return children != null;
    }

    @Override
    public PropertyTokenizer next() {
        // 對 children 進行再次切分,用於解析多重復合屬性
        return new PropertyTokenizer(children);
    }

    // 省略部分方法
}

以上是 PropertyTokenizer 的源碼分析,注釋的比較多,應該分析清楚了。大家如果看懂了上面的分析,那么可以自行舉例進行測試,以加深理解。

2.3.3 小結

本節的篇幅比較大,大家看起來應該蠻辛苦的。本節為了分析 MetaClass 的 hasSetter 方法,把這個方法涉及到的源碼均分析了一遍。其實,如果想簡單點分析,我可以直接把 MetaClass 當成一個黑盒,然后用一句話告訴大家 hasSetter 方法有什么用即可。但是這樣做我覺的文章太虛,沒什么深度。關於 MetaClass 及相關源碼大家第一次看可能會有點吃力,看不懂可以先放一放。后面多看幾遍,動手寫點測試代碼調試一下,可以幫助理解。

好了,關於 setting 節點的解析過程就先分析到這里,我們繼續往下分析。

2.4 設置 settings 配置到 Configuration 中

上一節講了 settings 配置的解析過程,這些配置解析出來要有一個存放的地方,以使其他代碼可以找到這些配置。這個存放地方就是 Configuration 對象,本節就來看一下這將 settings 配置設置到 Configuration 對象中的過程。如下:

private void settingsElement(Properties props) throws Exception {
    // 設置 autoMappingBehavior 屬性,默認值為 PARTIAL
    configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
    configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
    // 設置 cacheEnabled 屬性,默認值為 true
    configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));

    // 省略部分代碼

    // 解析默認的枚舉處理器
    Class<? extends TypeHandler> typeHandler = (Class<? extends TypeHandler>)resolveClass(props.getProperty("defaultEnumTypeHandler"));
    // 設置默認枚舉處理器
    configuration.setDefaultEnumTypeHandler(typeHandler);
    configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
    configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
    
    // 省略部分代碼
}

上面代碼處理調用 Configuration 的 setter 方法,就沒太多邏輯了。這里來看一下上面出現的一個調用resolveClass,它的源碼如下:

// -☆- BaseBuilder
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);
    }
}

protected final TypeAliasRegistry typeAliasRegistry;

protected Class<?> resolveAlias(String alias) {
    // 通過別名注冊器解析別名對於的類型 Class
    return typeAliasRegistry.resolveAlias(alias);
}

這里出現了一個新的類TypeAliasRegistry,大家對於它可能會覺得陌生,但是對於typeAlias應該不會陌生。TypeAliasRegistry 的用途就是將別名和類型進行映射,這樣就可以用別名表示某個類了,方便使用。既然聊到了別名,那下面我們不妨看看別名的配置的解析過程。

2.5 解析 typeAliases 配置

在 MyBatis 中,可以為我們自己寫的有些類定義一個別名。這樣在使用的時候,我們只需要輸入別名即可,無需再把全限定的類名寫出來。在 MyBatis 中,我們有兩種方式進行別名配置。第一種是僅配置包名,讓 MyBatis 去掃描包中的類型,並根據類型得到相應的別名。這種方式可配合 Alias 注解使用,即通過注解為某個類配置別名,而不是讓 MyBatis 按照默認規則生成別名。這種方式的配置如下:

<typeAliases>
    <package name="xyz.coolblog.model1"/>
    <package name="xyz.coolblog.model2"/>
</typeAliases>

第二種方式是通過手動的方式,明確為某個類型配置別名。這種方式的配置如下:

<typeAliases>
    <typeAlias alias="article" type="xyz.coolblog.model.Article" />
    <typeAlias type="xyz.coolblog.model.Author" />
</typeAliases>

對比這兩種方式,第一種自動掃描的方式配置起來比較簡單,缺點也不明顯。唯一能想到缺點可能就是 MyBatis 會將某個包下所有符合要求的類的別名都解析出來,並形成映射關系。如果你不想讓某些類被掃描,
這個好像做不到,沒發現 MyBatis 提供了相關的排除機制。不過我覺得這並不是什么大問題,最多是多解析並緩存了一些別名到類型的映射,在時間和空間上產生了一些額外的消耗而已。當然,如果無法忍受這些消耗,可以使用第二種配置方式,通過手工的方式精確配置某些類型的別名。不過這種方式比較繁瑣,特別是配置項比較多時。至於兩種方式怎么選擇,這個看具體的情況了。配置項非常少時,兩種皆可。比較多的話,還是讓 MyBatis 自行掃描吧。

以上介紹了兩種不同的別名配置方式,下面我們來看一下兩種不同的別名配置是怎樣解析的。代碼如下:

// -☆- XMLConfigBuilder
private void typeAliasesElement(XNode parent) {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // ⭐️ 從指定的包中解析別名和類型的映射
            if ("package".equals(child.getName())) {
                String typeAliasPackage = child.getStringAttribute("name");
                configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
                
            // ⭐️ 從 typeAlias 節點中解析別名和類型的映射
            } else {
                // 獲取 alias 和 type 屬性值,alias 不是必填項,可為空
                String alias = child.getStringAttribute("alias");
                String type = child.getStringAttribute("type");
                try {
                    // 加載 type 對應的類型
                    Class<?> clazz = Resources.classForName(type);

                    // 注冊別名到類型的映射
                    if (alias == null) {
                        typeAliasRegistry.registerAlias(clazz);
                    } else {
                        typeAliasRegistry.registerAlias(alias, clazz);
                    }
                } catch (ClassNotFoundException e) {
                    throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
                }
            }
        }
    }
}

如上,上面的代碼通過一個if-else條件分支來處理兩種不同的配置,這里我用⭐️標注了出來。下面我們來分別看一下這兩種配置方式的解析過程,首先來看一下手動配置方式的解析過程。

2.5.1 從 typeAlias 節點中解析並注冊別名

在別名的配置中,type屬性是必須要配置的,而alias屬性則不是必須的。這個在配置文件的 DTD 中有規定。如果使用者未配置 alias 屬性,則需要 MyBatis 自行為目標類型生成別名。對於別名為空的情況,注冊別名的任務交由void registerAlias(Class<?>)方法處理。若不為空,則由void registerAlias(String, Class<?>)進行別名注冊。這兩個方法的分析如下:

private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();

public void registerAlias(Class<?> type) {
    // 獲取全路徑類名的簡稱
    String alias = type.getSimpleName();
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    if (aliasAnnotation != null) {
        // 從注解中取出別名
        alias = aliasAnnotation.value();
    }
    // 調用重載方法注冊別名和類型映射
    registerAlias(alias, type);
}

public void registerAlias(String alias, Class<?> value) {
    if (alias == null) {
        throw new TypeException("The parameter alias cannot be null");
    }
    // 將別名轉成小寫
    String key = alias.toLowerCase(Locale.ENGLISH);
    /*
     * 如果 TYPE_ALIASES 中存在了某個類型映射,這里判斷當前類型與映射中的類型是否一致,
     * 不一致則拋出異常,不允許一個別名對應兩種類型
     */
    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);
}

如上,若用戶為明確配置 alias 屬性,MyBatis 會使用類名的小寫形式作為別名。比如,全限定類名xyz.coolblog.model.Author的別名為author。若類中有@Alias注解,則從注解中取值作為別名。

上面的代碼不是很復雜,注釋的也比較清楚了,就不多說了。繼續往下看。

2.5.2 從指定的包中解析並注冊別名

從指定的包中解析並注冊別名過程主要由別名的解析和注冊兩步組成。下面來看一下相關代碼:

public void registerAliases(String packageName) {
    // 調用重載方法注冊別名
    registerAliases(packageName, Object.class);
}

public void registerAliases(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    /*
     * 查找某個包下的父類為 superType 的類。從調用棧來看,這里的 
     * superType = Object.class,所以 ResolverUtil 將查找所有的類。
     * 查找完成后,查找結果將會被緩存到內部集合中。
     */ 
    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);
        }
    }
}

上面的代碼不多,相關流程也不復雜,可簡單總結為下面兩個步驟:

  1. 查找指定包下的所有類
  2. 遍歷查找到的類型集合,為每個類型注冊別名

在這兩步流程中,第2步流程對應的代碼上一節已經分析過了,這里不再贅述。第1步的功能理解起來不難,但是背后對應的代碼有點多。限於篇幅原因,這里我不打算詳細分析這一部分的代碼,只做簡單的流程總結。如下:

  1. 通過 VFS(虛擬文件系統)獲取指定包下的所有文件的路徑名,
    比如xyz/coolblog/model/Article.class
  2. 篩選以.class結尾的文件名
  3. 將路徑名轉成全限定的類名,通過類加載器加載類名
  4. 對類型進行匹配,若符合匹配規則,則將其放入內部集合中

以上就是類型資源查找的過程,並不是很復雜,大家有興趣自己看看吧。

2.5.3 注冊 MyBatis 內部類及常見類型的別名

最后,我們來看一下一些 MyBatis 內部類及一些常見類型的別名注冊過程。如下:

// -☆- Configuration
public Configuration() {
    // 注冊事務工廠的別名
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    // 省略部分代碼,下同

    // 注冊數據源的別名
    typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);

    // 注冊緩存策略的別名
    typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
    typeAliasRegistry.registerAlias("LRU", LruCache.class);

    // 注冊日志類的別名
    typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
    typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);

    // 注冊動態代理工廠的別名
    typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
    typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
}

// -☆- TypeAliasRegistry
public TypeAliasRegistry() {
    // 注冊 String 的別名
    registerAlias("string", String.class);

    // 注冊基本類型包裝類的別名
    registerAlias("byte", Byte.class);
    // 省略部分代碼,下同

    // 注冊基本類型包裝類數組的別名
    registerAlias("byte[]", Byte[].class);
    
    // 注冊基本類型的別名
    registerAlias("_byte", byte.class);

    // 注冊基本類型包裝類的別名
    registerAlias("_byte[]", byte[].class);

    // 注冊 Date, BigDecimal, Object 等類型的別名
    registerAlias("date", Date.class);
    registerAlias("decimal", BigDecimal.class);
    registerAlias("object", Object.class);

    // 注冊 Date, BigDecimal, Object 等數組類型的別名
    registerAlias("date[]", Date[].class);
    registerAlias("decimal[]", BigDecimal[].class);
    registerAlias("object[]", Object[].class);

    // 注冊集合類型的別名
    registerAlias("map", Map.class);
    registerAlias("hashmap", HashMap.class);
    registerAlias("list", List.class);
    registerAlias("arraylist", ArrayList.class);
    registerAlias("collection", Collection.class);
    registerAlias("iterator", Iterator.class);

    // 注冊 ResultSet 的別名
    registerAlias("ResultSet", ResultSet.class);
}

我記得以前配置<select/>標簽的resultType屬性,由於不知道有別名這回事,傻傻的使用全限定類名進行配置。當時還覺得這樣配置一定不會出錯吧,很放心。現在想想有點搞笑。

好了,以上就是別名解析的全部流程,大家看懂了嗎?如果覺得沒啥障礙的話,那繼續往下看唄。

2.6 解析 plugins 配置

插件是 MyBatis 提供的一個拓展機制,通過插件機制我們可在 SQL 執行過程中的某些點上做一些自定義操作。實現一個插件需要比簡單,首先需要讓插件類實現Interceptor接口。然后在插件類上添加@Intercepts@Signature注解,用於指定想要攔截的目標方法。MyBatis 允許攔截下面接口中的一些方法:

  • Executor: update 方法,query 方法,flushStatements 方法,commit 方法,rollback 方法, getTransaction 方法,close 方法,isClosed 方法
  • ParameterHandler: getParameterObject 方法,setParameters 方法
  • ResultSetHandler: handleResultSets 方法,handleOutputParameters 方法
  • StatementHandler: prepare 方法,parameterize 方法,batch 方法,update 方法,query 方法

比較常見的插件有分頁插件、分表插件等,有興趣的朋友可以去了解下。本節我們來分析一下插件的配置的解析過程,先來了解插件的配置。如下:

<plugins>
    <plugin interceptor="xyz.coolblog.mybatis.ExamplePlugin">
        <property name="key" value="value"/>
    </plugin>
</plugins>

解析過程分析如下:

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            String interceptor = child.getStringAttribute("interceptor");
            // 獲取配置信息
            Properties properties = child.getChildrenAsProperties();
            // 解析攔截器的類型,並創建攔截器
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
            // 設置屬性
            interceptorInstance.setProperties(properties);
            // 添加攔截器到 Configuration 中
            configuration.addInterceptor(interceptorInstance);
        }
    }
}

如上,插件解析的過程還是比較簡單的。首先是獲取配置,然后再解析攔截器類型,並實例化攔截器。最后向攔截器中設置屬性,並將攔截器添加到 Configuration 中。好了,關於插件配置的分析就先到這,繼續往下分析。

2.7 解析 environments 配置

在 MyBatis 中,事務管理器和數據源是配置在 environments 中的。它們的配置大致如下:

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </dataSource>
    </environment>
</environments>

接下來我們對照上面的配置進行分析,如下:

private String environment;

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            // 獲取 default 屬性
            environment = context.getStringAttribute("default");
        }
        for (XNode child : context.getChildren()) {
            // 獲取 id 屬性
            String id = child.getStringAttribute("id");
            /*
             * 檢測當前 environment 節點的 id 與其父節點 environments 的屬性 default 
             * 內容是否一致,一致則返回 true,否則返回 false
             */
            if (isSpecifiedEnvironment(id)) {
                // 解析 transactionManager 節點,邏輯和插件的解析邏輯很相似,不在贅述
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                // 解析 dataSource 節點,邏輯和插件的解析邏輯很相似,不在贅述
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                // 創建 DataSource 對象
                DataSource dataSource = dsFactory.getDataSource();
                Environment.Builder environmentBuilder = new Environment.Builder(id)
                    .transactionFactory(txFactory)
                    .dataSource(dataSource);
                // 構建 Environment 對象,並設置到 configuration 中
                configuration.setEnvironment(environmentBuilder.build());
            }
        }
    }
}

environments 配置的解析過程沒什么特別之處,按部就班解析就行了,不多說了。

2.8 解析 typeHandlers 配置

在向數據庫存儲或讀取數據時,我們需要將數據庫字段類型和 Java 類型進行一個轉換。比如數據庫中有CHARVARCHAR等類型,但 Java 中沒有這些類型,不過 Java 有String類型。所以我們在從數據庫中讀取 CHAR 和 VARCHAR 類型的數據時,就可以把它們轉成 String 。在 MyBatis 中,數據庫類型和 Java 類型之間的轉換任務是委托給類型處理器TypeHandler去處理的。MyBatis 提供了一些常見類型的類型處理器,除此之外,我們還可以自定義類型處理器以非常見類型轉換的需求。這里我就不演示自定義類型處理器的編寫方法了,沒用過或者不熟悉的同學可以 MyBatis 官方文檔,或者我在上一篇文章中寫的示例。

下面,我們來看一下類型處理器的配置方法:

<!-- 自動掃描 -->
<typeHandlers>
    <package name="xyz.coolblog.handlers"/>
</typeHandlers>
<!-- 手動配置 -->
<typeHandlers>
    <typeHandler jdbcType="TINYINT"
            javaType="xyz.coolblog.constant.ArticleTypeEnum"
            handler="xyz.coolblog.mybatis.ArticleTypeHandler"/>
</typeHandlers>

使用自動掃描的方式注冊類型處理器時,應使用@MappedTypes@MappedJdbcTypes注解配置javaTypejdbcType。關於注解,這里就不演示了,比較簡單,大家自行嘗試。下面開始分析代碼。

private void typeHandlerElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // 從指定的包中注冊 TypeHandler
            if ("package".equals(child.getName())) {
                String typeHandlerPackage = child.getStringAttribute("name");
                // 注冊方法 ①
                typeHandlerRegistry.register(typeHandlerPackage);

            // 從 typeHandler 節點中解析別名到類型的映射
            } else {
                // 獲取 javaType,jdbcType 和 handler 等屬性值
                String javaTypeName = child.getStringAttribute("javaType");
                String jdbcTypeName = child.getStringAttribute("jdbcType");
                String handlerTypeName = child.getStringAttribute("handler");

                // 解析上面獲取到的屬性值
                Class<?> javaTypeClass = resolveClass(javaTypeName);
                JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
                Class<?> typeHandlerClass = resolveClass(handlerTypeName);

                // 根據 javaTypeClass 和 jdbcType 值的情況進行不同的注冊策略
                if (javaTypeClass != null) {
                    if (jdbcType == null) {
                        // 注冊方法 ②
                        typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
                    } else {
                        // 注冊方法 ③
                        typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
                    }
                } else {
                    // 注冊方法 ④
                    typeHandlerRegistry.register(typeHandlerClass);
                }
            }
        }
    }
}

上面代碼中用於解析 XML 部分的代碼比較簡單,沒什么需要特別說明的。除此之外,上面的代碼中調用了4個不同的類型處理器注冊方法。這些注冊方法的邏輯不難理解,但是重載方法很多,上面調用的注冊方法只是重載方法的一部分。由於重載太多且重載方法之間互相調用,導致這一塊的代碼有點凌亂。我一開始在整理這部分代碼時,也很抓狂。后來沒轍了,把重載方法的調用圖畫了出來,才理清了代碼。一圖勝千言,看圖吧。

在上面的調用圖中,每個藍色背景框下都有一個標簽。每個標簽上面都已一個編號,這些編號與上面代碼中的標簽是一致的。這里我把藍色背景框內的方法稱為開始方法紅色背景框內的方法稱為終點方法白色背景框內的方法稱為中間方法。下面我會分析從每個開始方法向下分析,為了避免冗余分析,我會按照③ → ② → ④ → ①的順序進行分析。大家在閱讀代碼分析時,可以參照上面的圖片,輔助理解。好了,下面開始進行分析。

2.8.1 register(Class, JdbcType, Class) 方法分析

當代碼執行到此方法時,表示javaTypeClass != null && jdbcType != null條件成立,即使用者明確配置了javaTypejdbcType屬性的值。那下面我們來看一下該方法的分析。

public void register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass) {
    // 調用終點方法
    register(javaTypeClass, jdbcType, getInstance(javaTypeClass, typeHandlerClass));
}

/** 類型處理器注冊過程的終點 */
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
        // JdbcType 到 TypeHandler 的映射
        Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
        if (map == null || map == NULL_TYPE_HANDLER_MAP) {
            map = new HashMap<JdbcType, TypeHandler<?>>();
            // 存儲 javaType 到 Map<JdbcType, TypeHandler> 的映射
            TYPE_HANDLER_MAP.put(javaType, map);
        }
        map.put(jdbcType, handler);
    }

    // 存儲所有的 TypeHandler
    ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}

上面的代碼只有兩層調用,比較簡單。同時,所謂的注冊過程也就是把類型和處理器進行映射而已,沒什么特別之處。關於這個方法就先分析到這里,繼續往下分析。下面的方法對應注冊方法②。

2.8.2 register(Class, Class) 方法分析

當代碼執行到此方法時,表示javaTypeClass != null && jdbcType == null條件成立,即使用者僅設置了javaType屬性的值。下面我們來看一下該方法的分析。

public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
    // 調用中間方法 register(Type, TypeHandler)
    register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
}

private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
    // 獲取 @MappedJdbcTypes 注解
    MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
    if (mappedJdbcTypes != null) {
        // 遍歷 @MappedJdbcTypes 注解中配置的值
        for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
            // 調用終點方法,參考上一小節的分析
            register(javaType, handledJdbcType, typeHandler);
        }
        if (mappedJdbcTypes.includeNullJdbcType()) {
            // 調用終點方法,jdbcType = null
            register(javaType, null, typeHandler);
        }
    } else {
        // 調用終點方法,jdbcType = null
        register(javaType, null, typeHandler);
    }
}

上面的代碼包含三層調用,其中終點方法的邏輯上一節已經分析過,這里不再贅述。上面的邏輯也比較簡單,主要做的事情是嘗試從注解中獲取JdbcType的值。這個方法就分析這么多,下面分析注冊方法④。

2.8.3 register(Class) 方法分析

當代碼執行到此方法時,表示javaTypeClass == null && jdbcType != null條件成立,即使用者未配置javaTypejdbcType屬性的值。該方法的分析如下。

public void register(Class<?> typeHandlerClass) {
    boolean mappedTypeFound = false;
    // 獲取 @MappedTypes 注解
    MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
        // 遍歷 @MappedTypes 注解中配置的值
        for (Class<?> javaTypeClass : mappedTypes.value()) {
            // 調用注冊方法 ②
            register(javaTypeClass, typeHandlerClass);
            mappedTypeFound = true;
        }
    }
    if (!mappedTypeFound) {
        // 調用中間方法 register(TypeHandler)
        register(getInstance(null, typeHandlerClass));
    }
}

public <T> void register(TypeHandler<T> typeHandler) {
    boolean mappedTypeFound = false;
    // 獲取 @MappedTypes 注解
    MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
        for (Class<?> handledType : mappedTypes.value()) {
            // 調用中間方法 register(Type, TypeHandler)
            register(handledType, typeHandler);
            mappedTypeFound = true;
        }
    }
    // 自動發現映射類型
    if (!mappedTypeFound && typeHandler instanceof TypeReference) {
        try {
            TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
            // 獲取參數模板中的參數類型,並調用中間方法 register(Type, TypeHandler)
            register(typeReference.getRawType(), typeHandler);
            mappedTypeFound = true;
        } catch (Throwable t) {
        }
    }
    if (!mappedTypeFound) {
        // 調用中間方法 register(Class, TypeHandler)
        register((Class<T>) null, typeHandler);
    }
}

public <T> void register(Class<T> javaType, TypeHandler<? extends T> typeHandler) {
    // 調用中間方法 register(Type, TypeHandler)
    register((Type) javaType, typeHandler);
}

上面的代碼比較多,不過不用太擔心。不管是通過注解的方式,還是通過反射的方式,它們最終目的是為了解析出javaType的值。解析完成后,這些方法會調用中間方法register(Type, TypeHandler),這個方法負責解析jdbcType,該方法上一節已經分析過。一個復雜解析 javaType,另一個負責解析 jdbcType,邏輯比較清晰了。那我們趁熱打鐵,繼續分析下一個注冊方法,編號為①。

2.8.4 register(String) 方法分析

本節代碼的主要是用於自動掃描類型處理器,並調用其他方法注冊掃描結果。該方法的分析如下:

public void register(String packageName) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    // 從指定包中查找 TypeHandler
    resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
    Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
    for (Class<?> type : handlerSet) {
        // 忽略內部類,接口,抽象類等
        if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
            // 調用注冊方法 ④
            register(type);
        }
    }
}

上面代碼的邏輯比較簡單,其中注冊方法④已經在上一節分析過了,這里就不多說了。

2.8.5 小結

類型處理器的解析過程不復雜,但是注冊過程由於重載方法間相互調用,導致調用路線比較復雜。這個時候需要想辦法理清方法的調用路線,理清后,整個邏輯就清晰明了了。好了,關於類型處理器的解析過程就先分析到這。

2.9 解析 mappers 配置

前面分析的都是 MyBatis 的一些配置,本節的內容原本是打算分析 mappers 節點的解析過程。但由於本文的篇幅已經很大了,加之 mappers 節點的過程也比較復雜。所以,關於本節的內容,我會獨立成文,后面再進行更新。這里先告知大家一下。

3.總結

本文對 MyBatis 配置文件中部分配置的解析過程進行了詳細的分析。本文所關注的點不局限於配置文件的解析過程,如果僅分析配置文件,那就簡單多了。對於我來說,我更希望在分析配置文件的過程中,盡量把一些背景知識弄明白,這樣才能對 MyBatis 有更多的了解。從本文的篇幅以及內容上來說,我覺得本篇文章達到了自己的預期。通過本文的分析,也使我加深了對 MyBatis 的理解。總的來說,收獲還是比較多的。不過個人水平有限,若文章有錯誤不妥之處,也請大家多多指教。

本篇文章篇幅比較大,寫起來還是很耗費精力的。如果大家覺得這篇文章還不錯的話,不妨給個贊吧,算是對我的鼓勵了。好了,本篇文章就到這里了,感謝大家的閱讀。

參考

附錄:MyBatis 源碼分析系列文章列表

更新時間 標題
2018-09-11 MyBatis 源碼分析系列文章合集
2018-07-16 MyBatis 源碼分析系列文章導讀
2018-07-20 MyBatis 源碼分析 - 配置文件解析過程
2018-07-30 MyBatis 源碼分析 - 映射文件解析過程
2018-08-17 MyBatis 源碼分析 - SQL 的執行過程
2018-08-19 MyBatis 源碼分析 - 內置數據源
2018-08-25 MyBatis 源碼分析 - 緩存原理
2018-08-26 MyBatis 源碼分析 - 插件機制

本文在知識共享許可協議 4.0 下發布,轉載需在明顯位置處注明出處
作者:田小波
本文同步發布在我的個人博客:http://www.tianxiaobo.com

cc
本作品采用知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協議進行許可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM