解讀 MyBatis 源碼:探尋數(shù)據(jù)持久化的奧秘
在當(dāng)今軟件開發(fā)的廣袤領(lǐng)域中,MyBatis 作為一款備受青睞的持久層框架,以其強大的功能和靈活的特性發(fā)揮著重要作用。當(dāng)我們深入探究 MyBatis 的源碼時,就如同開啟一扇通往技術(shù)奧秘的大門。
在這里,每一行代碼都蘊含著智慧與巧思,每一個模塊都承載著獨特的設(shè)計理念。我們將逐步揭開 MyBatis 源碼那神秘的面紗,去追尋它高效數(shù)據(jù)處理、靈活映射機制以及出色性能表現(xiàn)背后的根源。通過對其源碼的仔細剖析,我們不僅能更深刻地理解 MyBatis 是如何工作的,更能汲取其中的精髓,為我們自身的技術(shù)成長和項目實踐提供寶貴的經(jīng)驗和啟示。讓我們懷揣著對技術(shù)的好奇與探索之心,正式踏上 MyBatis 源碼解析的精彩旅程……
詳解Mybatis的功能架構(gòu)與核心技術(shù)
按照工作層次劃分可以分為三層:
- 接口層:也就是我們用戶用到的這一層,提供各種對數(shù)據(jù)的CRUD以及配置信息維護的API調(diào)用。
- 數(shù)據(jù)處理層:這層是框架為上層提供的關(guān)鍵,這一層實現(xiàn)參數(shù)映射,SQL解析,SQL執(zhí)行,結(jié)果處理。
- 基礎(chǔ)支撐層:負責(zé)連接管理、配置加載,事務(wù)管理、緩存機制等。
詳解Mybatis執(zhí)行過程
本質(zhì)上mybatis執(zhí)行過程大體是:
- 參數(shù)映射
- sql解析
- sql執(zhí)行
- 結(jié)果和處理映射
我們以下面這段查詢代碼為例,針對該流程進行深入講解:
Tb1Example tb1Example = new Tb1Example();
tb1Example.createCriteria().andBirthdayIsNull();
List<Tb1> tb1List = SpringUtil.getBean(Tb1Mapper.class).selectByExample(tb1Example);
log.info("tb1List: {}", tb1List);
本質(zhì)上我們所使用的Tb1Mapper是基于我們的xml配置動態(tài)代理生成的一個MapperProxy,在執(zhí)行查詢請求時被本質(zhì)上就調(diào)用這個生成代理對象,以我們的selectByExample為例,在初始配置的時候我們指明了select標(biāo)簽在進行代理創(chuàng)建時該方法就會被標(biāo)準(zhǔn)為SELECT命令請求,執(zhí)行時就會按照代理的查詢邏輯執(zhí)行。
隨后代理的MapperProxy會調(diào)用MapperMethod進行參數(shù)解析,將參數(shù)轉(zhuǎn)換為后續(xù)可拼接到xml中所配置sql語句中的參數(shù)。
然后SqlSessionTemplate通過內(nèi)部sqlSessionProxy的selectList著手進行實際查詢工作,其內(nèi)部會拿到當(dāng)前sql連接的session和xml中配置的sql還有我們上述步驟的參數(shù)生成jdbc的Statement然后通過SimpleExecutor執(zhí)行sql查詢,然后通過resultSetHandler將結(jié)果解析并返回:
對此我們也給出相應(yīng)的源碼,首先從MapperProxy開始調(diào)用mapperMethod進行參數(shù)解析。
//MapperProxy的invoke方法調(diào)用mapperMethod
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
//MapperMethod解析參數(shù)并基于指令匹配SQL操作
```java
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
//......
}
case UPDATE: {
//......
}
case DELETE: {
//......
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
//......
} else if (method.returnsMany()) {
//內(nèi)部進行參數(shù)解析和查詢調(diào)用
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
//......
} else {
//......
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
//......
return result;
}
隨后步入MapperMethod進行通過convertArgsToSqlCommandParam參數(shù)解析,底層在基于sqlSession著手查詢和結(jié)果轉(zhuǎn)換:
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
//參數(shù)解析
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
//
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(command.getName(), param);
}
//......
return result;
}
最終來到SimpleExecutor的doQuery方法,通過xml配置所得的各種信息生成StatementHandler創(chuàng)建出Statement ,再通過resultSetHandler處理結(jié)果并返回:
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
//通過配置信息生成StatementHandler
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//基于StatementHandler 生成Statement
stmt = prepareStatement(handler, ms.getStatementLog());
//內(nèi)部通過Statement 執(zhí)行sql并將結(jié)果交由resultSetHandler轉(zhuǎn)換并返回
return handler.query(stmt, resultHandler);
} finally {
//......
}
}
為什么Mybatis不需要實現(xiàn)類
是通過代理生成的,我們不妨通過源碼來看看究竟,以下面這段代碼作為入口講解原生mapper創(chuàng)建思路:
SqlSession sqlSession = SpringUtil.getBean(SqlSessionFactory.class).openSession();
Tb1Mapper mapper = sqlSession.getMapper(Tb1Mapper.class);
本質(zhì)上getMapper會基于接口和sqlSession信息通過mapper創(chuàng)建工廠mapperProxyFactory ,然后mapperProxyFactory 底層通過反射的方式創(chuàng)建JDK動態(tài)代理mapper對象:
對此我們給出getMapper的入口,邏輯和筆者說的一樣mapperProxyFactory 傳遞元信息進行動態(tài)代理創(chuàng)建:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
//......
try {
//通過mapperProxyFactory創(chuàng)建動態(tài)代理
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
查看newInstance方法即可看到我們所說的基于類加載器、接口信息和methodCache內(nèi)部的MapperMethodInvoker完成動態(tài)代理對象的創(chuàng)建:
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
如何實現(xiàn)Mybatis插件
Mybatis支持對ParameterHandler、ResultSetHandler、StatementHandler、Executor進行攔截,例如我們想對mybatis查詢的SQL結(jié)果解析階段進行攔截,我們可以編寫下面這樣一段代碼:
import java.sql.Statement;
//@Intercepts({@Signature(
// type = Executor.class, //確定要攔截的對象
// method = "query", //確定要攔截的方法
// args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} //攔截方法的參數(shù)
//)})
@Intercepts({@Signature(
type = ResultSetHandler.class, //確定要攔截的對象
method = "handleResultSets", //確定要攔截的方法
args = {Statement.class} //攔截方法的參數(shù)
)})
public class MyInterceptor implements Interceptor {
private static Logger logger = LoggerFactory.getLogger(MyInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
logger.info("請求被攔截,攔截類:[{}],請求方法:[{}]請求參數(shù)[{}]", invocation.getTarget().getClass().getName(),
invocation.getMethod().getName(),
invocation.getArgs());
//如果當(dāng)前代理的是一個非代理對象,那么就會調(diào)用真實攔截對象的方法
// 如果不是它就會調(diào)用下個插件代理對象的invoke方法
Object obj = invocation.proceed();
logger.info("請求被攔截結(jié)果:[{}]", obj);
return obj;
}
}
然后配置文件,增加對這個攔截類的配置:
<plugins>
<plugin interceptor="com.sharkchili.mapper.MyInterceptor">
<property name="dbType" value="mysql"/>
</plugin>
</plugins>
執(zhí)行我們的請求:
// 可以從配置或者直接編碼來創(chuàng)建SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2)通過SqlSessionFactory創(chuàng)建SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
//3)通過sqlsession執(zhí)行數(shù)據(jù)庫操作
User1Mapper user1Mapper = sqlSession.getMapper(User1Mapper.class);
User1 user = user1Mapper.select("1");
logger.info("查詢結(jié)果:[{}]", user.toString());
if (sqlSession != null) {
sqlSession.close();
}
從輸出結(jié)果就可以看出,我們的方法攔截到了結(jié)果處理的邏輯了。
2022-11-30 10:11:55,389 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Preparing: select * from user1 where id = ?
2022-11-30 10:11:55,526 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Parameters: 1(String)
[main] INFO com.sharkchili.mapper.MyInterceptor - 請求被攔截,攔截類:[org.apache.ibatis.executor.resultset.DefaultResultSetHandler],請求方法:[handleResultSets]請求參數(shù)[[org.apache.ibatis.logging.jdbc.PreparedStatementLogger@12d2ce03]]
2022-11-30 10:12:06,928 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - <== Total: 1
[main] INFO com.sharkchili.mapper.MyInterceptor - 請求被攔截結(jié)果:[[User1{id='1', name='小明', user2=null}]]
[main] INFO com.sharkchili.mapper.MyBatisTest - 查詢結(jié)果:[User1{id='1', name='小明', user2=null}]
2022-11-30 10:12:06,938 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@24959ca4]
Mybatis插件的工作原理
Mybatis如何引入自定義插件?
我們的業(yè)務(wù)代碼如下,創(chuàng)建SqlSessionFactory:
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
步入build邏輯會看到xml解析相關(guān)代碼有一行,如下所示,可以看出他就是對xml文件中plugins標(biāo)簽進行解析:
//獲取標(biāo)簽內(nèi)容并反射生成攔截器存到某個list中
this.pluginElement(root.evalNode("plugins"));
其內(nèi)部做的就是解析xml配置,生成攔截器對象MyInterceptor,并存放到interceptorChain中的一個名為interceptors的list中。
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
Iterator var2 = parent.getChildren().iterator();
while(var2.hasNext()) {
//解析配置生成Interceptor
XNode child = (XNode)var2.next();
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor)this.resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
//存到攔截器鏈中
this.configuration.addInterceptor(interceptorInstance);
}
}
}
我們看看addInterceptor,邏輯非常簡單,說白了就是存到一個名為interceptors的list集合中,然后進行鏈?zhǔn)秸{(diào)用:
public void addInterceptor(Interceptor interceptor) {
this.interceptorChain.addInterceptor(interceptor);
}
執(zhí)行真正邏輯,調(diào)用插件:
//3)通過sqlsession執(zhí)行數(shù)據(jù)庫操作
User1Mapper user1Mapper = sqlSession.getMapper(User1Mapper.class);
User1 user = user1Mapper.select("1");
注意在Plugin的signatureMap插個斷點,如下所示:
這時候進行debug,我們可以看到堆棧中停在這樣一段代碼上。由于我們編寫了一個結(jié)果解析的攔截插件MyInterceptor,所以在newResultSetHandler時會從上文注冊的interceptorChain中取出對應(yīng)處理器,給我們的resultSetHandler ,這過程中piugin類會通過Plugin.wrap(target, this);對我們的結(jié)果處理類進行包裝。
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
//獲取我們的結(jié)果解析器
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
//調(diào)用pluginAll將所有插件都引入
ResultSetHandler resultSetHandler = (ResultSetHandler)this.interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
所以我們步入看看wrap的邏輯,可以看到interceptor即我們自己編寫的插件,他會通過getSignatureMap獲取這個我們編寫插件MyInterceptor注解上的信息,通過反射生成一個新的代理對象,這個對象存放著signatureMap。
public static Object wrap(Object target, Interceptor interceptor) {
//獲取自定義插件信息存到signatureMap 中
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 使用插件包裝我們的目標(biāo)類
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
}
執(zhí)行SQL邏輯:
最終我們的結(jié)果處理插件會在handleResultSets階段發(fā)現(xiàn)signatureMap里面有值,當(dāng)前處理器有攔截,執(zhí)行this.interceptor.intercept(new Invocation(this.target, method, args))攔截相關(guān)處理器執(zhí)行我們的MyInterceptor邏輯。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//發(fā)現(xiàn)signatureMap有值
Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
//methods 中包含我們的這個階段的方法handleResultSets,故調(diào)用this.interceptor.intercept(new Invocation(this.target, method, args))
return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
} catch (Exception var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
Mybatis的分頁插件的實現(xiàn)原理
通過上面工作原理的介紹我們就知道原理了,分頁插件就是通過mybatis提供的接口,攔截Executor的query方法,重寫執(zhí)行的sql,例如select * from student,攔截sql后重寫為:select t.* from (select * from student) t limit 0, 10;即可。