MyBatis的動態(tài)代理實現(xiàn)機制
點擊上方?藍字?關注我們!
Java,Python,C/C++,Linux,PHP,Go,C#,QT,大數據,算法,軟件教程,前端,簡歷,畢業(yè)設計等分類,資源在不斷更新中... 點擊領取!


前言
一直以來都在使用MyBatis做持久化框架,也知道當我們定義XXXMapper接口類并利用它來做CRUD操作時,Mybatis是利用了動態(tài)代理的技術幫我們生成代理類。那么動態(tài)代理內部的實現(xiàn)細節(jié)到底是怎么的呀?XXXMapper.java類和XXXMapper.xml到底是如何關聯(lián)起來的呀?本篇文章就來詳細剖析下MyBatis的動態(tài)代理的具體實現(xiàn)機制。
MyBatis的核心組件及應用
在詳細探究MyBatis中動態(tài)代理機制之前,先來補充一下基礎知識,認識一下MyBatis的核心組件。
SqlSessionFactoryBuilder(構造器):它可以從XML、注解或者手動配置Java代碼來創(chuàng)建SqlSessionFactory。 SqlSessionFactory: 用于創(chuàng)建SqlSession (會話) 的工廠 SqlSession: SqlSession是Mybatis最核心的類,可以用于執(zhí)行語句、提交或回滾事務以及獲取映射器Mapper的接口 SQL Mapper:它是由一個Java接口和XML文件(或注解)構成的,需要給出對應的SQL和映射規(guī)則,它負責發(fā)送SQL去執(zhí)行,并返回結果
“注意:現(xiàn)在我們使用Mybatis,一般都是和Spring框架整合在一起使用,這種情況下,SqlSession將被Spring框架所創(chuàng)建,所以往往不需要我們使用SqlSessionFactoryBuilder或者SqlSessionFactory去創(chuàng)建SqlSession
下面展示一下如何使用MyBatis的這些組件,或者如何快速使用MyBatis:
數據庫表
???CREATE?TABLE??user(
?????id?int,
?????name?VARCHAR(255)?not?NULL?,
?????age?int?,
?????PRIMARY?KEY?(id)
???)ENGINE?=INNODB?DEFAULT?CHARSET=utf8;
聲明一個User類
???@Data
???public?class?User?{
???????private?int?id;
???????private?int?age;
???????private?String?name;
???
???????@Override
???????public?String?toString()?{
???????????return?"User{"?+
???????????????????"id="?+?id?+
???????????????????",?age="?+?age?+
???????????????????",?name='"?+?name?+?'\''?+
???????????????????'}';
???????}
???}
定義一個全局配置文件mybatis-config.xml (關于配置文件中具體屬性標簽解釋參閱官方文檔)
???"1.0"?encoding="UTF-8"??>
???"-//mybatis.org//DTD?Config?3.0//EN"
???????????"http://mybatis.org/dtd/mybatis-3-config.dtd">
???
???
???????
???????"development">
???????????"development">
???????????????
???????????????type="MANAGED"?/>
???????????????
???????????????type="POOLED">
???????????????????"driver"?value="com.mysql.jdbc.Driver"?/>
???????????????????"url"?value="jdbc:mysql://localhost:3306/test"/>
???????????????????"username"?value="root"?/>
???????????????????"password"?value="root"?/>
???????????????
???????????
???????
???????
???????
???????????"mapper/UserMapper.xml"/>
???????
???
UserMapper接口
???public?interface?UserMapper?{
???????User?selectById(int?id);
???}
UserMapper文件
???"-//mybatis.org//DTD?Mapper?3.0//EN"
???????????"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
???
???"com.pjmike.mybatis.UserMapper">
???????
???
測試類
???public?class?MybatisTest?{
???????private?static?SqlSessionFactory?sqlSessionFactory;
???????static?{
???????????try?{
???????????????sqlSessionFactory?=?new?SqlSessionFactoryBuilder()
???????????????????????.build(Resources.getResourceAsStream("mybatis-config.xml"));
???????????}?catch?(IOException?e)?{
???????????????e.printStackTrace();
???????????}
???????}
???????public?static?void?main(String[]?args)?{
???????????try?(SqlSession?sqlSession?=?sqlSessionFactory.openSession())?{
???????????????UserMapper?userMapper?=?sqlSession.getMapper(UserMapper.class);
???????????????User?user?=?userMapper.selectById(1);
???????????????System.out.println("User?:?"?+?user);
???????????}
???????}
???}
???//?結果:
???User?:?User{id=1,?age=21,?name='pjmike'}
上面的例子簡單的展示了如何使用MyBatis,與此同時,我也將用這個例子來進一步探究MyBatis動態(tài)原理的實現(xiàn)。
MyBatis動態(tài)代理的實現(xiàn)
public?static?void?main(String[]?args)?{
????try?(SqlSession?sqlSession?=?sqlSessionFactory.openSession())?{
????????UserMapper?userMapper?=?sqlSession.getMapper(UserMapper.class);//?<1>
????????User?user?=?userMapper.selectById(1);
????????System.out.println("User?:?"?+?user);
????}
}
在前面的例子中,我們使用sqlSession.getMapper()方法獲取UserMapper對象,實際上這里我們是獲取了UserMapper接口的代理類,然后再由代理類執(zhí)行方法。那么這個代理類是如何生成的呢?在探究動態(tài)代理類如何生成之前,我們先來看下SqlSessionFactory工廠的創(chuàng)建過程做了哪些準備工作,比如說mybatis-config配置文件是如何讀取的,映射器文件是如何讀取的?
mybatis全局配置文件解析
private?static?SqlSessionFactory?sqlSessionFactory;
static?{
????try?{
????????sqlSessionFactory?=?new?SqlSessionFactoryBuilder()
????????????????.build(Resources.getResourceAsStream("mybatis-config.xml"));
????}?catch?(IOException?e)?{
????????e.printStackTrace();
????}
}
我們使用new SqlSessionFactoryBuilder().build()的方式創(chuàng)建SqlSessionFactory工廠,走進build方法
public?SqlSessionFactory?build(InputStream?inputStream,?Properties?properties)?{
???return?build(inputStream,?null,?properties);
?}
?public?SqlSessionFactory?build(InputStream?inputStream,?String?environment,?Properties?properties)?{
???try?{
?????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.
?????}
???}
?}
對于mybatis的全局配置文件的解析,相關解析代碼位于XMLConfigBuilder的parse()方法中:
public?Configuration?parse()?{
???if?(parsed)?{
?????throw?new?BuilderException("Each?XMLConfigBuilder?can?only?be?used?once.");
???}
???parsed?=?true;
???//解析全局配置文件
???parseConfiguration(parser.evalNode("/configuration"));
???return?configuration;
?}
?private?void?parseConfiguration(XNode?root)?{
???try?{
?????//issue?#117?read?properties?first
?????propertiesElement(root.evalNode("properties"));
?????Properties?settings?=?settingsAsProperties(root.evalNode("settings"));
?????loadCustomVfs(settings);
?????loadCustomLogImpl(settings);
?????typeAliasesElement(root.evalNode("typeAliases"));
?????pluginElement(root.evalNode("plugins"));
?????objectFactoryElement(root.evalNode("objectFactory"));
?????objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
?????reflectorFactoryElement(root.evalNode("reflectorFactory"));
?????settingsElement(settings);
?????//?read?it?after?objectFactory?and?objectWrapperFactory?issue?#631
?????environmentsElement(root.evalNode("environments"));
?????databaseIdProviderElement(root.evalNode("databaseIdProvider"));
?????typeHandlerElement(root.evalNode("typeHandlers"));
?????//解析mapper映射器文件
?????mapperElement(root.evalNode("mappers"));
???}?catch?(Exception?e)?{
?????throw?new?BuilderException("Error?parsing?SQL?Mapper?Configuration.?Cause:?"?+?e,?e);
???}
?}
從parseConfiguration方法的源代碼中很容易就可以看出它對mybatis全局配置文件中各個元素屬性的解析。當然最終解析后返回一個Configuration對象,Configuration是一個很重要的類,它包含了Mybatis的所有配置信息,它是通過XMLConfigBuilder取錢構建的,Mybatis通過XMLConfigBuilder讀取mybatis-config.xml中配置的信息,然后將這些信息保存到Configuration中
映射器Mapper文件的解析
//解析mapper映射器文件
mapperElement(root.evalNode("mappers"));
該方法是對全局配置文件中mappers屬性的解析,走進去:
[
](https://pjmike-1253796536.cos.ap-beijing.myqcloud.com/mybatis/mapper xml解析.png)mapper xml
mapperParser.parse()方法就是XMLMapperBuilder對Mapper映射器文件進行解析,可與XMLConfigBuilder進行類比
public?void?parse()?{
??if?(!configuration.isResourceLoaded(resource))?{
????configurationElement(parser.evalNode("/mapper"));?//解析映射文件的根節(jié)點mapper元素
????configuration.addLoadedResource(resource);??
????bindMapperForNamespace();?//重點方法,這個方法內部會根據namespace屬性值,生成動態(tài)代理類
??}
??parsePendingResultMaps();
??parsePendingCacheRefs();
??parsePendingStatements();
}
configurationElement(XNode context)方法
該方法主要用于將mapper文件中的元素信息,比如insert、select這等信息解析到MappedStatement對象,并保存到Configuration類中的mappedStatements屬性中,以便于后續(xù)動態(tài)代理類執(zhí)行CRUD操作時能夠獲取真正的Sql語句信息

configurationElement
buildStatementFromContext方法就用于解析insert、select這類元素信息,并將其封裝成MappedStatement對象,具體的實現(xiàn)細節(jié)這里就不細說了。
bindMapperForNamespace()方法
該方法是核心方法,它會根據mapper文件中的namespace屬性值,為接口生成動態(tài)代理類,這就來到了我們的主題內容——動態(tài)代理類是如何生成的。
動態(tài)代理類的生成
bindMapperForNamespace方法源碼如下所示:
private?void?bindMapperForNamespace()?{
???//獲取mapper元素的namespace屬性值
???String?namespace?=?builderAssistant.getCurrentNamespace();
???if?(namespace?!=?null)?{
?????Class>?boundType?=?null;
?????try?{
???????//?獲取namespace屬性值對應的Class對象
???????boundType?=?Resources.classForName(namespace);
?????}?catch?(ClassNotFoundException?e)?{
???????//如果沒有這個類,則直接忽略,這是因為namespace屬性值只需要唯一即可,并不一定對應一個XXXMapper接口
???????//沒有XXXMapper接口的時候,我們可以直接使用SqlSession來進行增刪改查
?????}
?????if?(boundType?!=?null)?{
???????if?(!configuration.hasMapper(boundType))?{
?????????//?Spring?may?not?know?the?real?resource?name?so?we?set?a?flag
?????????//?to?prevent?loading?again?this?resource?from?the?mapper?interface
?????????//?look?at?MapperAnnotationBuilder#loadXmlResource
?????????configuration.addLoadedResource("namespace:"?+?namespace);
?????????//如果namespace屬性值有對應的Java類,調用Configuration的addMapper方法,將其添加到MapperRegistry中
?????????configuration.addMapper(boundType);
???????}
?????}
???}
?}
這里提到了Configuration的addMapper方法,實際上Configuration類里面通過MapperRegistry對象維護了所有要生成動態(tài)代理類的XxxMapper接口信息,可見Configuration類確實是相當重要一類
public?class?Configuration?{
????...
????protected?MapperRegistry?mapperRegistry?=?new?MapperRegistry(this);
????...
????public??void?addMapper(Class?type)?{
??????mapperRegistry.addMapper(type);
????}
????public??T?getMapper(Class?type,?SqlSession?sqlSession)?{
??????return?mapperRegistry.getMapper(type,?sqlSession);
????}
????...
}
其中兩個重要的方法:getMapper()和addMapper()
getMapper(): 用于創(chuàng)建接口的動態(tài)類 addMapper(): mybatis在解析配置文件時,會將需要生成動態(tài)代理類的接口注冊到其中
1. Configuration#addMappper()
Configuration將addMapper方法委托給MapperRegistry的addMapper進行的,源碼如下:
public??void?addMapper(Class?type)?{
??//?這個class必須是一個接口,因為是使用JDK動態(tài)代理,所以需要是接口,否則不會針對其生成動態(tài)代理
??if?(type.isInterface())?{
????if?(hasMapper(type))?{
??????throw?new?BindingException("Type?"?+?type?+?"?is?already?known?to?the?MapperRegistry.");
????}
????boolean?loadCompleted?=?false;
????try?{
??????//?生成一個MapperProxyFactory,用于之后生成動態(tài)代理類
??????knownMappers.put(type,?new?MapperProxyFactory<>(type));
??????//以下代碼片段用于解析我們定義的XxxMapper接口里面使用的注解,這主要是處理不使用xml映射文件的情況
??????MapperAnnotationBuilder?parser?=?new?MapperAnnotationBuilder(config,?type);
??????parser.parse();
??????loadCompleted?=?true;
????}?finally?{
??????if?(!loadCompleted)?{
????????knownMappers.remove(type);
??????}
????}
??}
}
MapperRegistry內部維護一個映射關系,每個接口對應一個MapperProxyFactory(生成動態(tài)代理工廠類)
private?final?Map,?MapperProxyFactory>>?knownMappers?=?new?HashMap<>();
這樣便于在后面調用MapperRegistry的getMapper()時,直接從Map中獲取某個接口對應的動態(tài)代理工廠類,然后再利用工廠類針對其接口生成真正的動態(tài)代理類。
2. Configuration#getMapper()
Configuration的getMapper()方法內部就是調用MapperRegistry的getMapper()方法,源代碼如下:
public??T?getMapper(Class?type,?SqlSession?sqlSession)?{
??//根據Class對象獲取創(chuàng)建動態(tài)代理的工廠對象MapperProxyFactory
??final?MapperProxyFactory?mapperProxyFactory?=?(MapperProxyFactory)?knownMappers.get(type);
??if?(mapperProxyFactory?==?null)?{
????throw?new?BindingException("Type?"?+?type?+?"?is?not?known?to?the?MapperRegistry.");
??}
??try?{
????//這里可以看到每次調用都會創(chuàng)建一個新的代理對象返回
????return?mapperProxyFactory.newInstance(sqlSession);
??}?catch?(Exception?e)?{
????throw?new?BindingException("Error?getting?mapper?instance.?Cause:?"?+?e,?e);
??}
}
從上面可以看出,創(chuàng)建動態(tài)代理類的核心代碼就是在MapperProxyFactory.newInstance方法中,源碼如下:
protected?T?newInstance(MapperProxy?mapperProxy)?{
??//這里使用JDK動態(tài)代理,通過Proxy.newProxyInstance生成動態(tài)代理類
??// newProxyInstance的參數:類加載器、接口類、InvocationHandler接口實現(xiàn)類
??//?動態(tài)代理可以將所有接口的調用重定向到調用處理器InvocationHandler,調用它的invoke方法
??return?(T)?Proxy.newProxyInstance(mapperInterface.getClassLoader(),?new?Class[]?{?mapperInterface?},?mapperProxy);
}
public?T?newInstance(SqlSession?sqlSession)?{
??final?MapperProxy?mapperProxy?=?new?MapperProxy<>(sqlSession,?mapperInterface,?methodCache);
??return?newInstance(mapperProxy);
}
“PS: 關于JDK動態(tài)代理的詳細介紹這里就不再細說了,有興趣的可以參閱我之前寫的文章:動態(tài)代理的原理及其應用
這里的InvocationHandler接口的實現(xiàn)類是MapperProxy,其源碼如下:
public?class?MapperProxy?implements?InvocationHandler,?Serializable?{
??private?static?final?long?serialVersionUID?=?-6424540398559729838L;
??private?final?SqlSession?sqlSession;
??private?final?Class?mapperInterface;
??private?final?Map?methodCache;
??public?MapperProxy(SqlSession?sqlSession,?Class?mapperInterface,?Map?methodCache)?{
????this.sqlSession?=?sqlSession;
????this.mapperInterface?=?mapperInterface;
????this.methodCache?=?methodCache;
??}
??@Override
??public?Object?invoke(Object?proxy,?Method?method,?Object[]?args)?throws?Throwable?{
????
????try?{
??????//如果調用的是Object類中定義的方法,直接通過反射調用即可
??????if?(Object.class.equals(method.getDeclaringClass()))?{
????????return?method.invoke(this,?args);
??????}?else?if?(isDefaultMethod(method))?{
????????return?invokeDefaultMethod(proxy,?method,?args);
??????}
????}?catch?(Throwable?t)?{
??????throw?ExceptionUtil.unwrapThrowable(t);
????}
????//調用XxxMapper接口自定義的方法,進行代理
????//首先將當前被調用的方法Method構造成一個MapperMethod對象,然后掉用其execute方法真正的開始執(zhí)行。
????final?MapperMethod?mapperMethod?=?cachedMapperMethod(method);
????return?mapperMethod.execute(sqlSession,?args);
??}
??private?MapperMethod?cachedMapperMethod(Method?method)?{
????return?methodCache.computeIfAbsent(method,?k?->?new?MapperMethod(mapperInterface,?method,?sqlSession.getConfiguration()));
??}
??...
}
最終的執(zhí)行邏輯在于MapperMethod類的execute方法,源碼如下:
public?class?MapperMethod?{
??private?final?SqlCommand?command;
??private?final?MethodSignature?method;
??public?MapperMethod(Class>?mapperInterface,?Method?method,?Configuration?config)?{
????this.command?=?new?SqlCommand(config,?mapperInterface,?method);
????this.method?=?new?MethodSignature(config,?mapperInterface,?method);
??}
??public?Object?execute(SqlSession?sqlSession,?Object[]?args)?{
????Object?result;
????switch?(command.getType())?{
??????//insert語句的處理邏輯
??????case?INSERT:?{
????????Object?param?=?method.convertArgsToSqlCommandParam(args);
????????result?=?rowCountResult(sqlSession.insert(command.getName(),?param));
????????break;
??????}
??????//update語句的處理邏輯
??????case?UPDATE:?{
????????Object?param?=?method.convertArgsToSqlCommandParam(args);
????????result?=?rowCountResult(sqlSession.update(command.getName(),?param));
????????break;
??????}
??????//delete語句的處理邏輯
??????case?DELETE:?{
????????Object?param?=?method.convertArgsToSqlCommandParam(args);
????????result?=?rowCountResult(sqlSession.delete(command.getName(),?param));
????????break;
??????}
??????//select語句的處理邏輯
??????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);
??????????//調用sqlSession的selectOne方法
??????????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;
??}
??...
}
在MapperMethod中還有兩個內部類,SqlCommand和MethodSignature類,在execute方法中首先用switch case語句根據SqlCommand的getType()方法,判斷要執(zhí)行的sql類型,比如INSET、UPDATE、DELETE、SELECT和FLUSH,然后分別調用SqlSession的增刪改查等方法。
慢著,說了這么多,那么這個getMapper()方法什么時候被調用呀?實際是一開始我們調用SqlSession的getMapper()方法:
UserMapper?userMapper?=?sqlSession.getMapper(UserMapper.class);
public?class?DefaultSqlSession?implements?SqlSession?{
??private?final?Configuration?configuration;
??private?final?Executor?executor;
??@Override
??public??T?getMapper(Class?type)?{
????return?configuration.getMapper(type,?this);
??}
??...
}
所以getMapper方法的大致調用邏輯鏈是:SqlSession#getMapper() ——> Configuration#getMapper() ——> MapperRegistry#getMapper() ——> MapperProxyFactory#newInstance() ——> Proxy#newProxyInstance()
還有一點我們需要注意:我們通過SqlSession的getMapper方法獲得接口代理來進行CRUD操作,其底層還是依靠的是SqlSession的使用方法。
小結
根據上面的探究過程,簡單畫了一個邏輯圖(不一定準確):

本篇文章主要介紹了MyBatis的動態(tài)原理,回過頭來,我們需要知道我們使用UserMapper的動態(tài)代理類進行CRUD操作,本質上還是通過SqlSession這個關鍵類執(zhí)行增刪改查操作,但是對于SqlSession如何具體執(zhí)行CRUD的操作并沒有仔細闡述,有興趣的同學可以查閱相關資料。
往期推薦

END
若覺得文章對你有幫助,隨手轉發(fā)分享,也是我們繼續(xù)更新的動力。
長按二維碼,掃掃關注哦
?「C語言中文網」官方公眾號,關注手機閱讀教程??
目前收集的資料包括:?Java,Python,C/C++,Linux,PHP,go,C#,QT,git/svn,人工智能,大數據,單片機,算法,小程序,易語言,安卓,ios,PPT,軟件教程,前端,軟件測試,簡歷,畢業(yè)設計,公開課?等分類,資源在不斷更新中...

