mybatis源碼分析之配置文件解析

一、簡介

我們上一個篇文章已經配置好了,mybatis配置文件和測試類。我們先分析一下mybatis的是如何加載mybatis-config.xml文件的。

String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);

這裏是通過mybatis工具類Resources加載配置文件,得到一個InputStream輸入流。

二、資源文件加載

接着我們先看一下Resources的資源加載工具類。

public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
    // 通過classLoaderWrapper包裝類加載
    InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
    if (in == null) {
        throw new IOException("Could not find resource " + resource);
    }
    return in;
}

接着通過類加載器,從資源路徑(classpath)中加載資源,

public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {
    return getResourceAsStream(resource, getClassLoaders(classLoader));
}

這時候初始化類加載有五種:

ClassLoader[] getClassLoaders(ClassLoader classLoader) {
    return new ClassLoader[]{
        classLoader,  // 出入的類加載
        defaultClassLoader, // 默認類加載
        Thread.currentThread().getContextClassLoader(),  // 線程類上下文加載器
        getClass().getClassLoader(),  // 當前類加載器
        systemClassLoader   // 系統類加載器
    };
}

然後會從這些類加載器中加載我們需要資源:

InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
    // 循環遍歷獲取資源
    for (ClassLoader cl : classLoader) {
        if (null != cl) {

            // try to find the resource as passed
            InputStream returnValue = cl.getResourceAsStream(resource);

            // 有些加載器需要添加`/`,所以加上這個`/`前綴,重試
            if (null == returnValue) {
                returnValue = cl.getResourceAsStream("/" + resource);
            }

            if (null != returnValue) {
                return returnValue;
            }
        }
    }
    return null;
}

資源路徑: 是我們編譯之後target/classes/目錄下的資源。

這個邏輯其實還是很簡單的,重點是我們需要複習一下類加載器相關的技能。下面是我們常見的類加載器基本上是這些:

  • ClassLoader.getSystemClassLoader():系統類加載器
  • xxx.class.getClassLoader()this.getClass().getClassLoader():當前類加載器
  • Thread.currentThread().getContextClassLoader():線程上下文加載器

主要區別:

  1. getClassLoader:是獲取加載當前類的類加載器。可能是***啓動類加載器***、拓展類加載器系統類加載器,取決於當前類是有哪個加載器加載的。
  2. getClontextCLassLoader:是獲取當前線程上下文的類加載器,用戶可以自己設置,Java SE環境下一般是AppClassLoaderJava EE環境下是WebappClassLoader
  3. getSystemClassLoader:是獲取系統類加載器AppClassLoader

三、構建SessionFactory

接着我們看看一下sessionFactory是如何構建的

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

我們看一下SqlSessionFactoryBuilder這個類,這個就一個核心方法:build。這個方法大體分成兩類,都是參數重載的方法。

public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
}

這個方法可以將數據源信息和屬性一起構建,我們是從配置文件中構建的,所以這兩個參數時null

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      // 構建配置文件解析器  
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // 這裏面調用的是解析器的解析各個節點  
      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.
      }
    }
  }

這裏面核心方法是parser.parse()。這裏面用來解析mybatis-config.xml的各個節點。

public Configuration parse() {
    // step 防止別多次讀取
    if (parsed) {
        throw new BuilderException("每個XMLConfigBuilder只能使用一次.");
    }
    parsed = true;
    // step 解析各個節點
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

/**
 * 解析配置文件的各個節點
 * @param root  根節點
 */
private void parseConfiguration(XNode root) {
    try {
        // 解析 properties 節點
        propertiesElement(root.evalNode("properties"));
        // 解析 解析 settings 配置,並將其轉換爲 Properties 對象
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        // 加載 vfs  VFS主要用來加載容器內的各種資源,比如jar或者class文件
        loadCustomVfs(settings);
        //  解析日誌配置
        loadCustomLogImpl(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("解析SQL Mapper配置時出錯. 原因: " + e, e);
    }
}

3.1. properties節點的解析

我們在mybatis-config.xml中配置properties節點, 我們裏面配置的是resource

<configuration>
  <properties resource="jdbc.properties" />
  ......
</configuration>

jdbc.properties裏面配置的是我們的數據庫連接相關信息

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.252.139:3306/mybatis-test
jdbc.username=root
jdbc.password=123456

接着看着propertiesElement這個是如何解析properties節點的。

/**
 * 解析properties節點
 * @param context  properties節點
 * @throws Exception
 */
private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
        // step 讀取子節點數據
        Properties defaults = context.getChildrenAsProperties();
        // step 獲取節點裏面的resource和url
        String resource = context.getStringAttribute("resource");
        String url = context.getStringAttribute("url");
        // step resource和url只能有一個存在
        if (resource != null && url != null) {
            throw new BuilderException("properties元素不能同時指定URL和基於資源的屬性文件引用。請指定其中一個。");
        }
        // step resource存在,使用Resources工具類加載properties文件
        if (resource != null) {
            defaults.putAll(Resources.getResourceAsProperties(resource));
        } else if (url != null) {
            defaults.putAll(Resources.getUrlAsProperties(url));
        }
        // step 如果節點裏面有屬性,在設置到defaults裏面
        Properties vars = configuration.getVariables();
        if (vars != null) {
            defaults.putAll(vars);
        }
        // step 將屬性設置到XPathParser對象裏面
        parser.setVariables(defaults);
        // step 將屬性設置到configuration對象裏面
        configuration.setVariables(defaults);
    }
}

這裏我們可以看到,在加載properties節點的時候,會先加載子節點的數據,然後纔會加載resource中的數據,所有就會發生resource會覆蓋properties節點中子節點數據。

3.2. setting節點解析

setting配置是mybatis中非常重要的配置,一般我們使用默認配置就可以了。我們先看一下mybatis有哪些全局運行參數:

  • cacheEnabled:默認是true,改配置影響的所有映射器中配置的緩存的全局開關。
  • lazyLoadingEnabled:默認是true, 延遲加載的全局開關,當開啓時,所有關聯對象都會延遲加載。特定關聯關係中可以通過設置fetchType屬性來覆蓋該項的開關狀態。
  • multipleResultSetsEnabled:默認值是true,是否允許單一語句返回多結果集(需要兼容驅動)
  • useColumnLabel: 默認值是true,使用列標籤代替列名。不同驅動有不同的表現。
  • useGeneratedKeys:默認值false,允許自動生成主鍵,需要驅動兼容。
  • autoMappingBehavior:默認值PARTIAL,指定mybatis應如何自動映射到字段或者屬性。NONE:表示取消自動映射,PARTIAY:只會自動映射沒有定義嵌套結果集映射的結果集,FULL會自動映射任意複雜的結果集
  • autoMappingUnkownColumnBehavior:默認值WARNING
  • defaultExecutorType:默認值SIMPLE,配置默認的執行器。SIMPLE:就是普通執行器,REUSE:執行器會重用預處理器(prepared statements)BATCH:執行器將重用語句並執行批量更新。
  • defaultStatementTimeout:默認值25,設置超時時間,它決定驅動等待數據庫影響的秒數。
  • defaultFetchSize:默認值100,爲驅動的結果集獲取數量(fetchSize)設置一個建議值。此參數只可以在查詢設置中被覆蓋。
  • safeRowBoundsEnabled:默認值false,允許在嵌套語句中使用分頁RowBounds
  • mapUnderscoreToCamelCase:默認值false,是否開啓自動駝峯命名規則映射。
  • localCacheScopeMyBatis 利用本地緩存機制(Local Cache)防止循環引用和加速重複的嵌套查詢。 默認值爲 SESSION,會緩存一個會話中執行的所有查詢。 若設置值爲 STATEMENT,本地緩存將僅用於執行語句,對相同 SqlSession 的不同查詢將不會進行緩存。
  • jdbcTypeForNull:當沒有提供特定的JDBC類型時,爲空值指定JDBC類型。某些驅動需要指定列的JDBC類型,多數情況直接用一般類型,比如NULLVARCHAR或者OTHER
  • lazyLoadTriggerMethods:默認值equalsclonehashCodetoString,指定哪個對象的方法觸發一次延遲加載。

其他的配置可以參看官網:https://mybatis.org/mybatis-3/zh/configuration.html

一般來說這些配置我們保持默認就可以。先添加一些setting的配置。

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

接着我們看一下如何解析setting節點的。

private Properties settingsAsProperties(XNode context) {
    if (context == null) {
        return new Properties();
    }
    // 獲取setting屬性內容
    Properties props = context.getChildrenAsProperties();
    // 檢查配置類是否知道所有設置
    MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
    for (Object key : props.keySet()) {
        // 查看系統的全局配置項中是否存在我們配置的,不存在則跑出異常。
        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;
}

context.getChildrenAsProperties可以獲取我們配置的那些配置項。

MetaClass.forClass將用於獲取所有的全局配置信息。在裏面或包含我們方纔配置的那兩個屬性。這個MetaClass具體如何運行的,我們後面分析。(這個地方我們可以先去省略MetaClass中操作,這個裏面方法還是挺多的,我們可以知道其的作用是啥,具體怎麼實現後面再看,當成一個黑盒子,不影響我們整體去看源碼的結構。)

最後需要將setting解析出來的數據設置到configuration中:

這裏面我們和3.2上面的那些全局屬性相互對應的看看就可以了。

3.3. typeAliases別名節點解析

現在我們先配置一些別名,我們先來測試debug跟一下代碼我們就知道怎麼解析別名了

mybatis中,我們可以一些類定義一個別名,在使用的時候用直接用別名就可以了,不需要出入類所在類路徑。

/**
 * 解析typeAliases
 * @param parent
 */
private void typeAliasesElement(XNode parent) {
    if (parent != null) {
        // step 1. 遍歷typeAliases節點
        for (XNode child : parent.getChildren()) {
            // step 2. 判斷是否節點名字爲 package
            if ("package".equals(child.getName())) {
                // step 2.1. 獲取節點屬性的name
                String typeAliasPackage = child.getStringAttribute("name");
                configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
            } else { // step 3. 判斷節點名爲typeAlias
                // step 3.1. 獲取節點屬性的alias和type
                String alias = child.getStringAttribute("alias");
                String type = child.getStringAttribute("type");
                try {
                    // step 3.2. 獲取class
                    Class<?> clazz = Resources.classForName(type);
                    // step 3.3. 註冊別名到映射
                    if (alias == null) {
                        typeAliasRegistry.registerAlias(clazz);
                    } else {
                        typeAliasRegistry.registerAlias(alias, clazz);
                    }
                } catch (ClassNotFoundException e) { // step 4. class沒有找到拋出異常
                    throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
                }
            }
        }
    }
}

從這個大體的解析過程,我們可以看到mybatis解析別名的時候是有兩種方法的,第一種是解析<package name="com.mly.learn.entity"/>這一中通過掃描包的方式將別名註冊到映射,第二種是解析<typeAlias type="com.mly.learn.entity.Student" alias="Student" />這兩個是不可以並存的,我們只能選擇其中一種。

接着我們看一下這兩種方法是如何註冊到映射中的。

首先mybatis已經註冊一堆類型映射。

在有的別名也需要註冊到typeAliasesmap中。我們看一下mybatis是如何將別名註冊到map中的呢!

public void registerAliases(String packageName, Class<?> superType) {
    // 包掃描或者我們在package下的類
    ResolverUtil<Class<?>> resolverUtil = new 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);
        }
    }
}

接着在registerAlias判斷類上是否有Alias註解,有的話將value作爲key,或者將類名作爲key,然後判斷typeAliases中是否存在或者value相同,否則拋出異常。

/**
 * 註冊別名
 * @param type
 */
public void registerAlias(Class<?> type) {
    // step 獲取類名
    String alias = type.getSimpleName();
    // step 2. 判斷是否存在Alias 註解, 存在的話,將註解中value值作爲key, 否則將類名作爲key
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    if (aliasAnnotation != null) {
        alias = aliasAnnotation.value();
    }
    registerAlias(alias, type);
}

/**
 * 註冊alias:class到map中
 * @param alias
 * @param value
 */
public void registerAlias(String alias, Class<?> value) {
    // step 1. 別名== null, 拋出異常
    if (alias == null) {
        throw new TypeException("The parameter alias cannot be null");
    }
    // step 2. 轉成小寫字母
    String key = alias.toLowerCase(Locale.ENGLISH);
    // step 3. 判斷typeAliases是否包含key或者value相同,拋出異常
    if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
        throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
    }
    typeAliases.put(key, value);
}

另一種方法,實際調用的還是registerAlias這個方法一樣的。就不做累述。

3.4. typeHandlers類型轉換節點解析

我們數據庫的類型和Java類型是不一樣的,我們需要進行轉換纔可以,例如我們在使用JavaString類型,但是在MySql數據類型是char或者varchar類型。這種數據類型之後的對應就需要typeHanlder進行準換。

看一下默認註冊的一些類型轉換器:

接着看一下mybatis的是如何處理typeHandlerxml節點的。

private void typeHandlerElement(XNode parent) {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
                String typeHandlerPackage = child.getStringAttribute("name");
                typeHandlerRegistry.register(typeHandlerPackage);
            } else {
                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);
                if (javaTypeClass != null) {
                    if (jdbcType == null) {
                        typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
                    } else {
                        typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
                    }
                } else {
                    typeHandlerRegistry.register(typeHandlerClass);
                }
            }
        }
    }
}

掃描包下面的class將其註冊到類型轉換映射器中,這裏面的就是各種重載方法之間的調用,有點回看暈,先畫個圖看看啊。

先看一下掃描包這個方法執行的過程, 代碼就不一一展示了。

public void register(String packageName) {
    // 掃描包路徑下的class
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    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);
        }
    }
}

其他的就不一一貼代碼了,可以跟着上面的圖一一看一下就OK,邏輯不復雜。

private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
        Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
        if (map == null || map == NULL_TYPE_HANDLER_MAP) {
            map = new HashMap<>();
        }
        map.put(jdbcType, handler);
        typeHandlerMap.put(javaType, map);
    }
    allTypeHandlersMap.put(handler.getClass(), handler);
}

3.5. environments節點解析

MyBatis 中,事務管理器和數據源是配置在<environments>節點中的。接着我們來看是如何解析的:

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            // step  獲取默認節點
            environment = context.getStringAttribute("default");
        }
        // step 遍歷environment節點
        for (XNode child : context.getChildren()) {
            // step 獲取id的子節點
            String id = child.getStringAttribute("id");
            // step  判斷是否爲指定的節點
            if (isSpecifiedEnvironment(id)) {
                // step 配置事務
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                // step 構建 DataSourceFactory 對象
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                DataSource dataSource = dsFactory.getDataSource();
                // step 將事務處理器、數據源和id組裝到一起,並設置到configuration中
                Environment.Builder environmentBuilder = new Environment.Builder(id)
                    .transactionFactory(txFactory)
                    .dataSource(dataSource);
                configuration.setEnvironment(environmentBuilder.build());
            }
        }
    }
}

當我們配置兩個數據源的時候,mybatis只會讀取default的數據源ID。然後將事務管理器和數據源組裝成一個Environment對象。

這個是比較簡單。

3.6. plugins插件節點解析

這個部分在分析分頁插件的時候在去具體分析吧。先挖個坑。

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