手寫mybatis源碼
來自專欄 java手寫各種底層源碼2 人贊了文章
源碼及sql鏈接:https://pan.baidu.com/s/1hqt3hvz5bNGD9oDtwetQOw 密碼:uane
參考視頻:騰訊課堂
此文需要大家熟悉mybatis的基本操作,不然難以理解。
mybatis核心流程的三大階段:
1.初始化階段:讀取xml配置文件和註解中的配置信息,創建配置對象,並完成各個模塊的初始化的工作;
2.代理階段:封裝ibatis的編程模型,使用mapper介面開發初始化的工作。
3.數據讀寫階段:通過sqlsession完成sql的解析,參數的映射,sql的執行,結果的解析過程。
mapper.xml里的重要元素:namespace(對應映射的java介面文件),id(通過id尋找介面文件里的方法名),resultmap(接收從mysql返回的數據時使用的實體類,就表示一種映射規則)
首先,我們完成第一階段:
1.首先準備好實體類,mapper.java文件和mapper.xml文件,資料庫的基本配置文件db.properities.
2.然後需要一個MapperStatement類來存儲xml文件裡面sql語句的信息。具體是用於存儲xml文件里的數據結構,namespace,id,resultmap,sql語句。一個MapperStatement實例只能保存一個sql語句信息。
3. 需要一個Configuration對象存儲db.properities文件裡面的信息,並且存儲所有sql語句的信息,即用一個hashmap存儲所有的MapperStatement實例,用xml文件里的namespace+id來作為key,這樣就可以實現唯一定位。
4.創建SqlSessionFactory類,它有兩個作用:1.把配置文件載入到configuration實例裡面 2.生產sqlsession
第一步是載入db.properities文件裡面的信息到configuration裡面,這裡就是先找到該文件的位置,然後用Properties類讀取裡面的數據,然後set進configuration裡面,
第二步是載入指定文件夾下的mapper.xml文件,也就是說遍歷mappers文件夾下所有的mapper.xml文件,解析信息後註冊到conf中。這裡就需要工具包dom4j---- java代碼專門解析xml文件的工具類,用SAXReader讀取文件信息,然後丟到document裡面,然後就是用dom4j的體系結構讀取出xml裡面的sql的namespace,id,resultmap,sql到mapperStatement中。最後是用configuration對象中的hashmap以namespace+id名作為key存儲mapperStatement。
第三步是創建一個opensession方法,把configuration傳到sqlsession當中,也就是說從sqlsessionfactory當中出來的sqlsession都是帶有所有的配置信息的。此時要創建sqlsession介面及其實現類,構造函數中傳入configuration。
以下為SqlSessionFactory源碼:
package com.enjoylearning.mybatis.session;import java.io.File;import java.io.IOException;import java.io.InputStream;import java.net.URL;import java.util.List;import java.util.Properties;import org.dom4j.Document;import org.dom4j.DocumentException;import org.dom4j.Element;import org.dom4j.io.SAXReader;import com.enjoylearning.mybatis.config.Configuration;import com.enjoylearning.mybatis.config.MapperStatement;//1.把配置文件載入到內存configuration裡面//2.生產sqlsessionpublic class SqlSessionFactory { private Configuration conf = new Configuration();// 配置信息,必然為單例模式 public SqlSessionFactory() { loadDbInfo();// 載入資料庫的信息到conf裡面 loadMappersInfo();// 載入所有的mapper信息到conf裡面 } // 定位配置文件夾的位置 public static final String MAPPER_CONFIG_LOCATION = "mappers"; public static final String DB_CONFIG_FILE = "db.properties"; // 載入資料庫配置信息 private void loadDbInfo() { // 載入資料庫信息配置文件 // 其實我很好奇不在同一當前路徑下也能找到該文件並且載入進來嗎???事實證明是可以的 InputStream dbIn = SqlSessionFactory.class.getClassLoader().getResourceAsStream(DB_CONFIG_FILE); Properties p = new Properties(); System.out.println("dbin=" + dbIn); System.out.println("SqlSessionFactory.class.getClassLoader()=" + SqlSessionFactory.class.getClassLoader()); try { p.load(dbIn);// 將配置信息寫入properties對象 } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } // 將資料庫信息寫入conf對象 conf.setJdbcDriver(p.getProperty("jdbc.driver").toString()); conf.setJdbcPassword(p.getProperty("jdbc.password").toString()); conf.setJdbcUrl(p.getProperty("jdbc.url").toString()); conf.setJdbcUsername(p.getProperty("jdbc.username").toString()); } // 載入指定文件夾下的mapper.xml文件 private void loadMappersInfo() { URL resources = null; resources = SqlSessionFactory.class.getClassLoader().getResource(MAPPER_CONFIG_LOCATION); // System.out.println("resources=" + resources); // 注意這個是在target文件夾中!!! // E:/java/workspace/jackhe-mybatis/target/classes/mappers // System.out.println("resources.getFile()" + resources.getFile()); File mappers = new File(resources.getFile());// 獲取指定文件夾信息 if (mappers.isDirectory()) { File[] listFiles = mappers.listFiles(); // 遍歷mappers文件夾下所有的mapper.xml文件,解析信息後註冊到conf中 for (File file : listFiles) { loadMapperInfo(file); } } } private void loadMapperInfo(File file) { // dom4j java代碼專門解析xml文件的工具類 // 用SAXReader讀取文件信息,然後丟到document裡面 SAXReader reader = new SAXReader(); Document document = null; try { document = reader.read(file); } catch (DocumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } // 獲取根節點元素對象mapper Element node = document.getRootElement(); // 獲取命令空間 String namespace = node.attribute("namespace").getData().toString(); // 獲取select子節點列表 List<Element> selects = node.elements("select"); // 遍歷selec節點,將信息記錄到MapperStatement對象,並登記到configuration當中。 for (Element element : selects) { MapperStatement mapperStatement = new MapperStatement(); String id = element.attribute("id").getData().toString(); String resultType = element.attribute("resultType").getData().toString(); String sql = element.getData().toString(); String sourceId = namespace + "." + id; // ??? mapperStatement.setNamespace(namespace); mapperStatement.setResultType(resultType); mapperStatement.setSourceId(sourceId); mapperStatement.setSql(sql); System.out.println("namespace=" + namespace + " id= " + id); // 京東面試題:為啥xml里的id名要和介面名相同? // 因為mybatis底層就是namespace+id名作為key存儲sql語句的。 // 所以通過命名空間+id名就可以從xml中找到唯一的sql conf.getMapperStatement().put(sourceId, mapperStatement); System.out.println("初始化時放入工廠里的ms=" + sourceId); MapperStatement ms = conf.getMapperStatement().get(sourceId); System.out.println("初始化時從工廠里取出來的ms=" + ms); } } // 由這個sqlsessionfactory生成的sqlsession都會帶有配置信息 // 一個線程只有一個sqlsession public SqlSession openSession() { return new DefaultSqlSession(conf); }}
接著,我們就可以先調試一下程序,check一下目前為止的程序都是對的。生成一個sqlsessionfactory,然後獲得一個sqlsession。然後debug as java application,單點調試程序,查看sqlsessionfactory,sqlsession的成員變數(用滑鼠移到相應位置即可),看其是否正確注入資料庫信息和xml裡面的sql信息,然後查看裡面的hashmap成員變數的值(順便複習一下hashmap)。我的一切正常。從中可以看到sql語句裡面有/n /t,這些並不會影響,mysql會處理掉這些沒用的東西。
現在,第一階段完成,所有的配置信息均已載入。
在進入第二階段之前,先補充一些基礎知識。
sqlsession意味著創建資料庫會話,代表了一次與資料庫的連接。關係一:是mybatis對外提供數據訪問的主要API。
關係二:實際上sqlsession的功能都是基於executor實現的。
對於關係一:我們可以看到sqlsession的內部方法都非常完整,所以我們說資料庫方面是面向sqlsession編程。所以,對於外面的人來講,只知道sqlsession就可以了。對於裡面的人來說,才知道真正幹活的人是executor。
對於關係二:這個時候大家需要去看mybatis裡面的sqlsession的源碼,發現裡面所有的selectXXX的方法最後都是指向selectList方法,然後指向executor的query方法。而select方法則是直接指向executor的query方法。然後就是裡面所有的方法底層都是executor來提供的,包括事務的提交、回滾。所以大家可以理解關係二的存在了。
ibatis編程模型,不調用sqlsession的getmapper方法進行介面和sql語句的映射,而是直接調用sqlsession的selectXXX方法,把命名空間和id傳進去,然後得到返回。而mybatis是選擇面向介面編程,這樣操作起來更符合程序員的思維,而且傳參數不用把命名空間和id啥的都寫進去,這樣麻煩。但面向介面介面編程的方式的底層也是使用ibatis的方式去訪問資料庫的,這個需要翻譯。比如說
SqlSession session = MyBatisUtil.getSqlSession();
BlogMapper blogMapper = session.getMapper(BlogMapper.class);
Blog blog = blogMapper.selectByPrimaryKey(28);
最後都會轉化為session.selectOne(「namespace+id」,28);所以這就又mybatis對面向介面方式的封裝了,也就是說一個翻譯:
1.找到命名空間和方法名
2.然後找到session中對應的方法去執行,同時傳遞參數
這就需要解析之前的配置文件和動態代理增強實現介面的實現類。
阿里面試題:為啥使用mapper介面就能實現對資料庫的訪問?
答:也就是通過動態代理增強回調sqlsession里的方法(如selectlist,selectone之類的方法)。
接著,進入第二階段。
1.首先承接著第一階段後面生成的sqlsession,這裡由於是簡單實現mybatis,所以只寫出sqlsession的經典的幾個方法:
(整個第二階段都是圍繞著這三個方法來寫的)
<T> T selectOne(String statement, Object paramter);
<E> List<E> selectList(String statement, Object paramter);
<T> T getMapper(Class<T> type);
然後生成它的實現類DefaultSqlSession
由第一階段我們知道傳入sqlsession的有所有的配置信息,然後sqlsession的底層還要調用executor來實現,所以sqlsession有成員變數configuration和executor類型。由前面的理論基礎知道,sqlsession里的selectone方法是調用了自己的selectlist方法實現的,只是此時只返回一條數據而已,然後selectlist方法底層又是調用executor的query方法來執行的。所以,此時我們應該先新建一個executor介面,然後生成一個實現類,這裡只有一個query方法(簡便),然後暫時不操作資料庫,先列印從sqlsession里傳進來的configuration的mapperstatement的sql語句。這樣一來,sqlsession裡面的selectlist方法和selectone方法都被我們實現了。
2.寫sqlsession的getmapper方法。由我們之前的理論基礎知道,我們平常都是使用這個getmapper方法,就類似於
SqlSessionFactory factory = new SqlSessionFactory();
SqlSession session = factory.openSession();
BlogMapper blogMapper = session.getMapper(BlogMapper.class);
List<Blog> selectAll = blogMapper.selectAll();
這樣,也就是通過getmapper方法獲得blogmapper介面的實現類,而代碼中並沒有它的實現類。這就是getmapper要乾的事情。這裡是用到jdk的動態代理,在java反射包里的東西。
在getmapper方法中的代碼:
MapperProxy mp = new MapperProxy(this);
return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, mp);
首先看MapperProxy 類,它實現了InvocationHandler介面,它傳入了sqlsession進去。如果對動態反射熟悉的同學應該馬上反應過來那個介面是在反射包里的,然後重寫invoke方法。在這個類里就是實現一種關係,就是我們需要獲取這個介面的方法的返回值,然後根據返回的是否屬於集合類來調用sqlsession里的selectlist方法或者selectone方法(這就是我們前面所說的最終始終都是調用ibatis編程模型,google的開發人員只是把ibatis進行了封裝而已)。然後傳入參數method.getDeclaringClass().getName() + "." + method.getName(),這就是我們之前的namespace+id。也就是hashmap中的key.。然後在selectlist中根據key取出value的mapperstatement的sql語句,傳入executor的query中執行。所以總結一下就是這個invoke方法就是做了三件事:1.取出命名空間和方法名。2.找到sqlsession里對應的方法去執行。3.傳遞參數
然後看Proxy.newProxyInstance,它的第一個參數表示你生成的這個實現類要放在哪個的類載入器裡面,很明顯,要放在介面的類載入器裡面。第二個參數表示你的實現類要實現哪些介面,所以定義一個數組,數組有type介面。第三個參數就是實現了那個反射介面的實現類。這個類通過動態代理增強回調sqlsession裡面的方法。
所以,第二個階段(代理階段:封裝ibatis的編程模型,使用mapper介面開發初始化的工作。) 就這樣完成了。(微微鬆了一口氣)。此時就可以測試前面的代碼。最後觀察executor的query方法是否正確地輸出了sql語句。(由於我之前把xml文件里的namespace寫錯了,所以剛開始從hashmap中獲取的value值一直為空,就很尷尬,足可見這個namespace的重要性)
sqlsession源碼:
package com.enjoylearning.mybatis.session;import java.lang.reflect.Proxy;import java.util.List;import com.enjoylearning.mybatis.binding.MapperProxy;import com.enjoylearning.mybatis.config.Configuration;import com.enjoylearning.mybatis.config.MapperStatement;import com.enjoylearning.mybatis.executor.DefaultExecutor;import com.enjoylearning.mybatis.executor.Executor;//1.對外提供查詢介面,並把查詢請求轉發給executor//public class DefaultSqlSession implements SqlSession { private Configuration conf; private Executor executor; public DefaultSqlSession(Configuration conf) { this.conf = conf; executor = new DefaultExecutor(conf); } public <T> T selectOne(String statement, Object paramter) { List<Object> selectList = this.selectList(statement, paramter); if (selectList == null || selectList.size() == 0) { return null; } if (selectList.size() == 1) { return (T) selectList.get(0); } else { throw new RuntimeException("too many result"); } } public <E> List<E> selectList(String statement, Object paramter) { System.out.println("selectlist裡面的statement= " + statement); System.out.println("需要查詢的statement.hashCode()=" + statement.hashCode()); for (String key : conf.getMapperStatement().keySet()) { System.out.println("key=" + key + "key.hashcode=" + key.hashCode()); } MapperStatement ms = conf.getMapperStatement().get(statement); System.out.println("selectlist中從statement獲取出來的ms=" + ms); return executor.query(ms, paramter); } public <T> T getMapper(Class<T> type) { // 生成一個介面的實現類 第一個參數表示你生成的這個實現類要放在哪個的類載入器裡面,很明顯,要放在介面的類載入器裡面 // 第二個參數表示你的實現類要實現哪些介面,所以定義一個數組,數組有type介面 // 第三個參數就是實現了那個反射介面的實現類,這個類通過動態代理增強回調sqlsession裡面的方法 // 即MapperProxy實現了找到命名空間和id,然後找到相應的方法去執行,然後傳遞參數。 MapperProxy mp = new MapperProxy(this); return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, mp); }}
mapperproxy源碼:
package com.enjoylearning.mybatis.binding;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.util.Collection;import com.enjoylearning.mybatis.session.SqlSession;public class MapperProxy implements InvocationHandler { private SqlSession sqlSession; public MapperProxy(SqlSession sqlSession) { super(); this.sqlSession = sqlSession; } // 這裡的method就是blogmapper介面裡面的方法 , // 通過Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, // h)這個方法進行傳入 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 根據返回數據的個數去選擇調用sqlsession的哪個方法 Class<?> returnType = method.getReturnType();// 獲取它返回方法的類型 if (Collection.class.isAssignableFrom(returnType)) {// 看returnType的類型是不是collection的同類或者子類 // 如果是,就從方法中獲取命名空間中的包名+方法名作為mapperstatement, System.out.println("命名空間=" + method.getDeclaringClass().getName()); // 輸出com.enjoylearning.mybatis.mapper.BlogMapper System.out.println("方法名 =" + method.getName()); // 輸出selectAll // 如果args為null,則輸入參數null,否則就返回args的第一個參數。其實這個selectlist方法裡面就沒用到參數啊,不是selectall嗎? return sqlSession.selectList(method.getDeclaringClass().getName() + "." + method.getName(), args == null ? null : args[0]); } else { return sqlSession.selectOne(method.getDeclaringClass().getName() + "." + method.getName(), args == null ? null : args[0]); } }}
現在開始第三個階段,也就是最後一個階段:數據讀寫階段:通過sqlsession完成sql的解析,參數的映射,sql的執行,結果的解析過程。
在開始第三階段之前,我們需要回憶一下java通過jdbc規範訪問資料庫的一個基本的流程:
1.導入sql相關的包 java.sql.*;不是mysql的包
2.通過class.forname(com.mysql.jdbc.Driver)註冊mysql的驅動
3.通過驅動獲得一個連接(具體是conn=DriverManager.getConnection(DB_URL, USERNAME, PASSWORD)分別對應db.properities里的jdbc.url ,username,password)
4.通過連接創建一個查詢。(具體是stmt=conn.preparestatement(sql語句),然後如果要注入參數,通過stmt.setstring(1,」jackhe」))進行參數傳遞
5.執行查詢(具體是resultset rs=stmt.executeQuery())
6.把查詢出來的數據存儲到javabean裡面(具體就是while(rs.next){ blog blog=new blog();blog.setid=re.getint(「id」)} 類似於取出json中的數據)
7.關閉連接。(具體是rs.close;stmt.close;conn.close;)
然後,開始擼代碼。其實就是寫executor里的query方法。前面我們已經把所有需要的數據都準備好了,首先是executor自帶的configuration類,裡面有資料庫的基本信息和所有的mapper類的信息(也就是mapperstatement類),還有傳到sql語句中的參數。然後返回的是一個裝載著從資料庫返回的實體類的list。
然後開始:首先,載入mysql驅動,驅動從configuration裡面取,然後建立連接,接著創建查詢,這時需要一個專門的方法來處理參數傳遞的問題,然後執行查詢,然後需要一個專門的方法來把查詢結果轉換為pojo類的list返回出去。
處理參數傳遞的方法主要就是判斷傳進來的參數的類型,然後調用相應的方法去進行傳遞。
我的是stmt.setInt(1, Integer.parseInt(String.valueOf(paramter)));
視頻的方法是一個強制轉換,把參數對象強制轉換為基本數據類型,可是我試了好久,發現並不能這樣強制轉換,然後就用了上面這種方式,就是先把對象轉換為字元串,然後調用其它基本數據類型的對象的方法把把轉換為基本數據類型。然後調試ok。
把查詢結果轉換為pojo類的list返回出去的方法是利用了反射,主要就是先獲取類裡面全部的欄位,然後根據欄位從查詢回來的resultset中取出相應的屬性值(這裡可以用json的思想理解,其實就是個key-value)這裡封裝成了一個工具類。
然後寫一個測試類進行測試,結果除了個錯誤,java.sql.SQLException: Parameter index out of range (1 > number of parameters, which is 0). 原因就是我的xml文件中的sql語句傳遞參數時還是沿用了mybatis的方式,這裡應該用佔位符來傳遞參數。具體可參考
https://stackoverflow.com/questions/10896151/java-sql-sqlexception-parameter-index-out-of-range-1-number-of-parameters-wh/10898738#10898738
感覺stackoverflow上面的回答質量還是很高的。
executor源碼:
package com.enjoylearning.mybatis.executor;import java.sql.Connection;import java.sql.DriverManager;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException;import java.util.ArrayList;import java.util.List;import com.enjoylearning.mybatis.config.Configuration;import com.enjoylearning.mybatis.config.MapperStatement;import com.enjoylearning.mybatis.reflection.ReflectionUtils;public class DefaultExecutor implements Executor { private Configuration conf; public DefaultExecutor(Configuration conf) { super(); this.conf = conf; } public <E> List<E> query(MapperStatement ms, Object paramter) { List<E> ret = new ArrayList<E>(); try { Class.forName(conf.getJdbcDriver()); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = DriverManager.getConnection(conf.getJdbcUrl(), conf.getJdbcUsername(), conf.getJdbcPassword()); stmt = conn.prepareStatement(ms.getSql()); parameterize(stmt, paramter);// 參數傳遞的處理 rs = stmt.executeQuery(); handlerResultSet(rs, ret, ms.getResultType());// 處理查詢回來的數據轉換到list當中 } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { rs.close(); stmt.close(); conn.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return ret; } // 讀取resultset中的數據,並且轉換為目標對象 private <E> void handlerResultSet(ResultSet rs, List<E> ret, String className) { Class<?> clazz = null; try { clazz = Class.forName(className); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { while (rs.next()) { Object entity = clazz.newInstance(); ReflectionUtils.setPropToBeanFromResultSet(entity, rs); ret.add((E) entity); } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InstantiationException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } } private void parameterize(PreparedStatement stmt, Object paramter) throws NumberFormatException, SQLException { if (paramter instanceof Integer) { // 就沒看懂視頻里是怎麼把object的parameter變成(int)paramter的,這裡提示是 // 對象不能強制轉換為基本數據類型 stmt.setInt(1, Integer.parseInt(String.valueOf(paramter))); } else if (paramter instanceof Long) { stmt.setLong(1, Long.parseLong(String.valueOf(paramter))); } else if (paramter instanceof String) { stmt.setString(1, String.valueOf(paramter)); } }}
最後完美顯示結果。
結果如下:
其它的源碼就不在這裡進行展示了,怕文章太長了。文章最上面有全部源碼,親測可用。
感覺關鍵是記住那三個階段和這三個階段涉及到的類,然後把邏輯思想理清就很簡單了。
第一階段,載入配置信息,mapperstatement對象存儲xml文件裡面的sql語句信息,configuration存儲mysql的基本信息和用一個hashmap存儲所有的mapperstatement。所以就理解了這兩個類的關係。
第二階段,sqlsessionfactory裡面先把上面的信息載入完,然後把信息傳到sqlsession裡面,sqlsession里的方法都是調用executor的query方法,所以這裡又有了executor類。然後第二階段注意完成的是sqlsession的getmapper方法,完成對ibatis編程模型的封裝,利用動態代理的InvocationHandler介面實現邏輯關係,實現反向調用sqlsession的selectXXX方法。
第三階段就是詳寫executor的query方法,寫了一個反射工具類,實現了mysql的查詢。
後面打算專門寫個反射的文章,反射還是有必要好好記住api的。
歡迎交流討論。
推薦閱讀: