這個系列的文章的開篇《當我們說起看原始碼時,我們是在看什麼》在去年十月份就開始了,今天開始填這個系列的坑。MyBatis是我接觸的第一個ORM框架,也是我目前最為熟悉的ORM框架,對它一直停留在用的階段,今天嘗試來看MyBatis的內部構造。如果還不會MyBatis的,可以先去看下《假裝是小白之重學MyBatis(一)》。
那該如何看原始碼呢?
我是把MyBatis的原始碼下載下來, 茫無目的的看?這會不會迷失在原始碼中呢,我記得我剛到我當前這家公司的時候,看程式碼就是一個一個方法的看,然後感覺很頭疼,也沒看懂最後再做什麼。後面反思了一下,其實應該關注巨集觀的流程,就是這個程式碼實現了什麼功能,這些程式碼都是為了實現這個功能,不必每一個方法都一行一行的看,以方法為單位去看,這個方法從整體上來看做了什麼樣的事情,先不必過多的去關注內部的實現細節。這樣去看對程式碼大概心裡就有數了。同樣的在MyBatis這裡,這也是我第一個特別仔細研究的程式碼,所以MyBatis系列的第一篇,我們先從巨集觀上看其實現,在後面的過程中慢慢補全其細節。本篇的主線是我們在xml中寫的增刪改查語句究竟是怎麼被執行的。
參閱了很多MyBatis原始碼的資料,MyBatis的整體架構可以分為三層:
- 介面層: SqlSession 是我們平時與MyBatis完成互動的核心介面(包括後續整合SpringFramework用到的SqlSessionTemplte)
- 核心層: SqlSession執行的方法,底層需要經過配置檔案的解析、SQL解析,以及執行SQL時的引數對映、SQL執行、結果集對映,另外還有穿插其中的擴充套件外掛。
- 支援層: 核心層的功能實現,是基於底層的各個模組,共同協調完成的。
搭建MyBatis的環境
搭建MyBatis的環境在《假裝是小白之重學MyBatis(一)》已經講過了,這裡只簡單在講一下:
- 引入Maven依賴
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.30</version>
</dependency>
- 然後來一張表
CREATE TABLE `student` (
`id` int(11) NOT NULL COMMENT '唯一標識',
`name` varchar(255) ,
`number` varchar(255) ,
`money` int(255) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4;
- 來個MyBatis的配置檔案
<?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"/>
<!--指定預設環境, 一般情況下,我們有三套環境,dev 開發 ,uat 測試 ,prod 生產 -->
<environments default="development">
<environment id="development">
<!-- 設定事務管理器的管理方式 -->
<transactionManager type="JDBC"/>
<!-- 設定資料來源連線的關聯方式為資料池 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/studydatabase?characterEncoding=utf-8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--設定掃描的xml,org/example/mybatis是包的全類名,StudentMapper.xml會講-->
<mappers>
<!--設定掃描的xml,org/example/mybatis是包的全類名,這個BlogMapper.xml會講
<package name = "org.example.mybatis"/> <!-- 包下批量引入 單個註冊 -->
<mapper resource="org/example/mybatis/StudentMapper.xml"/>
</mappers>
</mappers>
</configuration>
- 來個Student類
public class Student {
private Long id;
private String name;
private String number;
private String money;
// 省略get set 函式
}
- 來個Mapper.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 = "org.example.mybatis.StudentMapper">
<select id = "selectStudent" resultType = "org.example.mybatis.Student">
SELECT * FROM STUDENT
</select>
</mapper>
- 來個介面
public interface StudentMapper {
List<Student> selectStudent();
}
- 日誌配置檔案
log4j.rootCategory=debug, CONSOLE
# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE
# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Encoding=UTF-8
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30
- 開始查詢之旅
public class MyBatisDemo {
public static void main(String[] args) throws Exception {
Reader reader = Resources.getResourceAsReader("conf.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession sqlSession = sqlSessionFactory.openSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
List<Student> studentList = studentMapper.selectStudent();
studentList.forEach(System.out::println);
}
}
執行之後就可以在控制檯看到如下輸出了:
讓我們從SQL的執行之旅開始談起
執行過程淺析
上面的執行過程大致可以分成三步:
- 解析配置檔案,構建SqlSessionFactory
- 通過SqlSessionFactory 拿到SqlSession,進而獲得代理類
- 執行代理類的方法
解析配置檔案
解析配置檔案通過SqlSessionFactoryBuilder的build方法來執行, build方法有幾個過載:
Reader指向了conf檔案, environment是環境,properties用於conf向其他properties取值。我們的配置檔案是一個xml,所以XmlConfigBuilder最終是對配置檔案的封裝。這裡我們不關注XmlBuilder是如何構建的,我們接著往下看,構建Xml物件之後,呼叫parse方法,將其轉換為MyBatis的Configuration物件:
// parseConfiguration 這個方法用於取xml標籤的值並將其設定到Configuration上
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
// 取標籤的過程,XML->Configuration
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
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")); // 獲取mapper方法,
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
- Configuration概覽
- mapperElement
注意我們本篇的主題是重點看我們寫在xml標籤中的sql是如何被執行的,所以我們這裡重點看parseConfiguration的mapperElement的方法。從名字上我們大致推斷,這個方法是載入mapper.xml檔案的。我們點進去看一下:
// parent 是mappers標籤
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) { // 遍歷mappers下面的結點
if ("package".equals(child.getName())) { // 如果是package標籤則走批量引入
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource"); // 我們本次看單個引入的方式
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource); // 載入指定資料夾下的XML
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse(); // 將mapper 中的標籤值對映成MyBatis的物件
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse(); // 我們大致看下parse方法的實現
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
parent引數是mappers標籤,我們可以通過除錯驗證這一點:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
在介紹的時候西安判斷該xml是否已經載入過了, 然後解析mapper標籤下的增刪改查等標籤,我們可以在configurationElement看到這一點。
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");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
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);
}
}
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) { // dataBaseId 用於指明該標籤在哪個資料庫下執行
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
parseStatementNode方法比較長,最終還是在解析Mapper.xml的select、insert、update、delete的屬性, 將解析的屬性傳遞builderAssistant.addMappedStatement()方法中去,該方法引數略多,這來我們上截圖:
到此我們基本結束看構建configuration的過程,我們可以認為在這一步,Mybatis的配置檔案和Mapper.xml已經基本解析完畢。
獲取SqlSession物件
SqlSession是一個介面,有兩個主要實現類:
我們在第一步build出來的事實上是DefaultSqlSessionFactory:
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
這裡事實上openSession也是由DefaultSqlSessionFactory來執行的,我們看下在openSession這個過程中大致做了什麼:
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
注意這個getDefaultExecutorType, 這個事實是MyBatis分層中核心層的SQL執行器,我們接著往下看openSessionFromDataSource:
// level 隔離級別, autoCommit 是否自動提交
// ExecutorType 是一個列舉值: SIMPLE、REUSE、BATCH
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 返回一個執行器,我們看下newExecutor這個方法
final Executor executor = configuration.newExecutor(tx, execType);
// 最後構造出來SqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
// 上面是根據executorType生成對應的執行器
// 如果開啟快取,則將其執行器包裝為另一種形式的執行器
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// interceptorChain 是一個攔截器鏈
// 將該執行器加入到攔截器鏈中增強,這事實上是MyBatis的外掛開發。
// 也是裝飾器模式的應用,後面會講。
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
執行增刪改查
接著我們來看我們的介面中的方法是如何執行的,
StudentMapper執行selectStudent方法事實上進入的應該是對應代理的物件, 我們進入下一步, 事實上是進入了invoke方法,這個invoke方法事實上重寫的InvocationHandler的方法,InvocationHandler是JDK提供的動態代理介面,呼叫被代理的的方法,事實上是會走到這個invoke方法中,實現如下:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 該方法會快取該方法,如果該在快取裡面有,則無需再次產生,裡面的methodCache是ConcurrentHashMap
// 最終會返回MapperMethod物件呼叫invoke方法。
// 我這裡最終的MethodInvoker是PlainMethodInvoker
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
最終的invoke方法如下圖所示:
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
//這個execute太長,裡面根據標籤型別,來做下一步的操縱,這裡我們放截圖
return mapperMethod.execute(sqlSession, args);
}
我們接著來跟executeForMany這個方法的執行:
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
// 預設的分頁
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
// 會走DefaultSqlSession的selectList下面
result = sqlSession.selectList(command.getName(), param);
}
// issue #510 Collections & arrays support
// 轉換結果
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
//
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 這個statement是方法引用:org.example.mybatis.StudentMapper.selectStudent
// 通過這個key就可以從configuration獲取構建的MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
// query裡面會判斷結果是否在快取裡,我們沒有引入快取
// 最終會走的query中的queryFromDatabase方法。
// queryFromDatabase 裡面會呼叫doQuery方法
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
我們這裡重點來看doQuery方法:
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 這裡我們其實已經可以看到MyBatis已經準備在呼叫JDBC了
// Statement 就位於JDBC中
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 根據引數處理標籤中的SQL
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 產生執行SQL的Statement
stmt = prepareStatement(handler, ms.getStatementLog());
// 接著調query方法. 最終會走到PreparedStatementHandler的query方法上
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
// 最終執行SQL
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
PreparedStatement是JDBC,到現在就已經開始呼叫JDBC執行SQL了。resultSetHandler是對JDBC結果進行處理的處理器。
這裡我們把上面遇到的Handler大致梳理一下:
- StatementHandler: 語句處理器
- ResultSetHandler:結果處理器,有結果處理器就有引數處理器
- ParameterHandler: 引數處理器,
總結一下
在MyBatis中我們寫在xml檔案中的SQL語句到底是怎樣被執行這個問題到現在已經有了答案:
- xml中的查詢語句、屬性會被預先的載入進入Configuration中,Configuration中有MappedStatements,這是一個Map,key是標籤的id。
- 我們在執行對應的Mapper的時候,首先要執行獲取Session,在這個過程中會經過MyBatis的攔截器,我們可以選擇在這個過程對MyBatis進行增強
- 呼叫介面對應的方法時, 事實上呼叫的時代理類的方法,代理類會先進行引數處理,根據方法簽名獲取MappedStatement,再轉換,交給JDBC來處理。
到現在我們已經對MyBatis已經有的執行流程已經有一個大致的瞭解了,可能一些方法沒有看太細,因為講那些細節也對巨集觀執行流程沒有太大的幫助。
參考資料
- MyBatis視訊教程(高階篇) 視訊 顏群 https://www.bilibili.com/vide...
- 玩轉 MyBatis:深度解析與定製 https://juejin.cn/book/694491...