天天看點

MyBatis源碼解析(一) --- 配置檔案解析

MyBatis給我們提供豐富的配置來滿足我們的需求,本文會對MyBatis的配置檔案解析過程進行分析, 其中包含但不限于 properties、 settings、typeAliase、typeHandlers 等。

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 的标簽,這裡選中這個标簽,并傳遞給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 的解析過程就出來了,每個配置的解析邏輯都封裝在了相應的方法中。

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;
}
           

需要注意的是,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
           

3、解析 settings 配置

settings 節點的解析過程

settings 相關配置是 MyBatis 中非常重要的配置,這些配置用于調整 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;
}
           

在上面的代碼中出現了一個陌生的類MetaClass,他是用來解析目标類的一些元資訊,比如類的成員變量,getter/setter 方法等。關于這個類的邏輯,待會我會詳細解析。接下來,簡單總結一下上面代碼的邏輯。如下:

  1. 解析 settings 子節點的内容,并将解析結果轉成 Properties 對象
  2. 為 Configuration 建立元資訊對象
  3. 通過 MetaClass 檢測 Configuration 中是否存在某個屬性的 setter 方法,不存在則抛異常
  4. 若通過 MetaClass 的檢測,則傳回 Properties 對象,方法邏輯結束

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

元資訊對象建立過程

元資訊類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);
    }

    // 省略其他方法
}
           

上面代碼出現了兩個新的類ReflectorFactory和Reflector,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 方法。下面來簡單介紹一下上面代碼中出現的幾個類:

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

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,TypeAliasRegistry 的用途就是将别名和類型進行映射,這樣就可以用别名表示某個類了,友善使用。

5、解析 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 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)) {
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          // 建構 Environment 對象,并設定到 configuration 中
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }
           

6、解析 typeHandlers 配置

在向資料庫存儲或讀取資料時,我們需要将資料庫字段類型和 Java 類型進行一個轉換。比如資料庫中有CHAR和VARCHAR等類型,但 Java 中沒有這些類型,不過 Java 有String類型。是以我們在從資料庫中讀取 CHAR 和 VARCHAR 類型的資料時,就可以把它們轉成 String 。

在 MyBatis 中,資料庫類型和 Java 類型之間的轉換任務是委托給類型處理器TypeHandler去處理的。MyBatis 提供了一些常見類型的類型處理器,除此之外,我們還可以自定義類型處理器以非常見類型轉換的需求。這裡我就不示範自定義類型處理器的編寫方法了,沒用過或者不熟悉的同學可以 MyBatis 官方文檔,或者我在上一篇文章中寫的示例。

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);
                }
        }
      }
    }
  }
           

上面調用的注冊方法隻是重載方法的一部分。由于重載太多且重載方法之間互相調用,導緻這一塊的代碼有點淩亂。我一開始在整理這部分代碼時,也很抓狂。後來沒轍了,把重載方法的調用圖畫了出來,才理清了代碼。一圖勝千言,看圖吧。

MyBatis源碼解析(一) --- 配置檔案解析

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

1、register(Class, JdbcType, Class) 方法分析

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

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、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的值

3、register(Class) 方法分析

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

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,邏輯比較清晰了。那我們趁熱打鐵,繼續分析下一個注冊方法,編号為①。

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);
        }
    }
}

           

小結

類型處理器的解析過程不複雜,但是注冊過程由于重載方法間互相調用,導緻調用路線比較複雜。這個時候需要想辦法理清方法的調用路線,理清後,整個邏輯就清晰明了了。

繼續閱讀