從源碼的角度弄懂MyBatis動態代理開發原理
MyBatis提供了一種動態代理實現SQL調用的功能,使用者只需要在映射文件中配置SQL語句與映射規則即可完成SQL調用和結果集封裝。下面代碼展示了動態代理調用的基本步驟:
public void testMyBatisBuild() throws IOException {
InputStream input = Resources.getResourceAsStream("SqlSessionConfig.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(input);
SqlSession sqlSession = sessionFactory.openSession();
TestMapper mapper = sqlSession.getMapper(TestMapper.class);
Student student = mapper.getStudentByIdToResultType("00000b373502481baa1a5f5229507cf8");
System.out.println(student);
}
而我們通過getMapper方法只是傳入了一個接口,在整個項目中我們沒有一個TestMapper的實現類,那MyBatis是如何幫我們生成實現類的,這一點就需要我們去分析源碼了。
一. DefaultSqlSessionFactory
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(input);
build方法用于解析配置文件并生成SqlSessionFactory,我們通過跟蹤build方法的源碼,我們可以發現,build方法實際上返回的是DefaultSqlSessionFactory的實例。DefaultSqlSessionFactory就是SqlSessionFactory接口的唯一實現類。
public SqlSessionFactory build(InputStream inputStream, String environment) {
return build(inputStream, environment, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//創建XMLConfigBuilder,用于解析配置文件
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
`
二. DefaultSqlSession
2.1 openSession()
SqlSession sqlSession = sessionFactory.openSession();
上面代碼是通過SqlSessionFactory獲取SqlSession的代碼,我們進入DefaultSqlSessionFactory::openSession方法一探究竟,看看它底層到底做了什么。
可以看到DefaultSqlSessionFactory::openSession方法最終生成了一個DefaultSqlSession實例,它就是Mybatis核心接口SqlSession的實現類。
public SqlSession openSession() {
//從數據源中獲取連接,然后創建SqlSessionFactory
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//獲取mybatis-config.xml中的enviroment對象
final Environment environment = configuration.getEnvironment();
//從Enviroment獲取TranslationFactory
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
//從數據源中獲取數據庫連接,然后創建Transaction對象
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//重點:根據配置創建Executor,該方法內部會根據用戶是否配置二級緩存去決定是否創建二級緩存的裝飾器去裝飾Executor,這也是二級緩存是否生效的關鍵
final Executor executor = configuration.newExecutor(tx, execType);
//創建DefaultSqlSession
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();
}
}
2.2 getMapper()獲取動態代理實例
TestMapper mapper = sqlSession.getMapper(TestMapper.class);
走到這里我們就來到今天主題的核心,我們現在就來抽絲剝繭般的看看MyBatis是如何幫我們生成動態代理實現類的。
首先我們進入DefaultSqlSession::getMapper方法,可以看到實際上它是調用的Configuration::getMapper方法獲取的代理實例:
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
而Configuration是MyBatis初始化后全局唯一的配置對象,它內部保存著配置文件解析過程中所有的配置信息。進入Configuration::getMapper我們可以發現它實際上調用的是MapperRegistry::getMapper方法:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//調用mapper注冊中心的getMapper方法獲取Mapper動態代理實現類
return mapperRegistry.getMapper(type, sqlSession);
}
MapperRegistry是Mapper接口動態代理工廠類的注冊中心,我們繼續進入MapperRegistry::getMapper方法,可以看到它實際上調用的是MapperProxyFactory::newInstance方法。
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 {
//使用Mapper代理工廠創建動態代理實例
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
MapperProxyFactory是生成動態代理對象的工廠類,走到這里,我有一種預感,我們離真相越來越近了。我們進入MapperProxyFactory::newInstance一探究竟:
public T newInstance(SqlSession sqlSession) {
//MapperProxy實現了InvocationHandler接口。它是Mapper動態代理的核心類
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
//創建動態代理實例
return newInstance(mapperProxy);
}
我們先暫時略過MapperProxy內部的處理流程,我們先看看newInstance方法內部是如何創建動態代理實例的。
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
看到上面的代碼是不是有一種恍然大悟的感覺,它是JDK動態代理的核心API,也就是說Mybatis底層是調用JDK的Proxy類來創建代理實例。對JDK動態代理不熟悉的小伙伴可以看看博主的另一篇文章:JDK動態代理的深入理解。
三. 動態實例是如何執行的
代理實例如何創建的過程我們已經清楚了,現在我們需要了解代理類內部是如何實現SQL語句的執行的。我們進入MapperProxy::invoke方法:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
//如果是Object方法,則調用方法本身
return method.invoke(this, args);
} else {
//根據被調用接口方法的Method對象,從緩存中獲取MapperMethodInvoker對象,如果沒有則創建一個并放入緩存,然后調用invoke。
//換句話說,Mapper接口中的每一個方法都對應一個MapperMethodInvoker對象,而MapperMethodInvoker對象里面的MapperMethod保存著對應的SQL信息和返回類型以完成SQL調用
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
看了看invoke方法內部,似乎并沒有看出真實的調用邏輯,那我們就先進入cacheInvoker方法中看看吧:
/**
* 獲取緩存中MapperMethodInvoker,如果沒有則創建一個,而MapperMethodInvoker內部封裝這一個MethodHandler
* @param method
* @return
* @throws Throwable
*/
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return methodCache.computeIfAbsent(method, m -> {
if (m.isDefault()) {
//如果調用接口的是默認方法(JDK8新增接口默認方法的概念)
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
//如果調用的普通方法(非default方法),則創建一個PlainMethodInvoker并放入緩存,其中MapperMethod保存對應接口方法的SQL以及入參和出參的數據類型等信息
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
可以看到進入cacheInvoker方法后首先會判斷用戶當前調用的是否是接口的default方法,如果不是就會創建一個PlainMethodInvoker對象并返回。
PlainMethodInvoker:類是Mapper接口普通方法的調用類,它實現了MethodInvoker接口。其內部封裝了MapperMethod實例。
MapperMethod:封裝了Mapper接口中對應方法的信息,以及對應的SQL語句的信息;它是mapper接口與映射配置文件中SQL語句的橋梁。
此時我們跳出cachedInvoker方法回到MapperProxy::invoke方法中。
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
我們可以看到當cacheInvoker返回了PalinMethodInvoker實例之后,緊接著調用了這個實例的PlainMethodInvoker::invoke方法。進入PlainMethodInvoker::invoke方法我們發現它底層調用的是MapperMethod::execute方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
//Mybatis如何幫助用戶實現動態代理的玄機就在里面
return mapperMethod.execute(sqlSession, args);
}
進入MapperMethod::invoke方法我們會發現眼前一亮,這就是MyBatis底層動態代理的邏輯,可以看到動態代理最后還是使用SqlSession操作數據庫的:
*/
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
// 將args進行解析,如果是多個參數則,則根據@Param注解指定名稱將參數轉換為Map,如果是封裝實體則不轉換
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
//查詢操作
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
//解析參數,因為SqlSession::selectOne方法參數只能傳入一個,但是我們Mapper中可能傳入多個參數,
//有可能是通過@Param注解指定參數名,所以這里需要將Mapper接口方法中的多個參數轉化為一個ParamMap,
//也就是說如果是傳入的單個封裝實體,那么直接返回出來;如果傳入的是多個參數,實際上都轉換成了Map
Object param = method.convertArgsToSqlCommandParam(args);
//可以看到動態代理最后還是使用SqlSession操作數據庫的
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
四. 動態代理調用過程時序圖

由于圖片版幅較大,網頁顯示字體極小,這里博主給出下載鏈接供大家下載:MyBatis核心調用流程時序圖-GitHub;MyBatis核心調用流程時序圖-Gitee
五. 總結
Mapper動態代理是通過MyBatis生成接口的實現類,然后調用SqlSession中的方法執行數據庫操作。上文只是對MyBatis這個過程源碼的簡單分析,希望讀者在讀后能夠明白Mapper動態代理的幾個核心問題:
- MyBatis如何生成動態代理實例的?
- MyBatis如何根據不同的情況將Mapper接口“翻譯”成
SqlSession調用的- 如何確定調用
SqlSession中的那個方法? - 如何確定命名空間和調用的
statementId? - 如何傳遞參數給
SqlSession?
- 如何確定調用
- 從源碼的角度看Mapper代理實例和
SqlSession是一對一的關系,而SqlSession是線程不安全的,那么在Spring和MyBatis集成的時候,項目的整個生命周期中Mapper接口是單例的(通常情況),那么MyBatis是如何解決SqlSession線程安全問題的?(這個問題在這個部分的源碼分析中暫時得不到解決)
最后,博主自己對MyBatis源碼進行了詳細注釋,如有需要,請移步至:GitHub或Gitee
本文闡述了自己對MyBatis源碼的一些理解,如有不足,歡迎大佬指點,感謝感謝!!

浙公網安備 33010602011771號