緣由
Mybatis 框架為作為Java開發人員的必備工具,現在大多使用 mybatis-plus,但是mybatis框架中的執行原理和技術點還是值得一探究竟的。
簡述
Mybatis官網 上面介紹的非常精煉,支援自定義SQL、儲存過程以及高階對映,不過儲存過程現在已經很少使用,其餘兩點使用頻率非常的高。框架最大的特點,也是ORM最大的特點,替代開發人員編寫JDBC程式碼、引數設定、獲取結果集,開發人員只要在註解或者xml檔案中編輯SQL。減輕開發人員工作量,能夠在更有意義的事情上投入更多的精力。
正文
框架替開發人員做了很多事情,這些事情是如何做到的呢?本文會娓娓道來。
一個Mybatis應用的中有一個例項非常的關鍵,就是 SqlSessionFactory。可以從它是怎麼來的、起到了什麼作用兩個方面來詳細說說。
SqlSessionFactotry的由來
SqlSessionFactory 是一個介面類,有兩個實現類 SqlSessionManager\ DefaultSqlSessionFactory,我們先把注意力放在 DefaultSqlSessionFactory 上面,它的建立則是在 SqlSessionFactoryBuilder 中,顯然是透過構造者模式進行的建立,若一個物件建立的過程不是簡單兩行程式碼的事情,有必要使用構造器模式,將建立邏輯獨立出來,與類的定義進行隔離。
SqlSessionFactoryBuilder 中的構建邏輯最終目的就是 new DefaultSqlSessionFactory(Configuration config)
,所有的構建方式最終會生成 Configuration,可以是 XMLConfigBuilder.parse(),也可以使用new Configuration。兩種方式都會將開發人員對Mybatis應用的配置轉換到 SqlSessionFactory 的建立過程中。
SqlSessionFactotry的使用
SqlSessionFactory 的目的就是為了建立一個SqlSession,有多個過載的介面方法 openSession(),最終會執行到 new DefaultSqlSession(configuration, executor)
,configuration還是熟悉的那個,Executor 是一個介面,有幾個簡單的實現 BatchExecutor 、ReuseExecutor、SimpleExecutor、CachingExecutor,可以根據引數執行指定,兩外開發人員自定義的Interceptor也會整合到Executor實現當中,Executor 的構造需要 Configuration 和 transaction(後續單獨說明)。
SqlSession介面定義了crud 和 事務相關 快取相關 的方法。最終的執行還是會使用 Executor 實現類。比如所有的查詢方法會進入到 List<E> selectList(String statement, Object parameter, RowBounds rowBounds)
,statement就是開發人員指定的SQL語句識別符號,呼叫executor之前,透過 configuration.getMappedStatement(statement) 獲取到 MappedStatement 作為執行物件,參與執行過程。
Executor 執行器的query(),主要是呼叫 doQuery(),在呼叫前會建立快取key,doQuery()中存在sqlSession 級別的快取邏輯,快取沒有命中會進入 queryFromDatabase()。
queryFromDatabase() 方法會將執行結果放入快取,執行邏輯是在 doQuery(ms, parameter, rowBounds, resultHandler, boundSql) 中,此方法是一個抽象方法,進入 SimpleExecutor的抽象實現中。
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 根據開發人員定義的sql語句時指定的 StatementType 生成對應的 StatementHandler
// 這一點也是一個擴充套件實現
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, rowBounds, resultHandler, boundSql);
// 建立 statement ,並將sql引數放入到 statement 中
stmt = prepareStatement(handler, ms.getStatementLog());
// 執行查詢,並對映結果集
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
// 執行查詢,並對映結果集
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.<E> handleResultSets(ps);
}
以上就是mybatis應用中 sqlSessionFactory 大致執行邏輯,下面會從框架的擴充套件性方面說明。
擴充套件性
StatementHandler 擴充套件
其實現類 RoutingStatementHandler 採用簡單工廠和裝飾器的方式,根據 StatementType 列舉建立不同的實現。
switch (ms.getStatementType()) {
case STATEMENT:
// sql 直接執行
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
// 使用 PreparedStatement 預處理引數,防止sql注入
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
// 使用 CallableStatement 方式,可以指定額外的回撥
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
Executor 擴充套件
Configuration 類中可根據 ExecutorType 建立指定的 Executor 實現類
public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
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);
}
if (cacheEnabled) {
// 裝飾模式,增加sqlSessionFactory級別的快取
// 二級快取當中的事務也需要特別處理
executor = new CachingExecutor(executor, autoCommit);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
外掛機制
SQL執行過程中,可以對其進行監控和攔截呼叫。外掛的執行邏輯入口在 configuration中,ParameterHandler、ResultSetHandler、StatementHandler、Executor 獲取過程中都會執行 interceptorChain.pluginAll() 方法。
interceptorChain負責收集開發人員定義的interceptor,並執行pluginAll() 對目標物件進行增強,採用動態代理的方式,將目標物件進行包裝。
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public static Object wrap(Object target, Interceptor interceptor) {
// 獲取自定義的interceptor上面註解
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
// 獲取目標物件的介面與攔截器的註解對比,匹配到的介面
// 下面的代理物件會根據匹配到的介面建立
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
// 指定代理邏輯所在的類
new Plugin(target, interceptor, signatureMap));
}
return target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
// 攔截器中的邏輯得到執行
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
Mapper 代理物件
開發過程當中總會在使用依賴注入的方式引入 Mapper 物件,開發業務功能時總會定義一些UserMapper 之類的介面,方法上面使用註解SQL或者xml檔案與Mapper繫結的方式編寫SQL。
本身是一個介面,卻可以當作物件引入,必定是Mybatis提供了動態代理機制,將一個介面生成為一個代理物件,供其他類使用。
SqlSession 提供一個介面方法 <T> T getMapper(Class<T> type);
定義獲取Mapper代理物件的行為,DefaultSqlSession 中使用了 configuration.getMapper(type,this); --> mapperRegistry.getMapper(type, sqlSession); 具體實現是在 mapperRegistry當中:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null)
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
try {
// mapperProxyFactory 肯定會建立一個 mapperProxy 物件,代理物件mapper的執行 也會在 mapperProxy 當中
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
// 實現 InvocationHandler 介面
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// MapperMethod 物件代表一個userMapper中的介面方法
final MapperMethod mapperMethod = cachedMapperMethod(method);
// execute 方法中的執行邏輯最終會呼叫到 sqlSession 完成與資料庫的互動
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
事務
開發人員可指定事務管理 TransactionFactory,不過一般都是 JdbcTransactionFactory,使用資料庫事務,獲取sqlSession 指定是事務隔離級別,作用在JdbcTransactionFactory 建立資料庫連線時指定到連線當中。
JdbcTransactionFactory 建立的 JdbcTransaction 會參與到執行器executor的建立中,執行器中的事務方法最終會呼叫到 JdbcTransaction 執行事務呼叫。
public void commit(boolean required) throws SQLException {
if (closed) throw new ExecutorException("Cannot commit, transaction is already closed");
// 清空一級快取
clearLocalCache();
flushStatements();
if (required) {
// 事務提交
transaction.commit();
}
}
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
// 事務回滾
transaction.rollback();
}
}
}
}
總結
Mybatis框架提供了半orm機制,開發人員可以編輯SQL,剩下的工作交給框架處理,節省開發人員精力。框架的原理進行了梳理,執行流程基本說明,篇幅有限,其中的技術細節可自行延申。