天天看點

Mybatis源碼閱讀(一):Mybatis初始化1.1 解析properties、settings前言入口引申結語

前言

筆者大概是從今年的5月份開始喜歡上源碼閱讀的,起初是閱讀徐郡明前輩的《Mybatis技術内幕》入的坑,不得不說大佬就是大佬,書中講得東西很細很全。半年過去了,筆者對mybatis略知一二,也開始在為公司搭架構,并且基于Mybatis寫了一套架構,但是盡管如此還是感覺自己對于源碼的了解稍微有點淺。好比是初高中學數學吧,光看例題不做題是記不住的,是以産生了為mybatis寫注釋的想法,想要通過寫注釋的過程,加強對mybatis的了解。雖然現在網上已經有了較全的mybatis中文注釋,但是感覺還是經過自己手敲更能加強記憶,是以便挖下了這個大坑。筆者也希望可以在一年内把這個坑填完,後續關于其他技術的文章可能就比較少,大多數應該就都是mybatis源碼閱讀犀利了

在這裡,附上我的碼雲位址(别問我為什麼是碼雲而不是github,下半天代碼下不動急死人)

mybatis中文注釋

同時,我也很歡迎更多的國中級開發者投入到閱讀源碼的行列,并且很樂意大家在我的倉庫上建立自己的分支,希望可以和大家一同進步。

入口

Mybatis

初始化入口檔案是SqlSessionFactoryBuilder。該類通過調用XMLConfigBuilder.parse方法初始化配置檔案。

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      // 讀取配置檔案
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
           

在XMLConfigBuilder.parse方法中,會先校驗配置檔案是否已經解析過了,如果重複解析就抛出異常

public Configuration parse() {
        if (parsed) {
            // 已經解析過就不再解析。這裡隻解析一次
            throw new BuilderException("每個 XMLConfigBuilder 隻能使用一次.");
        }
        parsed = true;
        // 擷取configuration節點進行解析
        // mybatis解析配置檔案使用的是XPathParser,這裡的evalNode方法就是解析xml的代碼
        // 這裡對XPathParser不進行注釋,這不屬于mybatis的範疇(其實是懶。)
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
    }
           

parseConfiguration方法中,傳入configuration節點配置,對mybatis-config.xml檔案中的該節點進行解析。解析結果會set到Configuration類中。今天隻注釋完了properties和settings兩個節點的解析

/**
     * 解析mybatis-config.xml檔案
     *
     * @param root
     */
    private void parseConfiguration(XNode root) {
        try {
            // 解析properties節點。該節點用來引入外部的資源檔案,如db.properties
            propertiesElement(root.evalNode("properties"));
            // 解析settings節點,校驗配置中的配置項是否合法。該節點用來設定一些mybatis的配置項
            Properties settings = settingsAsProperties(root.evalNode("settings"));
            // 加載使用者自己配置的虛拟檔案系統
            loadCustomVfs(settings);
            // 加載日志
            loadCustomLogImpl(settings);
            // TODO 解析typeAliases節點,下次繼續
            typeAliasesElement(root.evalNode("typeAliases"));
            pluginElement(root.evalNode("plugins"));
            objectFactoryElement(root.evalNode("objectFactory"));
            objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            reflectorFactoryElement(root.evalNode("reflectorFactory"));
            settingsElement(settings);
            // read it after objectFactory and objectWrapperFactory issue #631
            environmentsElement(root.evalNode("environments"));
            databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            typeHandlerElement(root.evalNode("typeHandlers"));
            mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }
           

先看propertiesElement方法,該方法用于解析properties節點。

/**
     * 解析mybatis-config.xml的properties節點
     * 将節點中所有的配置set到Configuration中
     *
     * @param context
     * @throws Exception
     */
    private void propertiesElement(XNode context) throws Exception {
        if (context != null) {
            // 解析拿到節點下的所有子節點配置
            Properties defaults = context.getChildrenAsProperties();
            // 擷取properties節點的resource屬性
            String resource = context.getStringAttribute("resource");
            // 擷取properties節點的url屬性。
            String url = context.getStringAttribute("url");
            if (resource != null && url != null) {
                // resource和url屬性隻能同時存在一個。
                throw new BuilderException("properties節點不能同時指定resource和url兩個屬性.");
            }
            // url和resource屬性隻能同時存在一個
            // 讀取引入的資源檔案所有屬性,put到properties節點之下
            if (resource != null) {
                defaults.putAll(Resources.getResourceAsProperties(resource));
            } else if (url != null) {
                defaults.putAll(Resources.getUrlAsProperties(url));
            }
            Properties vars = configuration.getVariables();
            // 如果configuration之前已經有了配置,也put進去
            // put這些設定是為了能夠保證後面set回configuration時可以set所有的配置
            if (vars != null) {
                defaults.putAll(vars);
            }
            parser.setVariables(defaults);
            // 将Properties節點下所有的配置set到configuration
            configuration.setVariables(defaults);
        }
    }
           

接着就是解析settings節點,該節點用于配置一些mybatis配置項

/**
     * 解析settings節點
     *
     * @param context
     * @return
     */
    private Properties settingsAsProperties(XNode context) {
        if (context == null) {
            return new Properties();
        }
        // 擷取settings節點下所有的setting節點
        Properties props = context.getChildrenAsProperties();
        // 通過Configuration擷取metaClass,用于友善對Configuration進行操作
        MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
        for (Object key : props.keySet()) {
            // 周遊setting配置
            // 如果Configuration沒有這個set方法,說明該配置是無效的
            if (!metaConfig.hasSetter(String.valueOf(key))) {
                throw new BuilderException("配置 " + key + " 無效. 請檢查拼寫是否正确.");
            }
        }
        // 校驗完settings之後傳回
        return props;
    }
           

解析完settings節點後,程式會加載使用者配置的虛拟檔案系統和日志。

/**
     * 加載使用者自己設定的虛拟檔案系統
     *
     * @param props
     * @throws ClassNotFoundException
     */
    private void loadCustomVfs(Properties props) throws ClassNotFoundException {
        // 從settings中拿到name是vfsImpl的配置節點
        String value = props.getProperty("vfsImpl");
        if (value != null) {
            String[] clazzes = value.split(",");
            for (String clazz : clazzes) {
                if (!clazz.isEmpty()) {
                    @SuppressWarnings("unchecked")
                    Class<? extends VFS> vfsImpl = (Class<? extends VFS>) Resources.classForName(clazz);
                    // 加載檔案系統,set到Configuration中
                    configuration.setVfsImpl(vfsImpl);
                }
            }
        }
    }

    /**
     * 加載日志。代碼比較簡單
     * 就是從settings中拿到name為logImpl的配置項
     * 然後set到Configuration中去
     *
     * @param props
     */
    private void loadCustomLogImpl(Properties props) {
        Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
        configuration.setLogImpl(logImpl);
    }
           

引申

上面的代碼中使用到了MetaClass類和Configuration類。下面對這兩個類進行解釋。

首先是Configuration類。該類通過名稱可以很明顯的知道這是mybatis的配置類,對應的是mybatis-config.xml檔案的配置。其中今天将properties和settings節點對應的字段進行注釋。

public class Configuration {

    /**
     * mybatis-config.xml屬性
     * settings節點
     * 允許嵌套語句中使用分頁
     */
    protected boolean safeRowBoundsEnabled;


    /**
     * mybatis-config.xml屬性
     * settings節點
     * 是否開啟自動駝峰命名規則映射
     * 即從經典資料庫列名 a_column 到經典 Java 屬性名 aColumn 的類似映射。
     */
    protected boolean mapUnderscoreToCamelCase;
    /**
     * mybatis-config.xml檔案下
     * settings節點
     * 當啟用時,對任意延遲屬性的調用會使帶有延遲加載屬性的對象完整加載;
     * 反之,每種屬性将會按需加載。
     */
    protected boolean aggressiveLazyLoading;

    /**
     * mybatis-config.xml檔案下
     * settings節點
     * 是否允許單一語句傳回多條結果集
     */
    protected boolean multipleResultSetsEnabled = true;

    /**
     * mybatis-config.xml檔案
     * settings節點
     * 允許 JDBC 支援自動生成主鍵
     */
    protected boolean useGeneratedKeys;

    /**
     * mybatis-config.xml檔案
     * settings節點
     * 使用列标簽代替列名
     */
    protected boolean useColumnLabel = true;
    /**
     * mybatis-config.xml檔案
     * settings節點
     * 該配置影響的所有映射器中配置的緩存的全局開關
     */
    protected boolean cacheEnabled = true;

    /**
     * mybatis-config.xml檔案
     * settings節點
     * 指定當結果集中值為null的時候是否調用映射對象的set方法
     */
    protected boolean callSettersOnNulls;


    /**
     * mybatis-config.xml檔案
     * settings節點
     * 指定MyBatis增加到日志名稱的字首
     */
    protected String logPrefix;

    /**
     * mybatis-config.xml檔案
     * settings節點
     * 指定MyBatis所用日志的具體實作
     */
    protected Class<? extends Log> logImpl;

    /**
     * mybatis-config.xml檔案
     * settings節點
     * VFS含義是虛拟檔案系統;
     * 主要是通過程式能夠友善讀取本地檔案系統、FTP檔案系統等系統中的檔案資源。
     * Mybatis中提供了VFS這個配置。
     * 主要是通過該配置可以加載自定義的虛拟檔案系統應用程式
     * 多個檔案系統使用逗号隔開
     */
    protected Class<? extends VFS> vfsImpl;

    /**
     * mybatis-config.xml檔案
     * settings節點
     * mybatis利用本地緩存機制防止循環引用的加速重複嵌套查詢。
     * 預設是SESSION,這種情況會緩存一個會話中執行的所有查詢
     * 如果是STATEMENT,本地會話僅用在語句執行上
     * 對相同的SqlSession的不同調用将不會共享資料
     */
    protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;

    /**
     * mybatis-config.xml檔案
     * settings節點
     * 當沒有為菜蔬提供特定的JDBC類型時
     * 為空值制定JDBC類型
     */
    protected JdbcType jdbcTypeForNull = JdbcType.OTHER;

    /**
     * mybatis-config.xml
     * settings節點
     * 指定哪個對象的方法觸發一次延遲加載
     */
    protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));

    /**
     * mybatis-config.xml檔案
     * settings節點
     * 設定逾時時間
     */
    protected Integer defaultStatementTimeout;

    /**
     * mybatis-config.xml檔案
     * settings節點
     * 為驅動程式設定提示以控制傳回結果的擷取大小
     */
    protected Integer defaultFetchSize;

    /**
     * mybatis-config.xml檔案
     * settings節點
     * 配置預設的執行器。
     * SIMPLE 就是普通的執行器;
     * REUSE 執行器會重用預處理語句(PreparedStatements);
     * BATCH 執行器将重用語句并執行批量更新。
     */
    protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;

    /**
     * mybatis-config.xml檔案
     * settings節點
     * 指定 MyBatis 應如何自動映射列到字段或屬性。
     * NONE 表示取消自動映射;
     * PARTIAL 隻會自動映射沒有定義嵌套結果集映射的結果集。
     * FULL 會自動映射任意複雜的結果集
     */
    protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;

    /**
     * mybatis-config.xml檔案下
     * properties節點的所有配置
     * 以及該節點對應的resource和url的所有配置
     * 在XMLConfigBuilder.propertiesElement方法中進行初始化
     */
    protected Properties variables = new Properties();

    /**
     * mybatis-config.xml檔案
     * settings節點屬性
     * 延遲加載的全局開關。
     * 當開啟時,所有關聯對象都會延遲加載。
     * 特定關聯關系中可通過設定fetchType屬性來覆寫該項的開關狀态
     */
    protected boolean lazyLoadingEnabled = false;

    /**
     * mybatis-config.xml檔案
     * settings節點
     * 指定Mybatis建立具有延遲加載能力對象所用到的代理工廠
     */
    protected ProxyFactory proxyFactory = new JavassistProxyFactory();

    /**
     * 将資料庫類型轉換成Java類型
     */
    protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);

    /**
     * 存儲掃包得到的别名
     */
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();

}
           

而MetaClass是反射工具箱裡的一個類。Reflector是Mybatis中反射子產品的基礎,每個Reflector對象都對應一個類,在該對象中緩存了反射操作需要使用的元資訊,如:可讀屬性、可寫屬性、get、set方法等。ReflectorFactory接口主要實作了對Reflector對象的建立和緩存。而MetaClass則是對Reflector和reflectorFactory的封裝,使其更友善通過反射去操作一個類。

這裡就不帖MetaClass的代碼了,感興趣可以自行閱讀。

結語

今天因為時間充裕是以寫的部落格比較清晰,後面可能會因為加班是以部落格僅僅是對代碼注釋的複制粘貼,還希望讀者可以諒解。這個坑我會繼續填下去的。

最後需要提一下java裡的一個容易被忽視的規範,也是面試、大學考試經常喜歡問的内容。

類中定義的成員變量也稱之為“字段”,而屬性則是指get和set方法。屬性隻與方法有關而與字段無關。如一個類中存在getName()和setName(String name)方法,不管該類中有沒有name字段,我們都認為它有name這個屬性。反之如果隻有字段name而沒有對應的get/set方法,則該類僅僅是有name這個字段而沒有name屬性。後面對于get/set方法我不會稱之為屬性,但是有必要厘清楚這兩個概念。