[干貨]源碼級探究Mybatis原理-以查詢?yōu)槔?/h1>
作為一名Java后端開發(fā)者,尤其是國內(nèi)開發(fā)者,從剛參加工作開始就與Mybatis打交道了。
用了這么久的Mybatis難免會心生疑問:
我只是寫了個Mapper接口,再配合xml或者注解,把SQL一寫,就可以執(zhí)行數(shù)據(jù)庫操作,這是為何? 都說Mybatis是對JDBC的封裝,可是我卻看不到JDBC相關(guān)的接口和對象,它們到哪里去了? 為什么在Spring中使用Mybatis,不用加@Repository/@Component之類的注解,就可以隨用隨注入(如:@Autowired)?
?硬核萬字長文,點個在看,轉(zhuǎn)發(fā),多謝啦~
?
隨著工作經(jīng)驗越多,對這些問題的疑惑就會越發(fā)強烈。而讀源碼是解決這些疑問的根本方法。
那么就跟隨筆者的腳步,試著用一篇文章,以一個查詢?yōu)槔?,從源碼角度一步一步揭開Mybatis的神秘面紗。
一、先看一個demo
????private?SqlSessionFactory?sqlSessionFactory;
????@Before
????public?void?prepare()?throws?IOException?{
????????String?resource?=?"mybatis-config.xml";
????????InputStream?inputStream?=?Resources.getResourceAsStream(resource);
????????sqlSessionFactory?=?new?SqlSessionFactoryBuilder().build(inputStream);
????}
????/**
?????*?通過?SqlSession.getMapper(XXXMapper.class)??接口方式
?????*?@throws?IOException
?????*/
????@Test
????public?void?testSelect()?throws?IOException?{
????????SqlSession?session?=?sqlSessionFactory.openSession();?//?ExecutorType.BATCH
????????try?{
????????????BlogMapper?mapper?=?session.getMapper(BlogMapper.class);
????????????Blog?blog?=?mapper.selectBlogById(1);
????????????System.out.println(blog);
????????}?finally?{
????????????session.close();
????????}
????}
這是一個非Spring項目的Test用例類,邏輯很直觀,就是在測試通過id查詢一行記錄;在執(zhí)行查詢之間加載配置文件。
執(zhí)行該測試用例,日志輸出如下:
????Opening?JDBC?Connection
????Created?connection?1325808650.
????Setting?autocommit?to?false?on?JDBC?Connection?[com.mysql.jdbc.JDBC4Connection@4f063c0a]
????==>??Preparing:?select?*?from?blog?where?bid?=???
????==>?Parameters:?1(Integer)
????<==????Columns:?bid,?name,?author_id,?type
????<==????????Row:?1,?RabbitMQ延時消息,?1001,?0
????getNullableResult---1NORMAL
????<==??????Total:?1
????Blog(bid=1,?name=RabbitMQ延時消息,?authorId=1001,?blogType=0)
我們就通過這個ById查詢的案例,對Mybatis運行的過程抽絲剝繭,還原出一個完整的脈絡(luò)。
二、一圖總覽全局
?按照慣例我們用一張簡單概括的流程圖引領(lǐng)全局,先建立一個宏觀的印象。
?

基本脈絡(luò).png
從圖中可以看到,Mybatis主要的工作流程分為以下幾步:
加載并解析配置文件 獲取SqlSession對象作為與數(shù)據(jù)庫交互的接口 通過Executor對象封裝數(shù)據(jù)庫操作,執(zhí)行SQL操作 調(diào)用底層的JDBC接口,與數(shù)據(jù)庫進行真正的交互 向數(shù)據(jù)庫提交參數(shù),并封裝返回參數(shù)
加載并解析配置文件
在Mybatis啟動的時候會去加載配置文件,一般來說文件包含全局配置文件(文件名為 「mybatis-config.xml」) ,以及映射器配置文件(也就是各種Mapper.xml文件);
獲取SqlSession對象作為與數(shù)據(jù)庫交互的接口
Mybatis在加載完配置文件之后,會去獲取SqlSession對象,這個對象是應(yīng)用程序與數(shù)據(jù)庫之間的橋梁,封裝了程序與數(shù)據(jù)庫之間的連接。
一般來說,一個SqlSession對象中包含了一個Connection,我們都知道Connection是線程不安全的,因此導致SqlSession對象也是線程不安全的。因此如果將SqlSession作為成員變量使用,存在風險。(應(yīng)當使用SqlSessionTemplate,這部分后面再說)。
?注意:SqlSession是提供給應(yīng)用層的一個訪問數(shù)據(jù)庫的接口,它并不是真正的SQL執(zhí)行者,它內(nèi)部封裝了JDBC核心對象,如Statement,ResultSet等。
?
通過SqlSessionFactory獲取SqlSession會話
如果要獲取一個SqlSession會話,就需要有會話工廠,即:SqlSessionFactory。它包含了所有的配置信息,而Factory又是通過Builder創(chuàng)建的,這部分后文代碼分析中會說。
通過Executor對象封裝數(shù)據(jù)庫操作,執(zhí)行SQL操作
SqlSession持有Executor對象,Executor在執(zhí)行query、update、insert等操作時,會創(chuàng)建一系列的對象處理參數(shù)、處理結(jié)果集,核心的對象是StatementHandler,它本質(zhì)上是對Statement的封裝。
三、走進源碼,一探究竟
3.1 SqlSessionFactory的創(chuàng)建
?首先是SqlSession的創(chuàng)建過程;SqlSession需要通過SqlSessionFactory創(chuàng)建,而SqlSessionFactory又是通過SqlSessionFactoryBuilder創(chuàng)建的。
?
????#?org.apache.ibatis.session.SqlSessionFactoryBuilder#build
????public?SqlSessionFactory?build(InputStream?inputStream)?{
??????return?build(inputStream,?null,?null);
????}
事實上,inputStream就是配置文件的文件輸入流,它傳給了SqlSessionFactoryBuilder的build重載方法,我們看一下這個方法的實現(xiàn)。
????public?SqlSessionFactory?build(InputStream?inputStream,?String?environment,?Properties?properties)?{
??????try?{
????????//?用于解析?mybatis-config.xml,同時創(chuàng)建了?Configuration?對象?>>
????????XMLConfigBuilder?parser?=?new?XMLConfigBuilder(inputStream,?environment,?properties);
????????//?解析XML,最終返回一個?DefaultSqlSessionFactory?>>
????????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.
????????}
??????}
????}
可以看到,SqlSessionFactoryBuilder底層是通過xml解析方式,對配置文件進行解析,并基于解析的結(jié)果構(gòu)建了SqlSessionFactory的實例,這里返回的是默認的SqlSessionFactory--->DefaultSqlSessionFactory。
????public?SqlSessionFactory?build(Configuration?config)?{
??????return?new?DefaultSqlSessionFactory(config);
????}
「注意:此處就已經(jīng)通過配置文件解析出了Configuration,并通過DefaultSqlSessionFactory構(gòu)造方法創(chuàng)建了DefaultSqlSessionFactory實例。后文要用!」
?xml解析過程,感興趣的讀者可以自行研究,簡單的說無非就是對xml文件的dom節(jié)點進行讀取和匹配,獲取屬性加載到內(nèi)存,Mybatis自己基于javax的xml操作api封裝了一個工具類,「org.apache.ibatis.parsing.XPathParser」 。
?
3.2 SqlSession的創(chuàng)建
在使用的demo中,我們通過SqlSessionFactory獲取到一個SqlSession實例。
????SqlSession?session?=?sqlSessionFactory.openSession();
進入 openSession 方法一探究竟。
????#?org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSession()
????public?SqlSession?openSession()?{
??????return?openSessionFromDataSource(configuration.getDefaultExecutorType(),?null,?false);
????}
繼續(xù)進入 openSessionFromDataSource 方法:
????private?SqlSession?openSessionFromDataSource(ExecutorType?execType,?TransactionIsolationLevel?level,?boolean?autoCommit)?{
??????Transaction?tx?=?null;
??????try?{
????????final?Environment?environment?=?configuration.getEnvironment();
????????//?獲取事務(wù)工廠
????????final?TransactionFactory?transactionFactory?=?getTransactionFactoryFromEnvironment(environment);
????????//?創(chuàng)建事務(wù)
????????tx?=?transactionFactory.newTransaction(environment.getDataSource(),?level,?autoCommit);
????????//?根據(jù)事務(wù)工廠和默認的執(zhí)行器類型,創(chuàng)建執(zhí)行器?>>
????????final?Executor?executor?=?configuration.newExecutor(tx,?execType);
????????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();
??????}
????}
這里的邏輯比較核心,主要做了幾件事:
獲取到事務(wù)工廠; 通過事務(wù)工廠創(chuàng)建了事務(wù),如果是使用Spring框架,則由Spring框架開啟事務(wù); 根據(jù)事務(wù)工廠和默認的執(zhí)行器類型,創(chuàng)建執(zhí)行器
最后通過DefaultSqlSession的構(gòu)造方法,創(chuàng)建出DefaultSqlSession實例,它是SqlSession接口的默認實現(xiàn)。
到此,我們就持有了一個SqlSession對象,并且它還持有了一個Executor執(zhí)行器實例。
代理Mapper對象,執(zhí)行SQL
回到我們的demo代碼中:
????@Test
????public?void?testSelect()?throws?IOException?{
????????SqlSession?session?=?sqlSessionFactory.openSession();?//?ExecutorType.BATCH
????????try?{
????????????//?重點看這行代碼
????????????BlogMapper?mapper?=?session.getMapper(BlogMapper.class);
????????????Blog?blog?=?mapper.selectBlogById(1);
????????????System.out.println(blog);
????????}?finally?{
????????????session.close();
????????}
????}
我們已經(jīng)拿到了SqlSession,接著通過 「session.getMapper(BlogMapper.class)」; 獲取到了BlogMapper接口的實現(xiàn)類。
注意,我說的并不是獲取到了BlogMapper,因為大家使用過Mybatis框架都知道BlogMapper是個接口,那么此處拿到的,必然是BlogMapper的實例。
接口的實例,嗯,有點意思了,我們明明只寫了個接口,并沒有實現(xiàn)這個接口???
是不是想到了什么?對,就是動態(tài)代理。
此處獲取到的Mapper實例,就是Mybatis框架幫我們創(chuàng)建出的代理對象。
?進入 DefaultSqlSession#getMapper 方法
?
????@Override
????public??T?getMapper(Class?type)?{
??????return?configuration.getMapper(type,?this);
????}
ok,繼續(xù)往下看:
????public??T?getMapper(Class?type,?SqlSession?sqlSession)?{
??????return?mapperRegistry.getMapper(type,?sqlSession);
????}
這里,我們發(fā)現(xiàn)Mapper對象是通過 mapperRegistry 這個所謂的Mapper注冊中心中獲取到的,它的數(shù)據(jù)結(jié)構(gòu)是一個HashMap:
????#?org.apache.ibatis.session.Configuration
????protected?final?MapperRegistry?mapperRegistry?=?new?MapperRegistry(this);
????#?org.apache.ibatis.binding.MapperRegistry
????public?class?MapperRegistry?{
??????private?final?Configuration?config;
??????private?final?Map,?MapperProxyFactory>>?knownMappers?=?new?HashMap<>();
既然我們能夠通過Mapper接口類型get到接口的代理類,那它是多會兒put到Map里的?
仔細想一下應(yīng)當能夠想到,我們此時已經(jīng)是在sql的執(zhí)行期了,在這之前必然是配置文件的解析期間執(zhí)行的put操作。具體代碼如下:
????/**
????*?org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement
????*?Mapper解析
????*?@param?parent
????*?@throws?Exception
????*/
????private?void?mapperElement(XNode?parent)?throws?Exception?{
??????if?(parent?!=?null)?{
????????for?(XNode?child?:?parent.getChildren())?{
??????????//?不同的定義方式的掃描,最終都是調(diào)用?addMapper()方法
??????????//?(添加到 MapperRegistry)。這個方法和 getMapper()?對應(yīng)
??????????//?package?包
??????????if?("package".equals(child.getName()))?{
????????????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)?{
??????????????//?resource?相對路徑
??????????????ErrorContext.instance().resource(resource);
??????????????InputStream?inputStream?=?Resources.getResourceAsStream(resource);
??????????????XMLMapperBuilder?mapperParser?=?new?XMLMapperBuilder(inputStream,?configuration,?resource,?configuration.getSqlFragments());
??????????????//?解析?Mapper.xml,總體上做了兩件事情?>>
??????????????mapperParser.parse();
????????????}?else?if?(resource?==?null?&&?url?!=?null?&&?mapperClass?==?null)?{
??????????????//?url?絕對路徑
??????????????ErrorContext.instance().resource(url);
??????????????InputStream?inputStream?=?Resources.getUrlAsStream(url);
??????????????XMLMapperBuilder?mapperParser?=?new?XMLMapperBuilder(inputStream,?configuration,?url,?configuration.getSqlFragments());
??????????????mapperParser.parse();
????????????}?else?if?(resource?==?null?&&?url?==?null?&&?mapperClass?!=?null)?{
??????????????//?class??單個接口
??????????????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.");
????????????}
??????????}
????????}
??????}
????}
通過這段代碼我們可以看到,無論是通過指定掃描包路徑,還是resources相對路徑,或者url絕對路徑,或者單個Mapper添加的方式,Mybatis本質(zhì)上都是通過 「addMapper()方法添加到 MapperRegistry」。
?繼續(xù)回到Mapper代理對象創(chuàng)建過程中來。
?
????#?org.apache.ibatis.session.Configuration#getMapper
????public??T?getMapper(Class?type,?SqlSession?sqlSession)?{
??????return?mapperRegistry.getMapper(type,?sqlSession);
????}
繼續(xù)看mapperRegistry.getMapper方法邏輯。
????public??T?getMapper(Class?type,?SqlSession?sqlSession)?{
??????final?MapperProxyFactory?mapperProxyFactory?=?(MapperProxyFactory)?knownMappers.get(type);
??????if?(mapperProxyFactory?==?null)?{
????????throw?new?BindingException("Type?"?+?type?+?"?is?not?known?to?the?MapperRegistry.");
??????}
??????try?{
????????return?mapperProxyFactory.newInstance(sqlSession);
??????}?catch?(Exception?e)?{
????????throw?new?BindingException("Error?getting?mapper?instance.?Cause:?"?+?e,?e);
??????}
????}
我們發(fā)現(xiàn),通過接口類型從HashMap中取到了一個 「MapperProxyFactory」 Mapper代理工廠的實例。
?MapperProxyFactory實際上是對Mapper接口的包裝,我們只需要看源碼就知道了。
?
????public?class?MapperProxyFactory?{
??????private?final?Class?mapperInterface;
??????private?final?Map?methodCache?=?new?ConcurrentHashMap<>();
?構(gòu)造方法接受一個Mapper的class類型,對其進行封裝。
?
??????public?MapperProxyFactory(Class?mapperInterface)?{
????????this.mapperInterface?=?mapperInterface;
??????}
獲取到MapperProxyFactory實例之后,通過 「mapperProxyFactory.newInstance(sqlSession)」 就創(chuàng)建出了Mapper的代理對象。
??????public?T?newInstance(SqlSession?sqlSession)?{
????????final?MapperProxy?mapperProxy?=?new?MapperProxy<>(sqlSession,?mapperInterface,?methodCache);
????????return?newInstance(mapperProxy);
??????}
這里通過SqlSession、Mapper接口、方法緩存(「簡單的說就是Mapper的那一堆方法,每次反射創(chuàng)建太耗費性能了,就緩存到一個Map里」)創(chuàng)建出MapperProxy 對象,進一步調(diào)用的如下方法:
??????@SuppressWarnings("unchecked")
??????protected?T?newInstance(MapperProxy?mapperProxy)?{
????????// 1:類加載器:2:被代理類實現(xiàn)的接口、3:實現(xiàn)了 InvocationHandler 的觸發(fā)管理類
????????return?(T)?Proxy.newProxyInstance(mapperInterface.getClassLoader(),?new?Class[]?{?mapperInterface?},?mapperProxy);
??????}
這里把創(chuàng)建代理對象的操作委托給了MapperProxy,「我們發(fā)現(xiàn),它的核心就是創(chuàng)建代理Mapper的代理對象 (h對象)?!?/strong>
MapperProxy具體是如何創(chuàng)建的Mapper代理?
我們都知道,動態(tài)代理在JDK中是通過實現(xiàn)InvocationHandler接口實現(xiàn)的,那么大膽猜想MapperProxy必然實現(xiàn)了InvocationHandler接口。
????public?class?MapperProxy?implements?InvocationHandler,?Serializable?{
果然如此。
我們來看它的invoke方法實現(xiàn):
????@Override
????public?Object?invoke(Object?proxy,?Method?method,?Object[]?args)?throws?Throwable?{
??????try?{
????????//?toString?hashCode?equals?getClass等方法,無需走到執(zhí)行SQL的流程
????????if?(Object.class.equals(method.getDeclaringClass()))?{
??????????return?method.invoke(this,?args);
????????}?else?{
??????????//?提升獲取?mapperMethod?的效率,到?MapperMethodInvoker(內(nèi)部接口)?的?invoke
??????????//?普通方法會走到?PlainMethodInvoker(內(nèi)部類)?的?invoke
??????????return?cachedInvoker(method).invoke(proxy,?method,?args,?sqlSession);
????????}
??????}?catch?(Throwable?t)?{
????????throw?ExceptionUtil.unwrapThrowable(t);
??????}
????}
可以看到,如果是普通方法,直接執(zhí)行,不需要特殊處理;
否則就獲取匹配的緩存Mapper方法,執(zhí)行數(shù)據(jù)庫操作。
?重點看一下 ?cachedInvoker(method).invoke(proxy, method, args, sqlSession); 邏輯。
?
????private?MapperMethodInvoker?cachedInvoker(Method?method)?throws?Throwable?{
??????try?{
????????//?Java8?中?Map?的方法,根據(jù)?key?獲取值,如果值是?null,則把后面Object?的值賦給?key
????????//?如果獲取不到,就創(chuàng)建
????????//?獲取的是?MapperMethodInvoker(接口)?對象,只有一個invoke方法
????????return?methodCache.computeIfAbsent(method,?m?->?{
??????????if?(m.isDefault())?{
????????????//?接口的默認方法(Java8),只要實現(xiàn)接口都會繼承接口的默認方法,例如?List.sort()
????????????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?{
????????????//?創(chuàng)建了一個?MapperMethod
????????????return?new?PlainMethodInvoker(new?MapperMethod(mapperInterface,?method,?sqlSession.getConfiguration()));
??????????}
????????});
??????}?catch?(RuntimeException?re)?{
????????Throwable?cause?=?re.getCause();
????????throw?cause?==?null???re?:?cause;
??????}
????}
這里針對Java8接口的默認方法做了些處理,這個地方不用特殊關(guān)注,我們重點看else邏輯:
??????//?創(chuàng)建了一個?MapperMethod
??????return?new?PlainMethodInvoker(
??????????new?MapperMethod(
????????????mapperInterface,?
????????????method,
????????????sqlSession.getConfiguration()));
Mybatis執(zhí)行sql語句的真正開端:
?上文中,我們費盡努力,獲取到了 「PlainMethodInvoker」 實例,其實到這里,才是Mybatis執(zhí)行SQL真正的起點。
?
不要慌,繼續(xù)跟上我的腳步,我們一鼓作氣往后看。
上文中,我們知道Mapper對象實際上是Mapper接口的代理對象,而且是JDK的動態(tài)代理。
當執(zhí)行Mapper的各種數(shù)據(jù)庫操作方法時,實際上是調(diào)用的代理對象的方法,也就是invoke方法。
對于Mapper方法而言,其實就是調(diào)用的PlainMethodInvoker的invoke方法。
忘了?那么我們再復(fù)習一下這部分的代碼:
??????//?org.apache.ibatis.binding.MapperProxy#invoke
??????//?普通方法會走到?PlainMethodInvoker(內(nèi)部類)?的?invoke
??????return?cachedInvoker(method).invoke(proxy,?method,?args,?sqlSession);
接著來看PlainMethodInvoker的invoke方法:
????@Override
????public?Object?invoke(
????????????????????????Object?proxy,?
????????????????????????Method?method,?
????????????????????????Object[]?args,?
????????????????????????SqlSession?sqlSession)?throws?Throwable?{
??????//?SQL執(zhí)行的真正起點
??????return?mapperMethod.execute(sqlSession,?args);
????}
實際上這里的mapperMethod就是我們Mapper接口或者說XML文件中定義的方法名了。
?接著就是重頭戲,MapperMethod#execute 方法,完整代碼我貼這兒了。
?
????//?org.apache.ibatis.binding.MapperMethod#execute
????public?Object?execute(SqlSession?sqlSession,?Object[]?args)?{
??????Object?result;
??????switch?(command.getType())?{
????????case?INSERT:?{
??????????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?{
????????????Object?param?=?method.convertArgsToSqlCommandParam(args);
????????????//?普通?select?語句的執(zhí)行入口?>>
????????????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;
????}
重點看那個switch case,不用注釋一眼看過去基本上也能看個八九不離十,這里就是通過sql的類型去執(zhí)行不同的jdbc操作。
?可以看到,熟悉的操作他來了,通過SqlSession完成一系列的數(shù)據(jù)庫操作。
?
我們的demo是一個查詢操作,那么我們就挑select來看看。
普通select語句的入口如下:
????result?=?sqlSession.selectOne(command.getName(),?param);
繼續(xù)深入:
????//?DefaultSqlSession#selectOne(java.lang.String,?java.lang.Object)
????@Override
????public??T?selectOne(String?statement,?Object?parameter)?{
??????//?來到了?DefaultSqlSession
??????//?Popular?vote?was?to?return?null?on?0?results?and?throw?exception?on?too?many.
??????List?list?=?this.selectList(statement,?parameter);
??????if?(list.size()?==?1)?{
????????return?list.get(0);
??????}?else?if?(list.size()?>?1)?{
????????throw?new?TooManyResultsException("Expected?one?result?(or?null)?to?be?returned?by?selectOne(),?but?found:?"?+?list.size());
??????}?else?{
????????return?null;
??????}
????}
可以看到是通過selectList來完成查詢多個和單個。
????@Override
????public??List?selectList(String?statement,?Object?parameter)?{
??????//?為了提供多種重載(簡化方法使用),和默認值
??????//?讓參數(shù)少的調(diào)用參數(shù)多的方法,只實現(xiàn)一次
??????return?this.selectList(statement,?parameter,?RowBounds.DEFAULT);
????}
繼續(xù)看多參重載方法:
????@Override
????public??List?selectList(String?statement,?Object?parameter,?RowBounds?rowBounds)?{
??????try?{
????????MappedStatement?ms?=?configuration.getMappedStatement(statement);
????????//?如果?cacheEnabled?=?true(默認),Executor會被?CachingExecutor裝飾
????????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();
??????}
????}
核心代碼就是executor.query,我們進去看看:
????@Override
????public??List?query(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler)?throws?SQLException?{
??????BoundSql?boundSql?=?ms.getBoundSql(parameter);
??????//?一級緩存和二級緩存的CacheKey是同一個
??????CacheKey?key?=?createCacheKey(ms,?parameter,?rowBounds,?boundSql);
??????return?query(ms,?parameter,?rowBounds,?resultHandler,?key,?boundSql);
????}
這里涉及到一級緩存和二級緩存,不是重點,我們就想看看最終是怎么執(zhí)行的jdbc操作,那么就只需要繼續(xù)看query重載。
????//?org.apache.ibatis.executor.BaseExecutor#query
????@Override
????public??List?query(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler,?CacheKey?key,?BoundSql?boundSql)?throws?SQLException?{
??????//?異常體系之?ErrorContext
??????ErrorContext.instance().resource(ms.getResource()).activity("executing?a?query").object(ms.getId());
??????if?(closed)?{
????????throw?new?ExecutorException("Executor?was?closed.");
??????}
??????if?(queryStack?==?0?&&?ms.isFlushCacheRequired())?{
????????//?flushCache="true"時,即使是查詢,也清空一級緩存
????????clearLocalCache();
??????}
??????List?list;
??????try?{
????????//?防止遞歸查詢重復(fù)處理緩存
????????queryStack++;
????????//?查詢一級緩存
????????//?ResultHandler?和?ResultSetHandler的區(qū)別
????????list?=?resultHandler?==?null???(List)?localCache.getObject(key)?:?null;
????????
????????if?(list?!=?null)?{
??????????handleLocallyCachedOutputParameters(ms,?key,?parameter,?boundSql);
????????}?else?{
??????????//?真正的查詢流程
??????????list?=?queryFromDatabase(ms,?parameter,?rowBounds,?resultHandler,?key,?boundSql);
??????????...省略N行代碼...
涉及到緩存的,通通與我無關(guān),只看真正的查詢流程 「queryFromDatabase」。
????private??List?queryFromDatabase(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler,?CacheKey?key,?BoundSql?boundSql)?throws?SQLException?{
??????List?list;
??????//?先占位
??????localCache.putObject(key,?EXECUTION_PLACEHOLDER);
??????try?{
????????//?三種?Executor?的區(qū)別,看doUpdate
????????//?默認Simple
????????list?=?doQuery(ms,?parameter,?rowBounds,?resultHandler,?boundSql);
??????}?finally?{
????????//?移除占位符
????????localCache.removeObject(key);
??????}
??????//?寫入一級緩存
??????localCache.putObject(key,?list);
??????if?(ms.getStatementType()?==?StatementType.CALLABLE)?{
????????localOutputParameterCache.putObject(key,?parameter);
??????}
??????return?list;
????}
看到j(luò)dbc了,勝利的曙光。
舒服,繼續(xù)看doQuery方法,看到resultHandler了么,結(jié)果處理器,感覺離結(jié)果更近了。
????//?org.apache.ibatis.executor.SimpleExecutor#doQuery
????@Override
????public??List?doQuery(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler,?BoundSql?boundSql)?throws?SQLException?{
??????Statement?stmt?=?null;
??????try?{
????????Configuration?configuration?=?ms.getConfiguration();
????????//?注意,已經(jīng)來到SQL處理的關(guān)鍵對象?StatementHandler?>>
????????StatementHandler?handler?=?configuration.newStatementHandler(wrapper,?ms,?parameter,?rowBounds,?resultHandler,?boundSql);
????????//?獲取一個?Statement對象
????????stmt?=?prepareStatement(handler,?ms.getStatementLog());
????????//?執(zhí)行查詢
????????return?handler.query(stmt,?resultHandler);
??????}?finally?{
????????//?用完就關(guān)閉
????????closeStatement(stmt);
??????}
????}
查詢用的Exucutor就是默認的SimpleExecutor,看到了熟悉的prepareStatement獲取流程,基本上就到底層jdbc了。那么我們就看看 「prepareStatement(handler, ms.getStatementLog());」
????private?Statement?prepareStatement(StatementHandler?handler,?Log?statementLog)?throws?SQLException?{
??????Statement?stmt;
??????Connection?connection?=?getConnection(statementLog);
??????//?獲取?Statement?對象
??????stmt?=?handler.prepare(connection,?transaction.getTimeout());
??????//?為?Statement?設(shè)置參數(shù)
??????handler.parameterize(stmt);
??????return?stmt;
????}
看到這里,就到j(luò)dbc層面了,我們看到了熟悉的Connection,獲取到connection之后再獲取Statement。
這里的Statement就是java.sql的statement接口。
?org.apache.ibatis.executor.statement.SimpleStatementHandler#query
?
????@Override
????public??List?query(Statement?statement,?ResultHandler?resultHandler)?throws?SQLException?{
??????String?sql?=?boundSql.getSql();
??????statement.execute(sql);
??????return?resultSetHandler.handleResultSets(statement);
????}
已經(jīng)獲取到了sql,通過Statement去執(zhí)行sql,再通過resultSetHandler處理結(jié)果集。
通過Statement去執(zhí)行sql
??????statement.execute(sql);
這里就已經(jīng)是jdbc層面的操作了,通過與數(shù)據(jù)庫建立的connection提交并執(zhí)行sql。
通過resultSetHandler處理結(jié)果集
?都到最后了,我們也不慌了,那么就看看org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSets是如何處理結(jié)果集的。
?
????@Override
????public?List
這么一坨代碼,只需要重點看
????handleResultSet(rsw,?resultMap,?null,?parentMapping);
????private?void?handleResultSet(ResultSetWrapper?rsw,?ResultMap?resultMap,?List?multipleResults,?ResultMapping?parentMapping)?throws?SQLException?{
??????try?{
????????if?(parentMapping?!=?null)?{
??????????handleRowValues(rsw,?resultMap,?null,?RowBounds.DEFAULT,?parentMapping);
????????}?else?{
??????????if?(resultHandler?==?null)?{
????????????DefaultResultHandler?defaultResultHandler?=?new?DefaultResultHandler(objectFactory);
????????????handleRowValues(rsw,?resultMap,?defaultResultHandler,?rowBounds,?null);
????????????multipleResults.add(defaultResultHandler.getResultList());
??????????}?else?{
????????????handleRowValues(rsw,?resultMap,?resultHandler,?rowBounds,?null);
??????????}
????????}
??????}?finally?{
????????//?issue?#228?(close?resultsets)
????????closeResultSet(rsw.getResultSet());
??????}
????}
看看handleRowValues的邏輯 (有點心累),一鼓作氣再瞅兩眼。
最終,來到了這個地方:
????//?org.apache.ibatis.executor.resultset.DefaultResultSetHandler
????//?????????#handleRowValuesForSimpleResultMap
????private?void?handleRowValuesForSimpleResultMap(ResultSetWrapper?rsw,?ResultMap?resultMap,?ResultHandler>?resultHandler,?RowBounds?rowBounds,?ResultMapping?parentMapping)
????????throws?SQLException?{
??????DefaultResultContext?resultContext?=?new?DefaultResultContext<>();
??????//?看到了吧,沒什么好說的,就是jdbc的結(jié)果集處理
??????ResultSet?resultSet?=?rsw.getResultSet();
??????skipRows(resultSet,?rowBounds);
??????while?(shouldProcessMoreRows(resultContext,?rowBounds)?&&?!resultSet.isClosed()?&&?resultSet.next())?{
????????ResultMap?discriminatedResultMap?=?resolveDiscriminatedResultMap(resultSet,?resultMap,?null);
????????Object?rowValue?=?getRowValue(rsw,?discriminatedResultMap,?null);
????????storeObject(resultHandler,?resultContext,?rowValue,?parentMapping,?resultSet);
??????}
????}
行了,不用再往下挖了,看到了熟悉的ResultSet獲取結(jié)果集的操作,Mybatis執(zhí)行sql的流程基本就結(jié)束了。
底層還是熟悉的JDBC操作。
小結(jié)
其實寫了這么多,也沒啥想總結(jié)的,我們通過一個查詢操作,完整的把Mybatis從解析文件到執(zhí)行sql,再到結(jié)果集處理都從源碼級別剖析了一遍。
那么我們回答一下開頭的問題:
我只是寫了個Mapper接口,再配合xml或者注解,把SQL一寫,就可以執(zhí)行數(shù)據(jù)庫操作,這是為何?
其實我們獲取到的Mapper對象,已經(jīng)是Mybatis幫我們生成的代理對象了,這個代理對象擁有與jdbc交互的一切必要條件。
都說Mybatis是對JDBC的封裝,可是我卻看不到JDBC相關(guān)的接口和對象,它們到哪里去了?
稍微往上翻翻,我們剛講了,實際上最底層就是封裝的jdbc的接口。
我們看不到但是用到了,并且用起來還很爽,這就是封裝的魅力啊。
為什么在Spring中使用Mybatis,不用加@Repository/@Component之類的注解,就可以隨用隨注入(如:@Autowired)?
這個問題,就放到之后的文章講解吧,那么就敬請期待下一篇:Mybatis與Spring的愛情故事(從源碼層面解析,Mybatis是如何利用Spring擴展點,實現(xiàn)與Spring整合的。)
最后,貼張圖,概括一下這個過程。圖是借來的,僅供學習討論,侵刪。
?創(chuàng)建會話工廠SqlSessionFactory
?

flow/1.png
?創(chuàng)建會話SqlSession
?

flow/2.png ?創(chuàng)建代理對象
?

flow/3.png ?調(diào)用代理對象,執(zhí)行SQL流程
?

flow/4.png 那么,不見不散。
瀏覽
66
作為一名Java后端開發(fā)者,尤其是國內(nèi)開發(fā)者,從剛參加工作開始就與Mybatis打交道了。
用了這么久的Mybatis難免會心生疑問:
我只是寫了個Mapper接口,再配合xml或者注解,把SQL一寫,就可以執(zhí)行數(shù)據(jù)庫操作,這是為何? 都說Mybatis是對JDBC的封裝,可是我卻看不到JDBC相關(guān)的接口和對象,它們到哪里去了? 為什么在Spring中使用Mybatis,不用加@Repository/@Component之類的注解,就可以隨用隨注入(如:@Autowired)?
?硬核萬字長文,點個在看,轉(zhuǎn)發(fā),多謝啦~
?
隨著工作經(jīng)驗越多,對這些問題的疑惑就會越發(fā)強烈。而讀源碼是解決這些疑問的根本方法。
那么就跟隨筆者的腳步,試著用一篇文章,以一個查詢?yōu)槔?,從源碼角度一步一步揭開Mybatis的神秘面紗。
一、先看一個demo
????private?SqlSessionFactory?sqlSessionFactory;
????@Before
????public?void?prepare()?throws?IOException?{
????????String?resource?=?"mybatis-config.xml";
????????InputStream?inputStream?=?Resources.getResourceAsStream(resource);
????????sqlSessionFactory?=?new?SqlSessionFactoryBuilder().build(inputStream);
????}
????/**
?????*?通過?SqlSession.getMapper(XXXMapper.class)??接口方式
?????*?@throws?IOException
?????*/
????@Test
????public?void?testSelect()?throws?IOException?{
????????SqlSession?session?=?sqlSessionFactory.openSession();?//?ExecutorType.BATCH
????????try?{
????????????BlogMapper?mapper?=?session.getMapper(BlogMapper.class);
????????????Blog?blog?=?mapper.selectBlogById(1);
????????????System.out.println(blog);
????????}?finally?{
????????????session.close();
????????}
????}
這是一個非Spring項目的Test用例類,邏輯很直觀,就是在測試通過id查詢一行記錄;在執(zhí)行查詢之間加載配置文件。
執(zhí)行該測試用例,日志輸出如下:
????Opening?JDBC?Connection
????Created?connection?1325808650.
????Setting?autocommit?to?false?on?JDBC?Connection?[com.mysql.jdbc.JDBC4Connection@4f063c0a]
????==>??Preparing:?select?*?from?blog?where?bid?=???
????==>?Parameters:?1(Integer)
????<==????Columns:?bid,?name,?author_id,?type
????<==????????Row:?1,?RabbitMQ延時消息,?1001,?0
????getNullableResult---1NORMAL
????<==??????Total:?1
????Blog(bid=1,?name=RabbitMQ延時消息,?authorId=1001,?blogType=0)
我們就通過這個ById查詢的案例,對Mybatis運行的過程抽絲剝繭,還原出一個完整的脈絡(luò)。
二、一圖總覽全局
?按照慣例我們用一張簡單概括的流程圖引領(lǐng)全局,先建立一個宏觀的印象。
?

從圖中可以看到,Mybatis主要的工作流程分為以下幾步:
加載并解析配置文件 獲取SqlSession對象作為與數(shù)據(jù)庫交互的接口 通過Executor對象封裝數(shù)據(jù)庫操作,執(zhí)行SQL操作 調(diào)用底層的JDBC接口,與數(shù)據(jù)庫進行真正的交互 向數(shù)據(jù)庫提交參數(shù),并封裝返回參數(shù)
加載并解析配置文件
在Mybatis啟動的時候會去加載配置文件,一般來說文件包含全局配置文件(文件名為 「mybatis-config.xml」) ,以及映射器配置文件(也就是各種Mapper.xml文件);
獲取SqlSession對象作為與數(shù)據(jù)庫交互的接口
Mybatis在加載完配置文件之后,會去獲取SqlSession對象,這個對象是應(yīng)用程序與數(shù)據(jù)庫之間的橋梁,封裝了程序與數(shù)據(jù)庫之間的連接。
一般來說,一個SqlSession對象中包含了一個Connection,我們都知道Connection是線程不安全的,因此導致SqlSession對象也是線程不安全的。因此如果將SqlSession作為成員變量使用,存在風險。(應(yīng)當使用SqlSessionTemplate,這部分后面再說)。
?注意:SqlSession是提供給應(yīng)用層的一個訪問數(shù)據(jù)庫的接口,它并不是真正的SQL執(zhí)行者,它內(nèi)部封裝了JDBC核心對象,如Statement,ResultSet等。
?
通過SqlSessionFactory獲取SqlSession會話
如果要獲取一個SqlSession會話,就需要有會話工廠,即:SqlSessionFactory。它包含了所有的配置信息,而Factory又是通過Builder創(chuàng)建的,這部分后文代碼分析中會說。
通過Executor對象封裝數(shù)據(jù)庫操作,執(zhí)行SQL操作
SqlSession持有Executor對象,Executor在執(zhí)行query、update、insert等操作時,會創(chuàng)建一系列的對象處理參數(shù)、處理結(jié)果集,核心的對象是StatementHandler,它本質(zhì)上是對Statement的封裝。
三、走進源碼,一探究竟
3.1 SqlSessionFactory的創(chuàng)建
?首先是SqlSession的創(chuàng)建過程;SqlSession需要通過SqlSessionFactory創(chuàng)建,而SqlSessionFactory又是通過SqlSessionFactoryBuilder創(chuàng)建的。
?
????#?org.apache.ibatis.session.SqlSessionFactoryBuilder#build
????public?SqlSessionFactory?build(InputStream?inputStream)?{
??????return?build(inputStream,?null,?null);
????}
事實上,inputStream就是配置文件的文件輸入流,它傳給了SqlSessionFactoryBuilder的build重載方法,我們看一下這個方法的實現(xiàn)。
????public?SqlSessionFactory?build(InputStream?inputStream,?String?environment,?Properties?properties)?{
??????try?{
????????//?用于解析?mybatis-config.xml,同時創(chuàng)建了?Configuration?對象?>>
????????XMLConfigBuilder?parser?=?new?XMLConfigBuilder(inputStream,?environment,?properties);
????????//?解析XML,最終返回一個?DefaultSqlSessionFactory?>>
????????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.
????????}
??????}
????}
可以看到,SqlSessionFactoryBuilder底層是通過xml解析方式,對配置文件進行解析,并基于解析的結(jié)果構(gòu)建了SqlSessionFactory的實例,這里返回的是默認的SqlSessionFactory--->DefaultSqlSessionFactory。
????public?SqlSessionFactory?build(Configuration?config)?{
??????return?new?DefaultSqlSessionFactory(config);
????}
「注意:此處就已經(jīng)通過配置文件解析出了Configuration,并通過DefaultSqlSessionFactory構(gòu)造方法創(chuàng)建了DefaultSqlSessionFactory實例。后文要用!」
?xml解析過程,感興趣的讀者可以自行研究,簡單的說無非就是對xml文件的dom節(jié)點進行讀取和匹配,獲取屬性加載到內(nèi)存,Mybatis自己基于javax的xml操作api封裝了一個工具類,「org.apache.ibatis.parsing.XPathParser」 。
?
3.2 SqlSession的創(chuàng)建
在使用的demo中,我們通過SqlSessionFactory獲取到一個SqlSession實例。
????SqlSession?session?=?sqlSessionFactory.openSession();
進入 openSession 方法一探究竟。
????#?org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSession()
????public?SqlSession?openSession()?{
??????return?openSessionFromDataSource(configuration.getDefaultExecutorType(),?null,?false);
????}
繼續(xù)進入 openSessionFromDataSource 方法:
????private?SqlSession?openSessionFromDataSource(ExecutorType?execType,?TransactionIsolationLevel?level,?boolean?autoCommit)?{
??????Transaction?tx?=?null;
??????try?{
????????final?Environment?environment?=?configuration.getEnvironment();
????????//?獲取事務(wù)工廠
????????final?TransactionFactory?transactionFactory?=?getTransactionFactoryFromEnvironment(environment);
????????//?創(chuàng)建事務(wù)
????????tx?=?transactionFactory.newTransaction(environment.getDataSource(),?level,?autoCommit);
????????//?根據(jù)事務(wù)工廠和默認的執(zhí)行器類型,創(chuàng)建執(zhí)行器?>>
????????final?Executor?executor?=?configuration.newExecutor(tx,?execType);
????????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();
??????}
????}
這里的邏輯比較核心,主要做了幾件事:
獲取到事務(wù)工廠; 通過事務(wù)工廠創(chuàng)建了事務(wù),如果是使用Spring框架,則由Spring框架開啟事務(wù); 根據(jù)事務(wù)工廠和默認的執(zhí)行器類型,創(chuàng)建執(zhí)行器
最后通過DefaultSqlSession的構(gòu)造方法,創(chuàng)建出DefaultSqlSession實例,它是SqlSession接口的默認實現(xiàn)。
到此,我們就持有了一個SqlSession對象,并且它還持有了一個Executor執(zhí)行器實例。
代理Mapper對象,執(zhí)行SQL
回到我們的demo代碼中:
????@Test
????public?void?testSelect()?throws?IOException?{
????????SqlSession?session?=?sqlSessionFactory.openSession();?//?ExecutorType.BATCH
????????try?{
????????????//?重點看這行代碼
????????????BlogMapper?mapper?=?session.getMapper(BlogMapper.class);
????????????Blog?blog?=?mapper.selectBlogById(1);
????????????System.out.println(blog);
????????}?finally?{
????????????session.close();
????????}
????}
我們已經(jīng)拿到了SqlSession,接著通過 「session.getMapper(BlogMapper.class)」; 獲取到了BlogMapper接口的實現(xiàn)類。
注意,我說的并不是獲取到了BlogMapper,因為大家使用過Mybatis框架都知道BlogMapper是個接口,那么此處拿到的,必然是BlogMapper的實例。
接口的實例,嗯,有點意思了,我們明明只寫了個接口,并沒有實現(xiàn)這個接口???
是不是想到了什么?對,就是動態(tài)代理。
此處獲取到的Mapper實例,就是Mybatis框架幫我們創(chuàng)建出的代理對象。
?進入 DefaultSqlSession#getMapper 方法
?
????@Override
????public??T?getMapper(Class?type)?{
??????return?configuration.getMapper(type,?this);
????}
ok,繼續(xù)往下看:
????public??T?getMapper(Class?type,?SqlSession?sqlSession)?{
??????return?mapperRegistry.getMapper(type,?sqlSession);
????}
這里,我們發(fā)現(xiàn)Mapper對象是通過 mapperRegistry 這個所謂的Mapper注冊中心中獲取到的,它的數(shù)據(jù)結(jié)構(gòu)是一個HashMap:
????#?org.apache.ibatis.session.Configuration
????protected?final?MapperRegistry?mapperRegistry?=?new?MapperRegistry(this);
????#?org.apache.ibatis.binding.MapperRegistry
????public?class?MapperRegistry?{
??????private?final?Configuration?config;
??????private?final?Map,?MapperProxyFactory>>?knownMappers?=?new?HashMap<>();
既然我們能夠通過Mapper接口類型get到接口的代理類,那它是多會兒put到Map里的?
仔細想一下應(yīng)當能夠想到,我們此時已經(jīng)是在sql的執(zhí)行期了,在這之前必然是配置文件的解析期間執(zhí)行的put操作。具體代碼如下:
????/**
????*?org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement
????*?Mapper解析
????*?@param?parent
????*?@throws?Exception
????*/
????private?void?mapperElement(XNode?parent)?throws?Exception?{
??????if?(parent?!=?null)?{
????????for?(XNode?child?:?parent.getChildren())?{
??????????//?不同的定義方式的掃描,最終都是調(diào)用?addMapper()方法
??????????//?(添加到 MapperRegistry)。這個方法和 getMapper()?對應(yīng)
??????????//?package?包
??????????if?("package".equals(child.getName()))?{
????????????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)?{
??????????????//?resource?相對路徑
??????????????ErrorContext.instance().resource(resource);
??????????????InputStream?inputStream?=?Resources.getResourceAsStream(resource);
??????????????XMLMapperBuilder?mapperParser?=?new?XMLMapperBuilder(inputStream,?configuration,?resource,?configuration.getSqlFragments());
??????????????//?解析?Mapper.xml,總體上做了兩件事情?>>
??????????????mapperParser.parse();
????????????}?else?if?(resource?==?null?&&?url?!=?null?&&?mapperClass?==?null)?{
??????????????//?url?絕對路徑
??????????????ErrorContext.instance().resource(url);
??????????????InputStream?inputStream?=?Resources.getUrlAsStream(url);
??????????????XMLMapperBuilder?mapperParser?=?new?XMLMapperBuilder(inputStream,?configuration,?url,?configuration.getSqlFragments());
??????????????mapperParser.parse();
????????????}?else?if?(resource?==?null?&&?url?==?null?&&?mapperClass?!=?null)?{
??????????????//?class??單個接口
??????????????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.");
????????????}
??????????}
????????}
??????}
????}
通過這段代碼我們可以看到,無論是通過指定掃描包路徑,還是resources相對路徑,或者url絕對路徑,或者單個Mapper添加的方式,Mybatis本質(zhì)上都是通過 「addMapper()方法添加到 MapperRegistry」。
?繼續(xù)回到Mapper代理對象創(chuàng)建過程中來。
?
????#?org.apache.ibatis.session.Configuration#getMapper
????public??T?getMapper(Class?type,?SqlSession?sqlSession)?{
??????return?mapperRegistry.getMapper(type,?sqlSession);
????}
繼續(xù)看mapperRegistry.getMapper方法邏輯。
????public??T?getMapper(Class?type,?SqlSession?sqlSession)?{
??????final?MapperProxyFactory?mapperProxyFactory?=?(MapperProxyFactory)?knownMappers.get(type);
??????if?(mapperProxyFactory?==?null)?{
????????throw?new?BindingException("Type?"?+?type?+?"?is?not?known?to?the?MapperRegistry.");
??????}
??????try?{
????????return?mapperProxyFactory.newInstance(sqlSession);
??????}?catch?(Exception?e)?{
????????throw?new?BindingException("Error?getting?mapper?instance.?Cause:?"?+?e,?e);
??????}
????}
我們發(fā)現(xiàn),通過接口類型從HashMap中取到了一個 「MapperProxyFactory」 Mapper代理工廠的實例。
?MapperProxyFactory實際上是對Mapper接口的包裝,我們只需要看源碼就知道了。
?
????public?class?MapperProxyFactory?{
??????private?final?Class?mapperInterface;
??????private?final?Map?methodCache?=?new?ConcurrentHashMap<>();
?構(gòu)造方法接受一個Mapper的class類型,對其進行封裝。
?
??????public?MapperProxyFactory(Class?mapperInterface)?{
????????this.mapperInterface?=?mapperInterface;
??????}
獲取到MapperProxyFactory實例之后,通過 「mapperProxyFactory.newInstance(sqlSession)」 就創(chuàng)建出了Mapper的代理對象。
??????public?T?newInstance(SqlSession?sqlSession)?{
????????final?MapperProxy?mapperProxy?=?new?MapperProxy<>(sqlSession,?mapperInterface,?methodCache);
????????return?newInstance(mapperProxy);
??????}
這里通過SqlSession、Mapper接口、方法緩存(「簡單的說就是Mapper的那一堆方法,每次反射創(chuàng)建太耗費性能了,就緩存到一個Map里」)創(chuàng)建出MapperProxy 對象,進一步調(diào)用的如下方法:
??????@SuppressWarnings("unchecked")
??????protected?T?newInstance(MapperProxy?mapperProxy)?{
????????// 1:類加載器:2:被代理類實現(xiàn)的接口、3:實現(xiàn)了 InvocationHandler 的觸發(fā)管理類
????????return?(T)?Proxy.newProxyInstance(mapperInterface.getClassLoader(),?new?Class[]?{?mapperInterface?},?mapperProxy);
??????}
這里把創(chuàng)建代理對象的操作委托給了MapperProxy,「我們發(fā)現(xiàn),它的核心就是創(chuàng)建代理Mapper的代理對象 (h對象)?!?/strong>
MapperProxy具體是如何創(chuàng)建的Mapper代理?
我們都知道,動態(tài)代理在JDK中是通過實現(xiàn)InvocationHandler接口實現(xiàn)的,那么大膽猜想MapperProxy必然實現(xiàn)了InvocationHandler接口。
????public?class?MapperProxy?implements?InvocationHandler,?Serializable?{
果然如此。
我們來看它的invoke方法實現(xiàn):
????@Override
????public?Object?invoke(Object?proxy,?Method?method,?Object[]?args)?throws?Throwable?{
??????try?{
????????//?toString?hashCode?equals?getClass等方法,無需走到執(zhí)行SQL的流程
????????if?(Object.class.equals(method.getDeclaringClass()))?{
??????????return?method.invoke(this,?args);
????????}?else?{
??????????//?提升獲取?mapperMethod?的效率,到?MapperMethodInvoker(內(nèi)部接口)?的?invoke
??????????//?普通方法會走到?PlainMethodInvoker(內(nèi)部類)?的?invoke
??????????return?cachedInvoker(method).invoke(proxy,?method,?args,?sqlSession);
????????}
??????}?catch?(Throwable?t)?{
????????throw?ExceptionUtil.unwrapThrowable(t);
??????}
????}
可以看到,如果是普通方法,直接執(zhí)行,不需要特殊處理;
否則就獲取匹配的緩存Mapper方法,執(zhí)行數(shù)據(jù)庫操作。
?重點看一下 ?cachedInvoker(method).invoke(proxy, method, args, sqlSession); 邏輯。
?
????private?MapperMethodInvoker?cachedInvoker(Method?method)?throws?Throwable?{
??????try?{
????????//?Java8?中?Map?的方法,根據(jù)?key?獲取值,如果值是?null,則把后面Object?的值賦給?key
????????//?如果獲取不到,就創(chuàng)建
????????//?獲取的是?MapperMethodInvoker(接口)?對象,只有一個invoke方法
????????return?methodCache.computeIfAbsent(method,?m?->?{
??????????if?(m.isDefault())?{
????????????//?接口的默認方法(Java8),只要實現(xiàn)接口都會繼承接口的默認方法,例如?List.sort()
????????????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?{
????????????//?創(chuàng)建了一個?MapperMethod
????????????return?new?PlainMethodInvoker(new?MapperMethod(mapperInterface,?method,?sqlSession.getConfiguration()));
??????????}
????????});
??????}?catch?(RuntimeException?re)?{
????????Throwable?cause?=?re.getCause();
????????throw?cause?==?null???re?:?cause;
??????}
????}
這里針對Java8接口的默認方法做了些處理,這個地方不用特殊關(guān)注,我們重點看else邏輯:
??????//?創(chuàng)建了一個?MapperMethod
??????return?new?PlainMethodInvoker(
??????????new?MapperMethod(
????????????mapperInterface,?
????????????method,
????????????sqlSession.getConfiguration()));
Mybatis執(zhí)行sql語句的真正開端:
?上文中,我們費盡努力,獲取到了 「PlainMethodInvoker」 實例,其實到這里,才是Mybatis執(zhí)行SQL真正的起點。
?
不要慌,繼續(xù)跟上我的腳步,我們一鼓作氣往后看。
上文中,我們知道Mapper對象實際上是Mapper接口的代理對象,而且是JDK的動態(tài)代理。
當執(zhí)行Mapper的各種數(shù)據(jù)庫操作方法時,實際上是調(diào)用的代理對象的方法,也就是invoke方法。
對于Mapper方法而言,其實就是調(diào)用的PlainMethodInvoker的invoke方法。
忘了?那么我們再復(fù)習一下這部分的代碼:
??????//?org.apache.ibatis.binding.MapperProxy#invoke
??????//?普通方法會走到?PlainMethodInvoker(內(nèi)部類)?的?invoke
??????return?cachedInvoker(method).invoke(proxy,?method,?args,?sqlSession);
接著來看PlainMethodInvoker的invoke方法:
????@Override
????public?Object?invoke(
????????????????????????Object?proxy,?
????????????????????????Method?method,?
????????????????????????Object[]?args,?
????????????????????????SqlSession?sqlSession)?throws?Throwable?{
??????//?SQL執(zhí)行的真正起點
??????return?mapperMethod.execute(sqlSession,?args);
????}
實際上這里的mapperMethod就是我們Mapper接口或者說XML文件中定義的方法名了。
?接著就是重頭戲,MapperMethod#execute 方法,完整代碼我貼這兒了。
?
????//?org.apache.ibatis.binding.MapperMethod#execute
????public?Object?execute(SqlSession?sqlSession,?Object[]?args)?{
??????Object?result;
??????switch?(command.getType())?{
????????case?INSERT:?{
??????????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?{
????????????Object?param?=?method.convertArgsToSqlCommandParam(args);
????????????//?普通?select?語句的執(zhí)行入口?>>
????????????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;
????}
重點看那個switch case,不用注釋一眼看過去基本上也能看個八九不離十,這里就是通過sql的類型去執(zhí)行不同的jdbc操作。
?可以看到,熟悉的操作他來了,通過SqlSession完成一系列的數(shù)據(jù)庫操作。
?
我們的demo是一個查詢操作,那么我們就挑select來看看。
普通select語句的入口如下:
????result?=?sqlSession.selectOne(command.getName(),?param);
繼續(xù)深入:
????//?DefaultSqlSession#selectOne(java.lang.String,?java.lang.Object)
????@Override
????public??T?selectOne(String?statement,?Object?parameter)?{
??????//?來到了?DefaultSqlSession
??????//?Popular?vote?was?to?return?null?on?0?results?and?throw?exception?on?too?many.
??????List?list?=?this.selectList(statement,?parameter);
??????if?(list.size()?==?1)?{
????????return?list.get(0);
??????}?else?if?(list.size()?>?1)?{
????????throw?new?TooManyResultsException("Expected?one?result?(or?null)?to?be?returned?by?selectOne(),?but?found:?"?+?list.size());
??????}?else?{
????????return?null;
??????}
????}
可以看到是通過selectList來完成查詢多個和單個。
????@Override
????public??List?selectList(String?statement,?Object?parameter)?{
??????//?為了提供多種重載(簡化方法使用),和默認值
??????//?讓參數(shù)少的調(diào)用參數(shù)多的方法,只實現(xiàn)一次
??????return?this.selectList(statement,?parameter,?RowBounds.DEFAULT);
????}
繼續(xù)看多參重載方法:
????@Override
????public??List?selectList(String?statement,?Object?parameter,?RowBounds?rowBounds)?{
??????try?{
????????MappedStatement?ms?=?configuration.getMappedStatement(statement);
????????//?如果?cacheEnabled?=?true(默認),Executor會被?CachingExecutor裝飾
????????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();
??????}
????}
核心代碼就是executor.query,我們進去看看:
????@Override
????public??List?query(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler)?throws?SQLException?{
??????BoundSql?boundSql?=?ms.getBoundSql(parameter);
??????//?一級緩存和二級緩存的CacheKey是同一個
??????CacheKey?key?=?createCacheKey(ms,?parameter,?rowBounds,?boundSql);
??????return?query(ms,?parameter,?rowBounds,?resultHandler,?key,?boundSql);
????}
這里涉及到一級緩存和二級緩存,不是重點,我們就想看看最終是怎么執(zhí)行的jdbc操作,那么就只需要繼續(xù)看query重載。
????//?org.apache.ibatis.executor.BaseExecutor#query
????@Override
????public??List?query(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler,?CacheKey?key,?BoundSql?boundSql)?throws?SQLException?{
??????//?異常體系之?ErrorContext
??????ErrorContext.instance().resource(ms.getResource()).activity("executing?a?query").object(ms.getId());
??????if?(closed)?{
????????throw?new?ExecutorException("Executor?was?closed.");
??????}
??????if?(queryStack?==?0?&&?ms.isFlushCacheRequired())?{
????????//?flushCache="true"時,即使是查詢,也清空一級緩存
????????clearLocalCache();
??????}
??????List?list;
??????try?{
????????//?防止遞歸查詢重復(fù)處理緩存
????????queryStack++;
????????//?查詢一級緩存
????????//?ResultHandler?和?ResultSetHandler的區(qū)別
????????list?=?resultHandler?==?null???(List)?localCache.getObject(key)?:?null;
????????
????????if?(list?!=?null)?{
??????????handleLocallyCachedOutputParameters(ms,?key,?parameter,?boundSql);
????????}?else?{
??????????//?真正的查詢流程
??????????list?=?queryFromDatabase(ms,?parameter,?rowBounds,?resultHandler,?key,?boundSql);
??????????...省略N行代碼...
涉及到緩存的,通通與我無關(guān),只看真正的查詢流程 「queryFromDatabase」。
????private??List?queryFromDatabase(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler,?CacheKey?key,?BoundSql?boundSql)?throws?SQLException?{
??????List?list;
??????//?先占位
??????localCache.putObject(key,?EXECUTION_PLACEHOLDER);
??????try?{
????????//?三種?Executor?的區(qū)別,看doUpdate
????????//?默認Simple
????????list?=?doQuery(ms,?parameter,?rowBounds,?resultHandler,?boundSql);
??????}?finally?{
????????//?移除占位符
????????localCache.removeObject(key);
??????}
??????//?寫入一級緩存
??????localCache.putObject(key,?list);
??????if?(ms.getStatementType()?==?StatementType.CALLABLE)?{
????????localOutputParameterCache.putObject(key,?parameter);
??????}
??????return?list;
????}
看到j(luò)dbc了,勝利的曙光。
舒服,繼續(xù)看doQuery方法,看到resultHandler了么,結(jié)果處理器,感覺離結(jié)果更近了。
????//?org.apache.ibatis.executor.SimpleExecutor#doQuery
????@Override
????public??List?doQuery(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler,?BoundSql?boundSql)?throws?SQLException?{
??????Statement?stmt?=?null;
??????try?{
????????Configuration?configuration?=?ms.getConfiguration();
????????//?注意,已經(jīng)來到SQL處理的關(guān)鍵對象?StatementHandler?>>
????????StatementHandler?handler?=?configuration.newStatementHandler(wrapper,?ms,?parameter,?rowBounds,?resultHandler,?boundSql);
????????//?獲取一個?Statement對象
????????stmt?=?prepareStatement(handler,?ms.getStatementLog());
????????//?執(zhí)行查詢
????????return?handler.query(stmt,?resultHandler);
??????}?finally?{
????????//?用完就關(guān)閉
????????closeStatement(stmt);
??????}
????}
查詢用的Exucutor就是默認的SimpleExecutor,看到了熟悉的prepareStatement獲取流程,基本上就到底層jdbc了。那么我們就看看 「prepareStatement(handler, ms.getStatementLog());」
????private?Statement?prepareStatement(StatementHandler?handler,?Log?statementLog)?throws?SQLException?{
??????Statement?stmt;
??????Connection?connection?=?getConnection(statementLog);
??????//?獲取?Statement?對象
??????stmt?=?handler.prepare(connection,?transaction.getTimeout());
??????//?為?Statement?設(shè)置參數(shù)
??????handler.parameterize(stmt);
??????return?stmt;
????}
看到這里,就到j(luò)dbc層面了,我們看到了熟悉的Connection,獲取到connection之后再獲取Statement。
這里的Statement就是java.sql的statement接口。
?org.apache.ibatis.executor.statement.SimpleStatementHandler#query
?
????@Override
????public??List?query(Statement?statement,?ResultHandler?resultHandler)?throws?SQLException?{
??????String?sql?=?boundSql.getSql();
??????statement.execute(sql);
??????return?resultSetHandler.handleResultSets(statement);
????}
已經(jīng)獲取到了sql,通過Statement去執(zhí)行sql,再通過resultSetHandler處理結(jié)果集。
通過Statement去執(zhí)行sql
??????statement.execute(sql);
這里就已經(jīng)是jdbc層面的操作了,通過與數(shù)據(jù)庫建立的connection提交并執(zhí)行sql。
通過resultSetHandler處理結(jié)果集
?都到最后了,我們也不慌了,那么就看看org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSets是如何處理結(jié)果集的。
?
????@Override
????public?List這么一坨代碼,只需要重點看
????handleResultSet(rsw,?resultMap,?null,?parentMapping);
????private?void?handleResultSet(ResultSetWrapper?rsw,?ResultMap?resultMap,?List?multipleResults,?ResultMapping?parentMapping)?throws?SQLException?{
??????try?{
????????if?(parentMapping?!=?null)?{
??????????handleRowValues(rsw,?resultMap,?null,?RowBounds.DEFAULT,?parentMapping);
????????}?else?{
??????????if?(resultHandler?==?null)?{
????????????DefaultResultHandler?defaultResultHandler?=?new?DefaultResultHandler(objectFactory);
????????????handleRowValues(rsw,?resultMap,?defaultResultHandler,?rowBounds,?null);
????????????multipleResults.add(defaultResultHandler.getResultList());
??????????}?else?{
????????????handleRowValues(rsw,?resultMap,?resultHandler,?rowBounds,?null);
??????????}
????????}
??????}?finally?{
????????//?issue?#228?(close?resultsets)
????????closeResultSet(rsw.getResultSet());
??????}
????}
看看handleRowValues的邏輯 (有點心累),一鼓作氣再瞅兩眼。
最終,來到了這個地方:
????//?org.apache.ibatis.executor.resultset.DefaultResultSetHandler
????//?????????#handleRowValuesForSimpleResultMap
????private?void?handleRowValuesForSimpleResultMap(ResultSetWrapper?rsw,?ResultMap?resultMap,?ResultHandler>?resultHandler,?RowBounds?rowBounds,?ResultMapping?parentMapping)
????????throws?SQLException?{
??????DefaultResultContext?resultContext?=?new?DefaultResultContext<>();
??????//?看到了吧,沒什么好說的,就是jdbc的結(jié)果集處理
??????ResultSet?resultSet?=?rsw.getResultSet();
??????skipRows(resultSet,?rowBounds);
??????while?(shouldProcessMoreRows(resultContext,?rowBounds)?&&?!resultSet.isClosed()?&&?resultSet.next())?{
????????ResultMap?discriminatedResultMap?=?resolveDiscriminatedResultMap(resultSet,?resultMap,?null);
????????Object?rowValue?=?getRowValue(rsw,?discriminatedResultMap,?null);
????????storeObject(resultHandler,?resultContext,?rowValue,?parentMapping,?resultSet);
??????}
????}
行了,不用再往下挖了,看到了熟悉的ResultSet獲取結(jié)果集的操作,Mybatis執(zhí)行sql的流程基本就結(jié)束了。
底層還是熟悉的JDBC操作。
小結(jié)
其實寫了這么多,也沒啥想總結(jié)的,我們通過一個查詢操作,完整的把Mybatis從解析文件到執(zhí)行sql,再到結(jié)果集處理都從源碼級別剖析了一遍。
那么我們回答一下開頭的問題:
我只是寫了個Mapper接口,再配合xml或者注解,把SQL一寫,就可以執(zhí)行數(shù)據(jù)庫操作,這是為何?
其實我們獲取到的Mapper對象,已經(jīng)是Mybatis幫我們生成的代理對象了,這個代理對象擁有與jdbc交互的一切必要條件。
都說Mybatis是對JDBC的封裝,可是我卻看不到JDBC相關(guān)的接口和對象,它們到哪里去了?
稍微往上翻翻,我們剛講了,實際上最底層就是封裝的jdbc的接口。
我們看不到但是用到了,并且用起來還很爽,這就是封裝的魅力啊。
為什么在Spring中使用Mybatis,不用加@Repository/@Component之類的注解,就可以隨用隨注入(如:@Autowired)?
這個問題,就放到之后的文章講解吧,那么就敬請期待下一篇:Mybatis與Spring的愛情故事(從源碼層面解析,Mybatis是如何利用Spring擴展點,實現(xiàn)與Spring整合的。)
最后,貼張圖,概括一下這個過程。圖是借來的,僅供學習討論,侵刪。
?創(chuàng)建會話工廠SqlSessionFactory
?

?創(chuàng)建會話SqlSession
?

?創(chuàng)建代理對象
?

?調(diào)用代理對象,執(zhí)行SQL流程
?

那么,不見不散。
