Mybatis源碼解析-配置、啓動加載

mybatis是目前非常流行的ORM框架,在其基礎上還衍生了mybatis-plus)等優秀的框架。mybatis簡單,易用,易擴展。 接下來我們用一個簡單的例子(幾乎包含所有配置)來看一下mybatis相關的配置項 下面是一個包含用戶、商品、訂單的DAO例子,關係圖如下: 02.learn-mybatis-demo.png

mybatis的配置

MyBatis 的配置文件包含了會深深影響 MyBatis 行爲的設置和屬性信息。 各屬性介紹可以參考[官網](mybatis – MyBatis 3 | 配置) 此示例項目的mybatis-config.xml內容如下

<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE configuration  
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"  
        "https://mybatis.org/dtd/mybatis-3-config.dtd">  
<configuration>  
    <!-- 定義屬性值 -->  
    <properties>  
        <property name="username" value="root"/>  
        <property name="id" value="development"/>  
    </properties>  
  
    <!-- 全局配置信息 -->  
    <!-- 一個配置完整的 settings 元素的示例如下:-->  
    <settings>  
        <!-- 全局性地開啓或關閉所有映射器配置文件中已配置的任何緩存。-->  
        <setting name="cacheEnabled" value="true"/>  
        <!-- 延遲加載的全局開關。當開啓時,所有關聯對象都會延遲加載。 特定關聯關係中可通過設置 fetchType 屬性來覆蓋該項的開關狀態。。-->  
        <setting name="lazyLoadingEnabled" value="true"/>  
        <!-- 開啓時,任一方法的調用都會加載該對象的所有延遲加載屬性。 否則,每個延遲加載屬性會按需加載(參考 lazyLoadTriggerMethods)。(在 3.4.1 及之前的版本中默認爲 true) -->  
        <setting name="aggressiveLazyLoading" value="true"/>  
        <!-- 是否允許單個語句返回多結果集(需要數據庫驅動支持)。 -->  
        <setting name="multipleResultSetsEnabled" value="true"/>  
        <!-- 使用列標籤代替列名。實際表現依賴於數據庫驅動,具體可參考數據庫驅動的相關文檔,或通過對比測試來觀察。 -->  
        <setting name="useColumnLabel" value="true"/>  
        <!-- 允許 JDBC 支持自動生成主鍵,需要數據庫驅動支持。如果設置爲 true,將強制使用自動生成主鍵。儘管一些數據庫驅動不支持此特性,但仍可正常工作(如 Derby)。 -->  
        <setting name="useGeneratedKeys" value="false"/>  
        <!-- 指定 MyBatis 應如何自動映射列到字段或屬性。 NONE 表示關閉自動映射;PARTIAL 只會自動映射沒有定義嵌套結果映射的字段。 FULL 會自動映射任何複雜的結果集(無論是否嵌套)。 -->  
        <setting name="autoMappingBehavior" value="PARTIAL"/>  
        <!-- 指定發現自動映射目標未知列(或未知屬性類型)的行爲。  
            NONE: 不做任何反應  
            WARNING: 輸出警告日誌('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日誌等級必須設置爲 WARN)  
            FAILING: 映射失敗 (拋出 SqlSessionException) -->        <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>  
        <!-- 配置默認的執行器。SIMPLE 就是普通的執行器;REUSE 執行器會重用預處理語句(PreparedStatement); BATCH 執行器不僅重用語句還會執行批量更新。 -->  
        <setting name="defaultExecutorType" value="SIMPLE"/>  
        <!-- 設置超時時間,它決定數據庫驅動等待數據庫響應的秒數。 -->  
        <setting name="defaultStatementTimeout" value="60"/>  
        <!-- 爲驅動的結果集獲取數量(fetchSize)設置一個建議值。此參數只可以在查詢設置中被覆蓋。 -->  
        <setting name="defaultFetchSize" value="100"/>  
        <!-- 指定語句默認的滾動策略。  
         FORWARD_ONLY | SCROLL_SENSITIVE | SCROLL_INSENSITIVE | DEFAULT(等同於未設置)-->  
        <setting name="defaultResultSetType" value="DEFAULT"/>  
        <!-- 是否允許在嵌套語句中使用分頁(RowBounds)。如果允許使用則設置爲 false。 -->  
        <setting name="safeRowBoundsEnabled" value="false"/>  
        <!-- 是否允許在嵌套語句中使用結果處理器(ResultHandler)。如果允許使用則設置爲 false。 -->  
        <setting name="safeResultHandlerEnabled" value="true"/>  
        <!-- 是否開啓駝峯命名自動映射,即從經典數據庫列名 A_COLUMN 映射到經典 Java 屬性名 aColumn。 -->  
        <setting name="mapUnderscoreToCamelCase" value="false"/>  
        <!-- MyBatis 利用本地緩存機制(Local Cache)防止循環引用和加速重複的嵌套查詢。 默認值爲 SESSION,會緩存一個會話中執行的所有查詢。  
            若設置值爲 STATEMENT,本地緩存將僅用於執行語句,對相同 SqlSession 的不同查詢將不會進行緩存。 -->  
        <setting name="localCacheScope" value="SESSION"/>  
        <!-- 當沒有爲參數指定特定的 JDBC 類型時,空值的默認 JDBC 類型。 某些數據庫驅動需要指定列的 JDBC 類型,多數情況直接用一般類型即可,比如 NULL、VARCHAR 或 OTHER。 -->  
        <setting name="jdbcTypeForNull" value="OTHER"/>  
        <!-- 指定對象的哪些方法觸發一次延遲加載。 -->  
        <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>  
        <!-- 指定動態 SQL 生成使用的默認腳本語言。 -->  
        <setting name="defaultScriptingLanguage" value="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver"/>  
        <!-- 指定 Enum 使用的默認 TypeHandler 。 -->  
        <setting name="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumTypeHandler"/>  
        <!-- 指定當結果集中值爲 null 的時候是否調用映射對象的 setter(map 對象時爲 put)方法,這在依賴於 Map.keySet() 或 null 值進行初始化時比較有用。注意基本類型(int、boolean 等)是不能設置成 null 的。 -->  
        <setting name="callSettersOnNulls" value="false"/>  
        <!-- 當返回行的所有列都是空時,MyBatis默認返回 null。 當開啓這個設置時,MyBatis會返回一個空實例。 請注意,它也適用於嵌套的結果集(如集合或關聯)。 -->  
        <setting name="returnInstanceForEmptyRow" value="false"/>  
        <!-- 指定 MyBatis 增加到日誌名稱的前綴。 -->  
        <setting name="logPrefix" value="learn_mybatis_log_"/>  
        <!-- 指定 MyBatis 所用日誌的具體實現,未指定時將自動查找。 -->  
        <!--        <setting name="logImpl"-->        <!--                 value="SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING"/>-->  
        <!-- 指定 Mybatis 創建可延遲加載對象所用到的代理工具。 CGLIB | JAVASSIST-->        <setting name="proxyFactory" value="JAVASSIST"/>  
        <!-- 指定 VFS 的實現 -->  
        <!-- <setting name="vfsImpl" value="org.mybatis.example.YourselfVfsImpl"/> -->        <!-- 允許使用方法簽名中的名稱作爲語句參數名稱。 爲了使用該特性,你的項目必須採用 Java 8 編譯,並且加上 -parameters 選項。(新增於 3.4.1) -->  
        <setting name="useActualParamName" value="true"/>  
        <!-- 指定一個提供 Configuration 實例的類。 這個被返回的 Configuration 實例用來加載被反序列化對象的延遲加載屬性值。 這個類必須包含一個簽名爲static Configuration getConfiguration() 的方法。(新增於 3.2.3) -->  
        <!-- <setting name="configurationFactory" value="org.mybatis.example.ConfigurationFactory"/> -->        <!-- 從SQL中刪除多餘的空格字符。請注意,這也會影響SQL中的文字字符串。 -->  
        <setting name="shrinkWhitespacesInSql" value="true"/>  
  
        <!-- 指定一個擁有 provider 方法的 sql provider 類 (新增於 3.5.6). 這個類適用於指定 sql provider 註解上的type(或 value) 屬性(當這些屬性在註解中被忽略時)。 (e.g. @SelectProvider) -->        <!-- <setting name="defaultSqlProviderType" value="true"/> -->  
        <!-- 爲 'foreach' 標籤的 'nullable' 屬性指定默認值。 -->  
        <setting name="nullableOnForEach" value="false"/>  
  
        <!-- 當應用構造器自動映射時,參數名稱被用來搜索要映射的列,而不再依賴列的順序。-->  
        <setting name="argNameBasedConstructorAutoMapping" value="false"/>  
    </settings>  
  
  
    <typeAliases>  
        <!-- 配置別名信息 -->  
        <typeAlias alias="CustomerEntity" type="com.mingshashan.mybatis.learn.entity.CustomerEntity"/>  
        <typeAlias alias="OrderEntity" type="com.mingshashan.mybatis.learn.entity.OrderEntity"/>  
        <typeAlias alias="OrderItemEntity" type="com.mingshashan.mybatis.learn.entity.OrderItemEntity"/>  
        <typeAlias alias="ProductEntity" type="com.mingshashan.mybatis.learn.entity.ProductEntity"/>  
        <typeAlias alias="TagEntity" type="com.mingshashan.mybatis.learn.entity.TagEntity"/>  
        <typeAlias alias="CustomCache" type="com.mingshashan.mybatis.learn.mybatis.CustomCache"/>  
    </typeAliases>  
  
    <typeHandlers>  
        <typeHandler handler="com.mingshashan.mybatis.learn.mybatis.handler.AddressInfoHandler"/>  
    </typeHandlers>  
  
    <!-- 每次 MyBatis 創建結果對象的新實例時,它都會使用一個對象工廠(ObjectFactory)實例來完成實例化工作。  
        默認的對象工廠需要做的僅僅是實例化目標類,要麼通過默認無參構造方法,        要麼通過存在的參數映射來調用帶有參數的構造方法。 如果想覆蓋對象工廠的默認行爲,可以通過創建自己的對象工廠來實現。 -->  
    <objectFactory type="com.mingshashan.mybatis.learn.mybatis.CustomObjectFactory"/>  
  
    <!-- MyBatis 提供在構造對象的時候,對於指定的對象進行特殊的加工,其配置方式如下 -->  
    <objectWrapperFactory type="com.mingshashan.mybatis.learn.mybatis.CustomMapWrapperFactory"/>  
  
    <reflectorFactory type="com.mingshashan.mybatis.learn.mybatis.CustomReflectorFactory"/>  
  
  
    <environments default="dev">  
        <environment id="dev">  
            <!-- 配置事務管理器的類型 -->  
            <transactionManager type="JDBC"/>  
            <!-- 配置數據源的類型,以及數據庫連接的相關信息 -->  
            <dataSource type="POOLED">  
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>  
                <property name="url" value="jdbc:mysql://192.168.75.138:3306/dev?useSSL=false"/>  
                <property name="username" value="root"/>  
                <property name="password" value="000000"/>  
            </dataSource>  
        </environment>  
        <environment id="test">  
            <!-- 配置事務管理器的類型 -->  
            <transactionManager type="JDBC"/>  
            <!-- 如果environment id爲test則使用JNDI -->  
            <dataSource type="JNDI">  
                <property name="data_source" value="jdbc/mybatis-jndi"/>  
                <property name="initial_context" value="java:/comp/env"/>  
            </dataSource>  
        </environment>  
    </environments>  
  
  
    <databaseIdProvider type="DB_VENDOR">  
        <property name="SQL Server" value="sqlserver"/>  
        <property name="DB2" value="db2"/>  
        <property name="Oracle" value="oracle"/>  
        <property name="Mysql" value="mysql"/>  
    </databaseIdProvider>  
  
    <!-- 配置映射配置文件的位置 -->  
    <mappers>  
        <mapper resource="mapper/CustomerMapper.xml"/>  
        <mapper resource="mapper/OrderItemMapper.xml"/>  
        <mapper resource="mapper/OrderMapper.xml"/>  
        <mapper resource="mapper/ProductMapper.xml"/>  
        <mapper resource="mapper/TagMapper.xml"/>  
    </mappers>  
</configuration>

下面是mapper配置,即xml映射文件的配置,我們只看其中的CustomerMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE mapper  
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"  
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">  
<mapper namespace="com.mingshashan.mybatis.learn.dao.mapper.CustomerMapper">  
  
    <!--  
    cache- 配置本定命名空間的緩存。  
        type- cache實現類,默認爲PERPETUAL,可以使用自定義的cache實現類(別名或完整類名皆可)  
        eviction- 回收算法,默認爲LRU,可選的算法有:  
            LRU– 最近最少使用的:移除最長時間不被使用的對象。  
            FIFO– 先進先出:按對象進入緩存的順序來移除它們。  
            SOFT– 軟引用:移除基於垃圾回收器狀態和軟引用規則的對象。  
            WEAK– 弱引用:更積極地移除基於垃圾收集器狀態和弱引用規則的對象。  
        flushInterval- 刷新間隔,默認爲1個小時,單位毫秒  
        size- 緩存大小,默認大小1024,單位爲引用數  
        readOnly- 只讀  
    -->  
    <cache type="com.mingshashan.mybatis.learn.mybatis.CustomCache"  
           size="2048" flushInterval="60000">  
        <property name="cacheFile" value="./cache.tmp"/>  
    </cache>  
  
    <!--  
    cache-ref–從其他命名空間引用緩存配置。  
        如果你不想定義自己的cache,可以使用cache-ref引用別的cache。因爲每個cache都以namespace爲id,所以cache-ref只需要配置一個namespace屬性就可以了。需要注意的是,如果cache-ref和cache都配置了,以cache爲準。  
    -->  
    <!-- <cache-ref namespace="com.someone.application.data.SomeMapper"/> -->  
  
    <resultMap id="CustomerMap" type="com.mingshashan.mybatis.learn.entity.CustomerEntity">  
        <id property="id" column="id"></id>  
        <result property="name" column="name"></result>  
        <result property="gender" column="gender"></result>  
        <result property="phone" column="phone"></result>  
        <result property="addressList" column="address_info"  
                typeHandler="com.mingshashan.mybatis.learn.mybatis.handler.AddressInfoHandler"  
                javaType="java.util.List"  
                jdbcType="VARCHAR"  
        ></result>  
        <result property="gmtCreated" column="gmt_created"></result>  
        <result property="gmtModified" column="gmt_modified"></result>  
        <result property="isValid" column="is_valid"></result>  
        <collection property="tagEntityList" ofType="TagEntity">  
            <id property="id" column="id"/>  
            <result property="name" column="name"/>  
        </collection>  
    </resultMap>  
  
  
    <sql id="customer_column">  
        id, name, gender, phone, address_info, gmt_created, gmt_modified  
    </sql>  
  
    <insert id="saveCustomer" parameterType="CustomerEntity">  
        INSERT INTO T_ARG_CUSTOMER(  
        <include refid="customer_column"></include>  
        )  
        values(#{id}, #{name}, #{gender}, #{phone}, #{addressList}, #{gmtCreated}, #{gmtModified})  
    </insert>  
  
    <update id="updateCustomer" parameterType="CustomerEntity">  
        update T_ARG_CUSTOMER set  
        name = #{name},  
        gender = #{gender},  
        phone = #{phone},  
        address_info = #{addressList},  
        gmt_modified = #{gmtModified}  
        from t_arg_customer  
        <where>  
            ID = #{id}  
        </where>  
    </update>  
    <update id="deleteCustomerById">  
        update T_ARG_CUSTOMER set is_valid = '0'  
        <where>  
            id = #{id}  
        </where>  
    </update>  
  
    <select id="findById" resultType="CustomerEntity">  
        select  
        <include refid="customer_column"></include>  
        from T_ARG_CUSTOMER  
        <where>  
            id = #{id}  
        </where>  
    </select>  
</mapper>

Mybatis的啓動加載

整個過程的時序圖大概如下 先是XMLConfigBuilder diagram-5542189421490136722.png 再是XMLMapperBuilder diagram-4007552790264809251.png 然後結合着代碼來分析

1.SqlSessionFactoryBuilder

接下來看一下mybatis的啓動加載過程,從mybatis的官方文檔看mybatis的啓動加載入口代碼示例:

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

可以知道,其入口就是new SqlSessionFactoryBuilder().build(inputStream)。接下來我們來分析一下其啓動加載過程。 代碼如下:

public class SqlSessionFactoryBuilder {
	// 方法入口
	public SqlSessionFactory build(Reader reader, String environment, Properties properties) {  
	  try {  
		// 構造XMLConfigBuilder,將解析邏輯交給XMLConfigBuilder去處理
	    XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);  
	    // 執行解析邏輯。解析完後構造DefaultSqlSessionFactory
	    return build(parser.parse());  
	  } catch (Exception e) {  
	    throw ExceptionFactory.wrapException("Error building SqlSession.", e);  
	  } finally {  
	    ErrorContext.instance().reset();  
	    try {  
	       if (reader != null) {  
	         reader.close();  
	       }  
	    } catch (IOException e) {  
	      // Intentionally ignore. Prefer previous error.  
	    }  
	  }  
	}
}

2.XMLConfigBuilder

parse方法會調用parseConfiguration方法解析mybatis的configuration配置

public class XMLConfigBuilder extends BaseBuilder {
	// 解析配置,即將mybatis-config.xml的xml配置轉換爲對應的mybatis模型配置
	private void parseConfiguration(XNode root) {  
	  try {  
		// properties標籤
	    propertiesElement(root.evalNode("properties"));  
	    // settings標籤
	    Properties settings = settingsAsProperties(root.evalNode("settings"));
	    // vfs配置  
	    loadCustomVfs(settings);
	    // 日誌配置  
	    loadCustomLogImpl(settings);  
	    // 別名配置
	    typeAliasesElement(root.evalNode("typeAliases")); 
	    // 插件配置 
	    pluginElement(root.evalNode("plugins"));  
	    // 自定義的objectFactory配置
	    objectFactoryElement(root.evalNode("objectFactory"));  
	    // 自定義的objectWrapperFactory配置
	    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));  
	    // 自定義的reflectorFactory配置
	    reflectorFactoryElement(root.evalNode("reflectorFactory"));  
	    // 其他的settings配置
	    settingsElement(settings);  
	    // 在objectFactory and objectWrapperFactory初始化後,配置environments
	    environmentsElement(root.evalNode("environments"));  
	    // databaseIdProvider
	    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);  
	  }  
	}
}

3.XmlMapperBuilder

可以看到mapperElement會調用XmlMapperBuilder的parse方法去解析處理,其主要代碼如下:

public class XMLMapperBuilder extends BaseBuilder {
	public void parse() {  
	//先從loadedResources判斷是不是已經加載過
	  if (!configuration.isResourceLoaded(resource)) {  
		// 如果沒有加載過,則去處理mapper標籤下面的元素
	    configurationElement(parser.evalNode("/mapper"));  
	    configuration.addLoadedResource(resource);  
	    bindMapperForNamespace();  
	  }  
	  
	  parsePendingResultMaps(); // 解析待定ResultMaps 
	  parsePendingCacheRefs();  // 解析待定ChacheRefs
	  parsePendingStatements(); // 解析待定Statements
	}
}

	private void configurationElement(XNode context) {  
	  try {  
	    String namespace = context.getStringAttribute("namespace");  
	    if (namespace == null || namespace.isEmpty()) {  
	      throw new BuilderException("Mapper's namespace cannot be empty");  
	    }  
	    // 用到了MapperBuilderAssistant
	    builderAssistant.setCurrentNamespace(namespace);  
	    // 處理cache-ref
	    cacheRefElement(context.evalNode("cache-ref"));  
	    // 處理cache
	    cacheElement(context.evalNode("cache"));
	    // 處理parameterMap  
	    parameterMapElement(context.evalNodes("/mapper/parameterMap"));  
	    // 處理resultMap
	    resultMapElements(context.evalNodes("/mapper/resultMap"));  
	    // 處理sql
	    sqlElement(context.evalNodes("/mapper/sql"));  
	    // "select|insert|update|delete" 這些statement的處理
	    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));  
	  } catch (Exception e) {  
	    throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);  
	  }  
}

關於上面的

	  parsePendingResultMaps(); // 解析待定ResultMaps 
	  parsePendingCacheRefs();  // 解析待定ChacheRefs
	  parsePendingStatements(); // 解析待定Statements

可以參考:could not find a parent resultmap with id XMLMapperBuilder.parse() -> 存在resultMap繼承,父resultMap還未加載的,拋出Could not find a parent resultmap with id xx, 被上層XMLMapperBuilder.resultMapElement()方法捕獲, 存儲到 this.configuration.addIncompleteResultMap()即 configuration.IncompleteResultMap 列表內,屬於不完全加載, 每一次parse循環會執行 this.parsePendingResultMaps() 檢驗IncompleteResultMap的父(extends) resultMap是否被加載進來,如果上一次parse加載了,這次就可以處理IncompleteResultMap進行完全加載. 由於每次都要循環檢驗,畢竟有消耗,因此爲避免這種消耗,是否可以讓父resultMap優先加載呢,這樣後面就不會存在IncompleteResultMap , 需要把父mapper.xml放目錄結構父層級 或者 以AxxMapper.xml開頭排同層級之前(注意:這並不能解決後面checkDaoConfig報錯問題)

4.代碼調試過程

SqlSessionFactory的build方法爲mybatis配置加載的入口 image.png 然後XMLConfigBuilder的parseConfiguration解析mybatis-config.xml的配置 接着調用XMLMapperBuilder的parse方法解析所有的mapper.xml文件 image.png 其parse方法執行如下: 之後調用XMLStatementBuilder的parseStatementNode方法 image.png XMLStatementBuilder會交給MapperBuilderAssistant的addMappedStatement方法由MappedStatement的builder模式構造出MappedStatement image.png 再將構造出的MappedStatement放到Configuration的mappedStatements內部map容器中 image.png

5.Mybatis配置的實例化對象Configuration

整個mybatis的配置會保存在org.apache.ibatis.session.Configuration對象(一個全局的實例) 而可以從SqlSessionFactory接口獲取到Configuration,其關係圖如下:

diagram-14250686795475816002.png DefaultSqlSessionFactory會持有Configuration對象

public class DefaultSqlSessionFactory implements SqlSessionFactory {  
  
  private final Configuration configuration;  
  
  public DefaultSqlSessionFactory(Configuration configuration) {  
    this.configuration = configuration;  
  }
}

Configuration的結構如下圖所示: image.png

通過上面的配置信息,代碼調試及代碼執行流程,可以看到mybatis整個啓動加載過程會解析mybatis配置裏面的所有信息。

項目工程地址:learn-mybatis

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