上一篇,我們講過DAO層接口,是怎麼被mybatis自動實現的。DAO層接口,爲什麼能操作數據庫
這篇,我們繼續下去,講一講DAO層接口中selectById的流程。
一、準備工作
和上一篇文章中,用到的代碼幾乎沒有區別,只是Main類有所調整
public class Main {
public static SqlSession getSqlSession() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
return sqlSessionFactory.openSession();
}
public static void main(String[] args) throws IOException {
TestDAO testDAO = getSqlSession().getMapper(TestDAO.class);
Test test = testDAO.selectById(1);
testDAO.testDefaultMethod();
//類文件是緩存在java虛擬機中,我們將類文件打印到文件中,便於查看
// generateProxyFile("F:/TestDAOProxy.class");
}
private static void generateProxyFile(String path){
byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0", new Class<?>[]{TestDAO.class});
try(FileOutputStream fos = new FileOutputStream(path)) {
fos.write(classFile);
fos.flush();
System.out.println("代理類class文件寫入成功");
} catch (Exception e) {
System.out.println("寫文件錯誤");
}
}
}
二、開始debug
1、起始位置
從main方法中的Test test = testDAO.selectById(1);這一行開始debug。探究selectById的流程
2、UML圖
這裏先給出一個UML圖,照着這個UML進行講解。當然這裏UML圖太繁瑣了,根本記不住,我們會在最後給出一個簡化的UML圖,方便理解記憶。
3、TestDAO的selectById方法,實際上是調用了它的代理類MapperProxy的invoke方法
那麼我們就來理一理這個invoke方法:
1)Object.class.equals(method.getDeclaringClass()) 這句話的意思是:判斷method這個方法,是不是Object類中的方法,比如沒有在子類中被重寫的toString()、hashCode()、equals()方法。
2)isDefaultMethod(method) 的代碼如下:
/**
* Backport of java.lang.reflect.Method#isDefault()
*/
private boolean isDefaultMethod(Method method) {
return (method.getModifiers()
& (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC
&& method.getDeclaringClass().isInterface();
}
我們可以看到這個判斷語句,前半段的意思是:這個方法被public修飾,但是不被abstract、static修飾。後半段的意思是:這個方法是接口中的方法。
又要是接口中的方法,又不能被abstract、static修飾,接口中真的有這種方法嗎?
確實是有的,JDK8以後,接口可以有方法體。
4、很明顯selectById不是接口默認方法,那麼我們只能走final MapperMethod mapperMethod = cachedMapperMethod(method);這一步了。
從methodCache中get出一個MapperMethod。這個methodCache又是哪來的?
我們回溯一下,發現methodCache,是從MapperProxy中的構造方法來的。那麼看看誰調用了這個構造方法。
好了,我們知道methodCache是自己new出來的。
5、很明顯這個時候methodCache是空的,那麼就要自己new 一個MapperMethod
其中,SqlCommand如下:
6、MapperMethod已經被創建了,我們回到MapperProxy的invoke方法,發現下一步是調用MapperMethod的execute方法
其中Object param = method.convertArgsToSqlCommandParam(args)是獲取參數的值,也就是我們selectById(1)傳進來的1。
7、繼續下一步:result = sqlSession.selectOne(command.getName(), param);
defaultSqlSession的selectOne,實際上是調用了selectList。那麼我們繼續跟蹤selectList
8、接下來調用了CachingExecutor類的query方法。那麼這個CachingExecutor是什麼時候被創建的?回溯一下,發現是在DefaultSqlSessionFactory的openSession方法中,被創建的。也就是我們代碼中的這一句。
我們已經知道了CachingExecutor是什麼時候創建的,那麼繼續探索它的query方法
獲取BoundSql和CacheKey之後,繼續query
又調用了SimpleExecutor的query方法,這裏用到了裝飾器模式(同宗同源)。
9、來到BaseExecutor的queryFromDatabase方法,終於調用了doQuery方法
10、在doQuery方法中,我們來看一下StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
這句話,生成了3個handler,分別是StatementHandler、ParameterHandler、ResultSetHandler。後兩個是作爲StatementHandler的實例變量存在的。
這三個類的用法,可以參考官網,關於 插件 (plugins)的描述。
順道說一下,Executor、StatementHandler、ParameterHandler、ResultSetHandler這四個插件的生成順序是
Executor --> ParameterHandler --> ResultSetHandler --> StatementHandler。
11、生成3個handler之後,就來到了stmt = prepareStatement(handler, ms.getStatementLog());
這個方法的作用類似於我們使用JDBC時,獲取連接,創建PreparedStatement,做一些前期準備。sql語句中的佔位符也被傳進來的參數替代了。
12、執行handler.<E>query(stmt, resultHandler)
前面兩句,是執行sql語句。
resultSetHandler.<E> handleResultSets(ps)這句是處理返回結果,將ResultSet轉換成我們需要的實體對象。
三、簡化版的UML
這個步驟也太冗長了,我麼簡化一下,方便記憶