MyBatis 框架的自定義——手撕程式碼硬剛框架(從配置檔案到sqlsession)
Mybatis的底層
在自定義前,我們先來掌握一下MyBatis的底層流程。
首先我們分析一下這段程式碼:
這個方法是怎麼運作的呢?建立代理物件用Proxy類的newProxyInstance方法即可建立,三個引數分別為:類載入器,代理物件要實現的介面位元組碼陣列,如何代理
我們要在哪個引數做文章呢?明顯是第三個,如何代理。
究其本質,MyBatis的底層是如何執行的呢,不外乎就是解析xml配置,獲取驅動註冊連線,生成jdbc的那幾個connection什麼的類,完成連線執行查詢。然後建立dao的代理物件 最後由代理物件執行selectlist方法返回List<User>結果。
MyBatis的自定義
既然我們要自定義,我們就要自定義之前案例所有我們能看到的類:
class Resources
class SqlSessionBuilder
interface SqlSessionFactory
interface SqlSession
在第一個專案的基礎上,現在我們把mybatis給刪了,刪完之後相當於沒有mybatis那一套內容了
可以看到我們test裡面的類都不能用了
首先來創我們的Resources
寫完之後就可以在test裡導包了。
然後我們設計getResourceAsStream方法,返回值是InputStream。
package com.itheima.mybatis.io; import java.io.InputStream; /** * 使用類載入器讀取配置檔案的 類 */ public class Resources { /** * 根據傳入的引數,獲取一個位元組輸入流 * @param filePath * @return */ public static InputStream getResourceAsStream(String filePath) { return Resources.class.getClassLoader().getResourceAsStream(filePath);//三部走:拿到當前類的位元組碼,獲取位元組碼的類載入器,根據類和關鍵環節很快就可憐見,載入器讀取配置 } }
現在我們創SqlSessionFactoryBuilder類和SqlSessionFactory介面,前者的功能是建立一個sqlsessionfactory物件。
現在test裡面關於這兩個東西就不會報錯了
順便我們加入SqlSession介面
並加入兩個方法
至此,test類的程式碼全部不報錯了。
現在將配置檔案有關mybatis的部分刪去
現在我們在mybatis目錄下創一個utils包,在包下創一個XMLConfigBuilder類,並拷下面的程式碼
package com.itheima.mybatis.utils; import com.itheima.mybatis.annotations.Select; import com.itheima.mybatis.io.Resources; import com.itheima.mybatis.sqlsession.Configuration; import com.itheima.mybatis.sqlsession.defaults.DefaultSqlSession; import com.itheima.mybatis.sqlsession.mappers.Mapper; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author 黑馬程式設計師 * @Company http://www.ithiema.com * 用於解析配置檔案 */ public class XMLConfigBuilder { /** * 解析主配置檔案,把裡面的內容填充到DefaultSqlSession所需要的地方 * 使用的技術: * dom4j+xpath */ public static Configuration loadConfiguration(InputStream config){ try{ //定義封裝連線資訊的配置物件(mybatis的配置物件) Configuration cfg = new Configuration(); //1.獲取SAXReader物件 SAXReader reader = new SAXReader(); //2.根據位元組輸入流獲取Document物件 Document document = reader.read(config); //3.獲取根節點 Element root = document.getRootElement(); //4.使用xpath中選擇指定節點的方式,獲取所有property節點 List<Element> propertyElements = root.selectNodes("//property"); //5.遍歷節點 for(Element propertyElement : propertyElements){ //判斷節點是連線資料庫的哪部分資訊 //取出name屬性的值 String name = propertyElement.attributeValue("name"); if("driver".equals(name)){ //表示驅動 //獲取property標籤value屬性的值 String driver = propertyElement.attributeValue("value"); cfg.setDriver(driver); } if("url".equals(name)){ //表示連線字串 //獲取property標籤value屬性的值 String url = propertyElement.attributeValue("value"); cfg.setUrl(url); } if("username".equals(name)){ //表示使用者名稱 //獲取property標籤value屬性的值 String username = propertyElement.attributeValue("value"); cfg.setUsername(username); } if("password".equals(name)){ //表示密碼 //獲取property標籤value屬性的值 String password = propertyElement.attributeValue("value"); cfg.setPassword(password); } } //取出mappers中的所有mapper標籤,判斷他們使用了resource還是class屬性 List<Element> mapperElements = root.selectNodes("//mappers/mapper"); //遍歷集合 for(Element mapperElement : mapperElements){ //判斷mapperElement使用的是哪個屬性 Attribute attribute = mapperElement.attribute("resource"); if(attribute != null){ System.out.println("使用的是XML"); //表示有resource屬性,用的是XML //取出屬性的值 String mapperPath = attribute.getValue();//獲取屬性的值"com/itheima/dao/IUserDao.xml" //把對映配置檔案的內容獲取出來,封裝成一個map Map<String,Mapper> mappers = loadMapperConfiguration(mapperPath); //給configuration中的mappers賦值 cfg.setMappers(mappers); }else{ System.out.println("使用的是註解"); //表示沒有resource屬性,用的是註解 //獲取class屬性的值 String daoClassPath = mapperElement.attributeValue("class"); //根據daoClassPath獲取封裝的必要資訊 Map<String,Mapper> mappers = loadMapperAnnotation(daoClassPath); //給configuration中的mappers賦值 cfg.setMappers(mappers); } } //返回Configuration return cfg; }catch(Exception e){ throw new RuntimeException(e); }finally{ try { config.close(); }catch(Exception e){ e.printStackTrace(); } } } /** * 根據傳入的引數,解析XML,並且封裝到Map中 * @param mapperPath 對映配置檔案的位置 * @return map中包含了獲取的唯一標識(key是由dao的全限定類名和方法名組成) * 以及執行所需的必要資訊(value是一個Mapper物件,裡面存放的是執行的SQL語句和要封裝的實體類全限定類名) */ private static Map<String,Mapper> loadMapperConfiguration(String mapperPath)throws IOException { InputStream in = null; try{ //定義返回值物件 Map<String,Mapper> mappers = new HashMap<String,Mapper>(); //1.根據路徑獲取位元組輸入流 in = Resources.getResourceAsStream(mapperPath); //2.根據位元組輸入流獲取Document物件 SAXReader reader = new SAXReader(); Document document = reader.read(in); //3.獲取根節點 Element root = document.getRootElement(); //4.獲取根節點的namespace屬性取值 String namespace = root.attributeValue("namespace");//是組成map中key的部分 //5.獲取所有的select節點 List<Element> selectElements = root.selectNodes("//select"); //6.遍歷select節點集合 for(Element selectElement : selectElements){ //取出id屬性的值 組成map中key的部分 String id = selectElement.attributeValue("id"); //取出resultType屬性的值 組成map中value的部分 String resultType = selectElement.attributeValue("resultType"); //取出文字內容 組成map中value的部分 String queryString = selectElement.getText(); //建立Key String key = namespace+"."+id; //建立Value Mapper mapper = new Mapper(); mapper.setQueryString(queryString); mapper.setResultType(resultType); //把key和value存入mappers中 mappers.put(key,mapper); } return mappers; }catch(Exception e){ throw new RuntimeException(e); }finally{ in.close(); } } /** * 根據傳入的引數,得到dao中所有被select註解標註的方法。 * 根據方法名稱和類名,以及方法上註解value屬性的值,組成Mapper的必要資訊 * @param daoClassPath * @return */ private static Map<String,Mapper> loadMapperAnnotation(String daoClassPath)throws Exception{ //定義返回值物件 Map<String,Mapper> mappers = new HashMap<String, Mapper>(); //1.得到dao介面的位元組碼物件 Class daoClass = Class.forName(daoClassPath); //2.得到dao介面中的方法陣列 Method[] methods = daoClass.getMethods(); //3.遍歷Method陣列 for(Method method : methods){ //取出每一個方法,判斷是否有select註解 boolean isAnnotated = method.isAnnotationPresent(Select.class); if(isAnnotated){ //建立Mapper物件 Mapper mapper = new Mapper(); //取出註解的value屬性值 Select selectAnno = method.getAnnotation(Select.class); String queryString = selectAnno.value(); mapper.setQueryString(queryString); //獲取當前方法的返回值,還要求必須帶有泛型資訊 Type type = method.getGenericReturnType();//List<User> //判斷type是不是引數化的型別 if(type instanceof ParameterizedType){ //強轉 ParameterizedType ptype = (ParameterizedType)type; //得到引數化型別中的實際型別引數 Type[] types = ptype.getActualTypeArguments(); //取出第一個 Class domainClass = (Class)types[0]; //獲取domainClass的類名 String resultType = domainClass.getName(); //給Mapper賦值 mapper.setResultType(resultType); } //組裝key的資訊 //獲取方法的名稱 String methodName = method.getName(); String className = method.getDeclaringClass().getName(); String key = className+"."+methodName; //給map賦值 mappers.put(key,mapper); } } return mappers; } }XMLConfigBuilder
這裡用到的是jom4j+xpath解析xml,於是我們來到全域性配置檔案把dom4和jaxenj依賴導進去,
然後我們在itheima目錄下創一個cfg目錄,裡面創我們的Configuration類
定義我們連線的四個重要資訊
生成他們的getset方法後,我們在cfg目錄下設計一個mapper類。
mapper我們分析過,mapper包含兩部分,分別為執行的SQL語句和封裝結果的實體類全限定類名。
我們回到XMLConfigBuilder,可以看到loadMapperConfiguration和loadMapperAnnotation,他們分別是用xml文件配置和註解配置兩種方法。本階段不涉及註解,我們把註解的部分註釋掉。
現在我們重點看XMLConfigBuilder類,在取出mappers中的所有mapper標籤後,會根據mapperPath把對映配置檔案的內容獲取出來,封裝成一個map。
之前我們提到過map裡key值是namespace+id,value是一個mapper物件,包含sql查詢語句和結果型別的全限定類名。
現在我們來寫Configuration的setMappers方法。我們在Configuration里加一個mappers成員變數。
然後新增set和get方法,我們發現set方法是用=的方式賦值,那如果我有兩個mappers,迴圈第二遍就會覆蓋原來的mappers了。
顯然這是不對的,於是Configuration的set方法不能使用直接賦值的方式,要寫成這樣。
然後我們把XMLConfigBuilder裡導包的刪掉,註解的部分註釋掉。
現在我們來寫SqlSessionFactoryBuilder裡的build方法,在此之前我們建立一個SqlSessionFactory的實現類。
我們回到SqlSessionFactoryBuilder,我們首先建立一個Configuration,然後例項化一下我們的實現類。把cfg作為引數傳給這個實現類。
現在我們再來修改下DefaultSqlSessionFactory。
再創一個sqlsession的實現類。
再到sqlsessionfactory裡的方法改一改
這樣的話我們現在的類都有了對應的關係——配置檔案根據Resources類拿到io流傳給構建者,構建者通過工具類(Configuration)建立工廠,工廠提供opensession方法建立sqlsession類。