解析 MyBatis 中 Mapper 生效的來龍去脈
最近閑了快有半個多月了,一直提不起興致再去看一些書籍(沒有以前瘋狂吸食知識的欲望了😓)。
不過這一兩天不知道是什么筋搭錯了非常想寫點什么,但又不知道寫點啥(苦惱)。所以我就結(jié)合了一下本人工作中經(jīng)常用到但沒有深入的技術(shù)下手了,最后思來想去就選擇了 MyBatis 中 Mapper 文件這一塊的知識內(nèi)容入手了。
以前只是知道寫一個 Mapper 接口,對應(yīng)著再去寫一個 Mapper.xml 文件然后將 Mapper 接口位置和 Mapper.xml 文件位置通過 MyBatisConfig.xml 的配置文件關(guān)聯(lián)起來就可以非常方便的操作訪問數(shù)據(jù)庫,但究其原因確是說不上個所以然來(汗顏)。
那既然搞出了前因,后面就一起往下學(xué)咯!
一切都從最簡單的開始,所以先來回顧一下其基本的使用(不會吧不會吧,最基本的hello world別忘了)。
步驟:
1、首先我們要創(chuàng)建一個maven工程
2、添加MyBatis的依賴及MySQL依賴,如下:
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis --><dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version></dependency>
3、再添加一個測試單元依賴吧,等會要通過測試單元進(jìn)行測試
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version></dependency>
OK,到這一步項目的基本環(huán)境就搭建完畢了,下面就是正式的使用 MyBatis 框架相關(guān)的內(nèi)容了。
1.1 編寫配置文件在資源目錄下面創(chuàng)建下面兩個配置文件:
這里我們先準(zhǔn)備數(shù)據(jù)庫連接信息的配置類:jdbc.properties
jdbc.driver=com.mysql.jdbc.Driverjdbc.url=jdbc:mysql://127.0.0.1:3306/mybatistest?useUnicode=true&characterEncoding=utf-8jdbc.username=rootjdbc.password=root
接著就是最重要的一個配置類了:MyBatisConfig.xml
<?xml version='1.0' encoding='UTF-8' ?><!DOCTYPE configuration PUBLIC '-//mybatis.org//DTD Config 3.0//EN' 'http://mybatis.org/dtd/mybatis-3-config.dtd'><configuration> <!-- 導(dǎo)入數(shù)據(jù)庫配置文件的信息--> <properties resource='jdbc.properties'></properties> <!-- 配置setting屬性--> <settings><!-- 開啟了一個駝峰命名規(guī)則--><setting name='mapUnderscoreToCamelCase' value='true'/><!-- 日志--><setting name='logImpl' value='STDOUT_LOGGING'></setting> </settings> <!-- 配置數(shù)據(jù)庫--> <environments default='development'><environment id='development'> <transactionManager type='JDBC'/> <!-- 配置連接池 --> <dataSource type='POOLED'><property name='driver' value='${jdbc.driver}'/><property name='url' value='${jdbc.url}'/><property name='username' value='${jdbc.username}'/><property name='password' value='${jdbc.password}'/> </dataSource></environment> </environments> <!-- mappers中注冊我們所有寫的dao接口的實現(xiàn)(映射)文件--> <mappers><mapper resource='/.../IUserMapper.xml'/><!-- 如果映射文件有十幾百個的話,可以用下面的全局注冊 <package name='文件所在包路徑'></package> <package name='cn.liuliang.Dao'></package> --> </mappers></configuration>1.2 編寫Mapper接口及測試方法
Mapper接口類
public interface IUserMapper { /** * 查詢所有用戶 * @return */ List<User> findAll();}
開始測試
public class MyBatisTest { @Test public void test01() throws IOException {// 讀取配置文件InputStream in= Resources.getResourceAsStream('MyBatisConfig.xml');// 創(chuàng)建sqlSessionFactorySqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);// 獲得會話SqlSession session=sqlSessionFactory.openSession();// 得到代理IUserMapper iUserMapper =session.getMapper(IUserMapper.class);// 查詢數(shù)據(jù)庫List<User> userList= iUserMapper.findAll();for (User user : userList) { System.out.println(user);} }}1.3 結(jié)果
SQL和結(jié)果都打印出來了😁。
以后,只要是對數(shù)據(jù)庫的操作,我們就只需要編寫 Mapper 接口和其對應(yīng)的 xml 文件就可以非常快速操作數(shù)據(jù)庫,對比以前原生JDBC操作什么拼接SQL、結(jié)果集映射、資源關(guān)閉一大堆操作讓我們開發(fā)人員來處理,也太雞肋了吧!所以對于這個 MyBatis 持久層框架我只想說(牛逼)。
下面就要全程高能哦!但其實也很簡單了,它就只是把原生操作的 JDBC 進(jìn)行了封裝,暴露出按照它所定義的簡單規(guī)則走而已,多的不說了,你們有資格一睹 MyBatis 源碼的芳容了。
既然要分析源碼了,那么從什么地方入手呢!— 測試方法
通過測試方法,我們可以知道 MyBatis 會先加載資源文件(MyBatisConfig.xml),因為這文件是一切的開始,通過這個文件可以知道數(shù)據(jù)源、特性(日志,駝峰命名…)、Mapper 文件等一系列信息。
2.1 通過配置文件構(gòu)建出 SqlSessionFactory第一個類名出現(xiàn)了:SqlSessionFactory ,它的類圖如下:
簡單熟悉一下圖中出現(xiàn)的名字吧:
SqlSessionFactory接口:SqlSessionFactory 負(fù)責(zé)創(chuàng)建 SqlSession 對象,其中只包含了多個 openSession() 方法的重載,可以通過其參數(shù)指定事務(wù)的隔離級別、底層使用 Executor 的類型以及是否自動提交事務(wù)等方面的配置。
DefaultSqlSessionFactory類:一個具體的工廠,實現(xiàn)了 SqlSessionFactory 接口。它主要提供了兩種創(chuàng)建 DefaultSqlSession 對象的方式:
通過數(shù)據(jù)源獲取數(shù)據(jù)庫連接,并創(chuàng)建 Executor 對象及 DefaultSqlSession 。 通過用戶提供的數(shù)據(jù)連接對象,DefaultSqlSessionFactory 會使用該數(shù)據(jù)庫連接對象創(chuàng)建 Executor 對象及 DefaultSqlSession。SqlSessionManager類:同時實現(xiàn)了 SqlSession 接口和 SqlSessionFactory 接口 ,也就同時提供了SqlSessionFactory 創(chuàng)建 SqlSession 以及 SqlSession 操縱數(shù)據(jù)庫的功能。
SqlSession接口:是mybatis的核心操作類,其中對數(shù)據(jù)庫的crud都封裝在這個中,是一個頂級接口,其中默認(rèn)實現(xiàn)類是DefaultSqlSession這個類。
DefaultSqlSession類:默認(rèn) SqlSession 接口的 CRUD 實現(xiàn)類,且 DefaultSqlsession 不是線程安全的(對于線程安全,關(guān)注session和connnect的關(guān)系就好了)好了開始分析,從第一行代碼入手:
// 讀取配置文件InputStream in= Resources.getResourceAsStream('MyBatisConfig.xml');// 創(chuàng)建sqlSessionFactorySqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
SqlSessionFactoryBuilder # build
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { // ... // 根據(jù)文件流,創(chuàng)建 XMLConfigBuilder 對象 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // 先解析 配置文件,然后構(gòu)建出 SqlSessionFactory對象 return build(parser.parse()); // ...}
最終會創(chuàng)建一個 DefaultSqlSessionFactory 對象返回出去
public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config);}
流程如下:
在獲取到會話工廠之后,就是根據(jù)工廠獲得具體的會話了。
代碼入口:
// 獲得會話SqlSession session=sqlSessionFactory.openSession();
調(diào)用:DefaultSqlSessionFactory # openSession()
public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);}
最終來到:DefaultSqlSessionFactory # openSessionFromDataSource()
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try {// 根據(jù)配置文件 configuration 獲取對應(yīng)的會話環(huán)境(包括事物,數(shù)據(jù)源)final Environment environment = configuration.getEnvironment();// 獲取事物工廠final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);// 根據(jù)數(shù)據(jù)源,配置事物,autoCommit:是否自動提交事物tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);// 根據(jù)配置獲取執(zhí)行器(最終都是它執(zhí)行對應(yīng)的數(shù)據(jù)庫操作)final Executor executor = configuration.newExecutor(tx, execType);// 準(zhǔn)備好上面的信息之后,都封裝到默認(rèn)會話對象中返回出去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(); }}
在獲取 SqlSession 對象的過程中,都是根據(jù)默認(rèn)的會話工廠,從工廠中獲取對應(yīng)的會話。這樣在我看來非常的不錯,因為獲取一個數(shù)據(jù)庫的操作會話是需要配置非常多的屬性的,包括數(shù)據(jù)源配置、事物配置等。但是有了這個創(chuàng)建會話工廠類之后,那么一切就變得簡單起來了,工廠囊括了所有的細(xì)節(jié),只需要我們調(diào)一個對外的 API 我們就可以獲得對應(yīng)的 SqlSession 對象(工廠幫我們做了細(xì)節(jié)),進(jìn)而操作數(shù)據(jù)庫,讀了上面的代碼就是一個很好的提現(xiàn)😀。
提一點:
配置文件(MyBatisConfig.xml)構(gòu)造出默認(rèn)會話工廠(SqlSessionFactory),工廠再創(chuàng)建出具體的操作數(shù)據(jù)庫會話(SqlSession)
2.3 根據(jù) SqlSession 獲取 Mapper 代理在上面,已經(jīng)分析了如何獲取一個會話的源碼,那我們得到一個會話之后,就是要根據(jù)具體的 Mapper 接口獲得對應(yīng)的操作數(shù)據(jù)庫代理對象了,就是下面這段代碼:
// 得到代理IUserMapper iUserMapper =session.getMapper(IUserMapper.class);
點進(jìn)去看看
因為 session 對象是由 DefaultSqlSessionFactory 創(chuàng)建出來的 DefaultSqlSession,所以該代碼位于此類中
public <T> T getMapper(Class<T> type) { // 根據(jù)配置類,獲取 Mapper return configuration.getMapper(type, this);}
點進(jìn)去:Configuration # getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { // 從 mapperRegistry 中獲取具體 Mapper return mapperRegistry.getMapper(type, sqlSession);}
MapperRegistry:可以理解為 Mapper 接口的注冊中心,里面存放了所有 Mapper 接口相關(guān)屬性。
MapperRegistry# getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { // knownMappers,一個Map,存放 Mapper 代理工廠 // 在初始化的時候根據(jù)配置文件已經(jīng)將所有配置的 Mapper 接口注冊到此了 final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) 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); }}
點進(jìn)具體代理:MapperProxyFactory # newInstance
public T newInstance(SqlSession sqlSession) { // 根據(jù) SqlSession 和 Mapper 接口生成代理對象 final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); // 真正代理,如下 return newInstance(mapperProxy);}// 下面就是根據(jù) JDK 原生 API 進(jìn)行代理了,由此返回代理對象給用戶使用protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}
以上就是 Mapper 接口被代理的全部流程了,其中先是根據(jù)會話去獲得對應(yīng)的 Mapper 但其內(nèi)部調(diào)用的是 Mapper 注冊中心(MapperRegistry)獲取,這里面有所有配置的 Mapper 接口,在 MapperRegistry 中維護(hù)了一個 Map 鍵為 Class 值是 MapperProxyFactory ,這樣就可以獲得要代理 Mapper 接口的代理工廠,最后通過這個工廠生成我們想要的 Mapper 返回用戶。
流程不復(fù)雜,就是里面出現(xiàn)了很多 MapperXXX 相關(guān)的類,那么下面我梳理一下這些類關(guān)系圖如下:
對于具體的代理執(zhí)行類這一步就要到執(zhí)行這一塊了,當(dāng)用戶通過我們返回的代理類(Mapper 接口)執(zhí)行對應(yīng)方法時,就會走到圖中涉及的類。
按照慣例,來個流程圖吧!
上面的所有分析,都是為了等到一個具體的操作數(shù)據(jù)庫的一個橋梁,那就是 Mapper 代理了(iUserMapper)。
接下來就是分析最后一步了,真正操作數(shù)據(jù)庫,代碼如下:
// 查詢數(shù)據(jù)庫List<User> userList= iUserMapper.findAll();for (User user : userList) { System.out.println(user);}
對于 iUserMapper 對象,我們知道他是代理去執(zhí)行的,所以直接點進(jìn)去的話根本行不通,那么我們可以通過 Debug 進(jìn)去看看。
MapperProxy # invoke
@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try {// 方法的類為 object 直接通過原始 JDK 去執(zhí)行if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args);} else { // 根據(jù)方法,獲得方法的執(zhí)行器后再執(zhí)行代理方法 return cachedInvoker(method).invoke(proxy, method, args, sqlSession);} } catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t); }}
我們先進(jìn)入 MapperProxy # cachedInvoker 這個方法看看
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable { try {// 先查緩存,有就返回,沒有就創(chuàng)建MapperMethodInvoker invoker = methodCache.get(method);if (invoker != null) { return invoker;}return methodCache.computeIfAbsent(method, m -> { // ... // 返回 PlainMethodInvoker 類型的 Mapper 方法執(zhí)行器 return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); // ...}); } catch (RuntimeException re) {Throwable cause = re.getCause();throw cause == null ? re : cause; }}
接著進(jìn)入 PlainMethodInvoker# invoke 這個方法
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable { // 調(diào)用 mapperMethod 對象的 execute 方法去真正執(zhí)行了 return mapperMethod.execute(sqlSession, args);}
真正執(zhí)行的開始 execute
MapperMethod # execute
public Object execute(SqlSession sqlSession, Object[] args) { // 這里面內(nèi)容比較多,我簡單分析一下 // 1)封裝參數(shù) // 2)根據(jù)對應(yīng)的執(zhí)行類型(INSERT,UPDATE,DELETE,SELECT),執(zhí)行對應(yīng)的方法 // 3)根據(jù)參數(shù),執(zhí)行類型封裝對應(yīng)的 sql // 4)操作原生 JDBC API 執(zhí)行數(shù)據(jù)庫操作 // 5)封裝結(jié)果集,返回出去}
我們 Debug 這個方法最后一步,看看結(jié)果:
到此,我們的 Mapper 接口及文件生效的原理,就全部過了一邊,是不是覺得不是很難呢!
在分析這一塊源碼時,本人理解的步驟就是:
一步步點進(jìn)源碼看。 畫出流程圖,不清楚的就 Debug。 很重要一點,對很多出現(xiàn)類似名字的類,一定要畫出類圖,搞清楚關(guān)系在往下走(助于理解每個類的職責(zé))。 最后,那就是寫點筆記了,畢竟好記性不如爛筆頭。2.5 整體流程圖到此這篇關(guān)于解析 MyBatis 中 Mapper 生效的前因后果的文章就介紹到這了,更多相關(guān) MyBatis 中 Mapper 生效內(nèi)容請搜索好吧啦網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持好吧啦網(wǎng)!
相關(guān)文章:
1. oracle觸發(fā)器介紹2. 使用mysql記錄從url返回的http GET請求數(shù)據(jù)操作3. DB2中多種常用功能的解決方法(1)4. MYSQL數(shù)據(jù)庫存文本轉(zhuǎn)存數(shù)據(jù)庫問題5. mysql存儲過程多層游標(biāo)循環(huán)嵌套的寫法分享6. Mybatis傳入List實現(xiàn)批量更新的示例代碼7. mysql 模糊查詢 concat()的用法詳解8. Oracle故障處理Rman-06207&Rman-06214的方法9. SQL Server 2005-如何在SQL Server用戶自訂函數(shù)中調(diào)用GetDate()函數(shù)10. 恢復(fù)從 Access 2000、 Access 2002 或 Access 2003 中數(shù)據(jù)庫刪除表的方法
