文章目錄
- 前言
- 一、SqlSessionFactoryBuilder
- 1.1、XMLConfigBuilder
- 1.2、parse
- 二、mappers標簽的解析
- 2.1、cacheElement
- 2.1.1、緩存策略
- 2.2、buildStatementFromContext
- 2.2.1、sql的解析
前言
??本篇主要介紹MyBatis源碼中的配置文件解析
部分。MyBatis是對于傳統JDBC的封裝,屏蔽了傳統JDBC與數據庫進行交互,組裝參數,獲取查詢結果并自己封裝成對象的繁瑣過程。
??原生MyBatis首先需要配置mybatis-config.xml
:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration><properties resource="jdbc.properties"/><environments default="dev"><environment id="dev"><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><mappers><mapper resource="mapper/UserMapper.xml"/></mappers>
</configuration>
??并且指定數據源jdbc.properties
:
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456
??創建數據庫訪問層接口:
public interface UserMapper {List<User> selectAll();User selectById(int id);void insert(User user);void update(User user);void delete(int id);
}
??以及對應的xml文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.mybatis.mapper.UserMapper"><cache/><resultMap id="userResultMap" type="com.example.mybatis.entity.User"><id property="id" column="id"/><result property="name" column="name"/><result property="age" column="age"/></resultMap><select id="selectAll" resultMap="userResultMap">SELECT * FROM users</select><select id="selectById" resultMap="userResultMap" parameterType="int">SELECT * FROM users WHERE id = #{id}</select><insert id="insert" parameterType="com.example.mybatis.entity.User">INSERT INTO users (name, age) VALUES (#{name}, #{age})</insert><update id="update" parameterType="com.example.mybatis.entity.User">UPDATE users SET name = #{name}, age = #{age} WHERE id = #{id}</update><delete id="delete" parameterType="int">DELETE FROM users WHERE id = #{id}</delete>
</mapper>
??mybatis-config.xml
常見的標簽:
標簽 | 作用 |
---|---|
<settings> | 控制 MyBatis 全局行為(緩存、懶加載、日志等) |
<typeAliases> | 設置類型別名,簡化 Mapper XML 中類名書寫 |
<typeHandlers> | 自定義類型轉換器(Java類型 ? JDBC類型) |
<plugins> | 注冊插件(如分頁插件、SQL打印等) |
<objectFactory> | 自定義對象創建邏輯 |
<environments> | 配置數據庫環境及事務管理 |
<mappers> | 注冊 Mapper 映射文件或 Mapper 接口 |
??原生MyBatis的使用,其中讀取配置文件并進行解析,主要體現在SqlSessionFactoryBuilder
的build
方法中:
public class Main {public static void main(String[] args) throws Exception {//將xml構筑成configuration配置類Reader reader = Resources.getResourceAsReader("mybatis-config.xml");//解析xml,注冊成SqlSessionFactorySqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);try (SqlSession session = sqlSessionFactory.openSession()) {User user = session.selectOne("com.example.mybatis.mapper.UserMapper.selectById", 1);System.out.println(user);}}
}
一、SqlSessionFactoryBuilder
1.1、XMLConfigBuilder
??在調用SqlSessionFactoryBuilder
的build
方法時,首先會去創建一個XMLConfigBuilder
,目的是構建一個XML配置文件解析器對象。
??其中的核心代碼,這段代碼的作用是注冊別名,將配置文件中的 “JDBC”、"POOLED"等關鍵詞和實際的類型進行綁定。
別名 | 實際類 | 用途 |
---|---|---|
"JDBC" | JdbcTransactionFactory | JDBC事務管理器(默認事務方式) |
"MANAGED" | ManagedTransactionFactory | 受容器管理的事務(如 Spring) |
"JNDI" | JndiDataSourceFactory | 從 JNDI 獲取數據源 |
"POOLED" | PooledDataSourceFactory | 數據庫連接池(MyBatis 內置) |
"UNPOOLED" | UnpooledDataSourceFactory | 不使用連接池的數據源 |
"PERPETUAL" | PerpetualCache | 永久緩存 |
"FIFO" | FifoCache | 先進先出緩存 |
"LRU" | LruCache | 最近最少使用緩存 |
"SOFT" | SoftCache | 基于 SoftReference 的緩存 |
"WEAK" | WeakCache | 基于 WeakReference 的緩存 |
"DB_VENDOR" | VendorDatabaseIdProvider | 根據數據庫類型自動切換 SQL |
"XML" | XMLLanguageDriver | MyBatis 默認的 XML SQL 語言驅動器 |
"RAW" | RawLanguageDriver | 原生 SQL 寫法語言驅動器 |
"SLF4J" | Slf4jImpl | 使用 SLF4J 的日志輸出 |
"COMMONS_LOGGING" | JakartaCommonsLoggingImpl | 使用 Commons Logging 日志 |
"LOG4J" | Log4jImpl | 使用 Log4j 日志 |
"LOG4J2" | Log4j2Impl | 使用 Log4j2 日志 |
"JDK_LOGGING" | Jdk14LoggingImpl | 使用 JDK 內建日志 |
"STDOUT_LOGGING" | StdOutImpl | 輸出日志到控制臺 |
"NO_LOGGING" | NoLoggingImpl | 不輸出日志 |
"CGLIB" | CglibProxyFactory | 使用 CGLIB 動態代理 |
"JAVASSIST" | JavassistProxyFactory | 使用 Javassist 動態代理 |
1.2、parse
??真正解析配置文件的是利用上一步構造出的XMLConfigBuilder
的parse
方法,首先會進行判斷,如果已經解析過,則拋出異常,不會重復解析:
??否則就將標記設置為true。并且執行
parseConfiguration
方法,從根節點進行解析:
??每一行都對應了一個 <mybatis-config.xml> 中的標簽,逐步填充 Configuration 對象內容:
/*** 解析 <configuration> 根節點的各個子標簽,并將配置信息填充到 Configuration 對象中*/
private void parseConfiguration(XNode root) {try {// 【1】先解析 <properties> 標簽(必須最優先解析),以便后續標簽中的占位符 ${} 能被正確替換propertiesElement(root.evalNode("properties"));// 【2】解析 <settings> 標簽,將其內容轉換為 Properties 對象Properties settings = settingsAsProperties(root.evalNode("settings"));// 【3】解析 settings 中的 vfsImpl 屬性(如果配置了自定義 VFS 實現類)loadCustomVfs(settings);// 【4】解析 settings 中的 logImpl 屬性(設置日志實現類,如 LOG4J、STDOUT_LOGGING 等)loadCustomLogImpl(settings);// 【5】解析 <typeAliases> 標簽,注冊用戶自定義的別名或包掃描別名typeAliasesElement(root.evalNode("typeAliases"));// 【6】解析 <plugins> 標簽,注冊 MyBatis 插件(如分頁插件、SQL 攔截器等)pluginElement(root.evalNode("plugins"));// 【7】解析 <objectFactory> 標簽,設置自定義對象工廠(用于實例化結果對象)objectFactoryElement(root.evalNode("objectFactory"));// 【8】解析 <objectWrapperFactory> 標簽,自定義對象包裝器(封裝結果對象屬性訪問行為)objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));// 【9】解析 <reflectorFactory> 標簽,自定義反射器工廠(高級反射行為控制)reflectorFactoryElement(root.evalNode("reflectorFactory"));// 【10】將 <settings> 中的配置項應用到 Configuration 對象中settingsElement(settings);// 【11】解析 <environments> 標簽,注冊事務管理器和數據源配置(必須在 objectFactory 之后執行)environmentsElement(root.evalNode("environments"));// 【12】解析 <databaseIdProvider> 標簽,支持數據庫廠商識別(如區分 MySQL、Oracle)databaseIdProviderElement(root.evalNode("databaseIdProvider"));// 【13】解析 <typeHandlers> 標簽,注冊自定義類型處理器(TypeHandler)typeHandlerElement(root.evalNode("typeHandlers"));// 【14】解析 <mappers> 標簽,加載 Mapper 映射器(包括 XML 和接口方式)mapperElement(root.evalNode("mappers"));} catch (Exception e) {// 如果解析過程中發生異常,則封裝為 BuilderException 拋出throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);}
}
??當解析完成后,會得到一個configuration
對象,其中就包含了配置文件中的各種值。相當于此時的xml配置文件已經轉化為了configuration
對象。最后還會將其再次包裝成SqlSessionFactory
,后續會利用SqlSessionFactory
進行sql相關邏輯的執行。
??其中最關鍵的是mappers標簽的解析。
二、mappers標簽的解析
??mapperElement
方法,首先會拿到mappers
根標簽,然后進行解析。
/*** 解析 <mappers> 標簽,支持三種加載方式:package、resource/url、class*/
private void mapperElement(XNode parent) throws Exception {if (parent != null) {// 遍歷 <mappers> 下的所有子節點(可能是 <package> 或 <mapper>)for (XNode child : parent.getChildren()) {// 情況1:<package name="com.xxx.mapper"/>,批量注冊包下所有 Mapper 接口if ("package".equals(child.getName())) {String mapperPackage = child.getStringAttribute("name");// 自動掃描指定包下的所有接口,并注冊到 Configuration 中configuration.addMappers(mapperPackage);} else {// 情況2~4:單個 <mapper> 節點,通過 resource/url/class 指定加載方式String resource = child.getStringAttribute("resource"); // 從 classpath 中加載 Mapper XMLString url = child.getStringAttribute("url"); // 從網絡路徑加載 Mapper XMLString mapperClass = child.getStringAttribute("class"); // 直接加載 Mapper 接口類// 情況2:只指定 resource,加載 Mapper XML 文件if (resource != null && url == null && mapperClass == null) {ErrorContext.instance().resource(resource); // 設置錯誤上下文信息try (InputStream inputStream = Resources.getResourceAsStream(resource)) {XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());mapperParser.parse(); // 解析 Mapper XML,注冊語句映射}// 情況3:只指定 url,加載遠程 Mapper XML 文件} else if (resource == null && url != null && mapperClass == null) {ErrorContext.instance().resource(url);try (InputStream inputStream = Resources.getUrlAsStream(url)) {XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());mapperParser.parse(); // 同樣調用解析邏輯}// 情況4:只指定 class,注冊 Mapper 接口類(無 XML 時適用)} else if (resource == null && url == null && mapperClass != null) {Class<?> mapperInterface = Resources.classForName(mapperClass);configuration.addMapper(mapperInterface); // 注冊接口類到 MapperRegistry// 情況5:配置沖突,三種方式只能選一種,否則拋異常} else {throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");}}}}
}
??案例中對應的是情況2
,首先會注冊一個mapper解析器,然后調用其parse方法對案例中UserMapper.xml
進行解析,在該方法中,首先會進行判斷,如果已經進行過解析,則不會重復解析。
??解析的核心方法在于
configurationElement
,同樣是對于xml中的各種標簽再次分類解析:
??這里重點看一下
cacheElement
以及buildStatementFromContext
。
2.1、cacheElement
??cacheElement
和Mybatis的二級緩存有關。簡單的說,Mybatis有兩級緩存:
- 一級緩存是SqlSession 級別的,并且默認開啟。
- 二級緩存是Mapper 映射級別,默認不開啟,如果需要,應該在某個mapper.xml中使用cache標簽開啟。
??cacheElement
方法正是解析mapper.xml中的cache標簽:
/*** 解析 <cache> 標簽,構建二級緩存對象并注冊到 Configuration 中。*/
private void cacheElement(XNode context) {// 1. 判斷 <cache> 標簽是否存在if (context != null) {// 2. 解析緩存類型(默認是 PERPETUAL,即 PerpetualCache)String type = context.getStringAttribute("type", "PERPETUAL");Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);// 3. 解析緩存淘汰策略(默認是 LRU,即最近最少使用)String eviction = context.getStringAttribute("eviction", "LRU");Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);// 4. 緩存刷新間隔(可選):指定自動清空緩存的時間(毫秒)Long flushInterval = context.getLongAttribute("flushInterval");// 5. 緩存大小(可選):最大緩存對象個數Integer size = context.getIntAttribute("size");// 6. 是否為讀寫緩存(readOnly=false 表示使用序列化;true 表示共享引用)// readWrite = true 表示開啟對象副本,確保線程安全boolean readWrite = !context.getBooleanAttribute("readOnly", false);// 7. 是否阻塞:當緩存正在被其他線程刷新時,是否阻塞等待boolean blocking = context.getBooleanAttribute("blocking", false);// 8. 獲取 <cache> 中配置的其他 <property> 子節點Properties props = context.getChildrenAsProperties();// 9. 構建緩存并注冊到 Configuration,封裝為 MapperBuilderAssistant.useNewCache()builderAssistant.useNewCache(typeClass, // 緩存類型類(如 PerpetualCache)evictionClass, // 淘汰策略類(如 LruCache)flushInterval, // 緩存刷新間隔size, // 緩存容量readWrite, // 是否使用讀寫模式blocking, // 是否阻塞模式props // 自定義屬性);}
}
??在useNewCache
中,最終會調用CacheBuilder
的build
方法:
??
build
方法中運用到了裝飾器模式
,所有的Cache都實現了一個共同的父類Cache。
??在**cache = newCacheDecoratorInstance(decorator, cache);這一行代碼中,傳入LruCache
和當前的Cache
實例(PERPETUAL),將PERPETUAL
包裝到LRU
中:(LruCache的delegate屬性,指向的是傳入的PerpetualCache實例)
??然后繼續執行到cache = setStandardDecorators(cache);**這一行代碼,會繼續進行裝飾器的包裝:
??
setStandardDecorators
方法,對于Cache
實例層層包裝,賦值給各自的delegate
屬性:
??包裝完成的層次:SynchronizedCache線程同步緩存區->LoggingCache統計命中率以及打印日志->SerializedCache序列化->LruCache最少使用->PerpetualCache默認。
2.1.1、緩存策略
??默認的PerpetualCache
,使用的是HashMap
進行存儲。
??而LruCache
,為了實現最近最少使用的機制,使用了LinkedHashMap
的數據結構,并且重寫了它的removeEldestEntry
方法,關鍵在于,LinkedHashMap
構造時第三個參數為 true 表示按訪問順序排列:
LruCache cache = new LruCache(new PerpetualCache("myCache"));
cache.setSize(3);cache.put("A", 1); // A
cache.put("B", 2); // A B
cache.put("C", 3); // A B C
cache.get("A"); // B C A (A 被訪問過,移到尾部)
cache.put("D", 4); // C A D(B 被淘汰)
??SynchronizedCache
,每個方法上通過加synchronized
保證線程安全:
??
LoggingCache
,會記錄日志,以及統計緩存命中次數:
2.2、buildStatementFromContext
??buildStatementFromContext
是用來解析 select、insert、update、delete 標簽中sql語句的方法,首先會解析出這些節點,然后進行循環,獲取到XMLStatementBuilder
后,執行parseStatementNode
方法:
??在
parseStatementNode
方法中有幾個關鍵點,這一段代碼會判斷當前的標簽是否為select,如果是select標簽,則不會清除一級緩存(增刪改會清除),以及判斷是否使用二級緩存(默認 select 使用)
2.2.1、sql的解析
??真正執行解析sql的是下圖中的代碼:
??同樣地會先去構建一個
XMLScriptBuilder
,然后調用其parseScriptNode
方法進行解析:
??在
parseScriptNode
方法中,首先會解析 SQL 標簽中的所有子標簽,然后去進行判斷:
- 包含動態 SQL(即是否包含 if、choose、${} 等動態節點)構建 DynamicSqlSource(運行時動態拼接 SQL)
- 不包含動態 SQL(即是否包含 if、choose、${} 等動態節點)構建 RawSqlSource(直接編譯成靜態 SQL,提升效率)
??
MixedSqlNode
對象,實現了SqlNode
接口,SqlNode是所有動態 SQL節點的統一接口,而MixedSqlNode
代表了 一整個 SQL 腳本塊,比如select標簽中所有內容就會變成一個 MixedSqlNode。
SqlNode 接口
│
├── MixedSqlNode // 組合節點
├── StaticTextSqlNode // 靜態文本節點:普通 SQL 字符串
├── TextSqlNode // 動態文本節點:包含 ${}
├── IfSqlNode // if 標簽
├── ChooseSqlNode // choose/when/otherwise
├── ForEachSqlNode // foreach
├── WhereSqlNode // where
├── TrimSqlNode // trim
├── SetSqlNode // set
└── BindSqlNode // bind
??用一個案例說明,假如我在mapper.xml中定義了如下的sql語句:
<select id="findUser" parameterType="map" resultType="User">SELECT * FROM user<where><if test="name != null">AND name = #{name}</if><if test="age != null">AND age = #{age}</if></where>
</select>
??則生成的結構如下:
MixedSqlNode
├── StaticTextSqlNode(“SELECT * FROM user”)
└── WhereSqlNode
└── MixedSqlNode
├── IfSqlNode(test=“name != null”) → TextSqlNode(“AND name = #{name}”)
└── IfSqlNode(test=“age != null”) → TextSqlNode(“AND age = #{age}”)