俗話說,自己寫的代碼,6個月后也是別人的代碼……復習!復習!復習!涉及的知識點總結如下:
- 開源框架的學習思路(個人總結)
- Hibernate的運行原理總結
- Hibernate實現原理中的兩個主要技術
- Java的反射技術的原理
- 反射的應用和例子
- 反射的缺點
- 編寫一個模擬Hibernate的demo(V1.0版本)
- 后續的模擬ORM框架的設計思路
開源框架的學習思路(個人經驗,歡迎提出意見)
框架是為了解決開發中遇到的一個個問題而誕生的,程序員是為了解決問題而學習框架的,這才是正確的學習之道!一個框架的好與壞完全取決於其對問題解決程度和解決方式。個人的學習過程:
- 了解框架能解決的問題(為什么使用這個框架,使用前后的差異),並先了解其實現原理(看悟性和基礎)
- 閱讀開源框架自帶的幫助文檔(結合一些參考書和網絡的搜索去理解自己不懂的地方,實在不行再問高手,否則學不會思考)
- 很重要的一步:必須親手搭建環境,可以運行開源框架自帶的示例項目,也可以自己寫一個demo,讓它跑起來!不能紙上談兵!
- 親自寫實際的Demo體驗,帶着問題研究開源框架的源代碼,並總結收獲。
- 創建配置文件,和實體類,並使用Configuration的對象去生成SessionFactory對象,(默認)加載hibernate.cfg.xml這個核心配置文件,該核心文件里包含了對數據源的連接和一些屬性的設置等,當然還包含有映射實體關系配置文件。
1 // 通過new一個Configuration實例,然后用該實例去調用configure返回一個配置實例 2 Configuration configuration = new Configuration().configure(); 3 // 通過 配置實例的buildSessionFactory方法 生成一個 sessionFactory 對象 4 // buildSessionFactory方法會默認的去尋找配置文件hibernate.cfg.xml並解析xml文件 5 // 解析完畢生成sessionFactory,負責連接數據庫 6 SessionFactory sessionFactory = configuration.buildSessionFactory();
2. 生成的SessionFactory,等於是可以獲取數據庫的連接(能創建session),從而可以操作數據庫
3. 因為核心配置Hibernate.cfg.xml里引入了實體關系映射配置文件,故該文件也會自動被解析——加載對象-關系映射文件:vo類.hbm.xml
4、然后是創建session對象,通過SessionFactory創建session。session可以操作數據庫
// 通過 sessionFactory 獲得一個數據庫連接 session,可以操作數據庫 Session session = sessionFactory.openSession();
5. 開啟事務,也是通過session開啟
// 把操作封裝到數據庫的事務,則需要開啟一個事務 Transaction transaction = session.beginTransaction();
6. 調用session API,CRUD 對象
// 一般把對實體類和數據庫的操作,放到try-catch-finally塊 try { User user = new User(); user.setUserId(22); user.setUsername("dashuai"); user.setPassword("123456"); // 把user對象插入到數據庫 session.save(user); // 提交操作事務 transaction.commit(); LOG.info("transaction.commit(); ok"); } catch (Exception e) { // 提交事務失敗,必須要回滾 transaction.rollback(); // 打印日志 LOG.error("save user error......", e); } finally { // 不能丟這一步,要釋放資源 if (session != null) { session.close(); LOG.info("session.close(); ok"); } }
7. 根據Dialect(之前在核心配置文件配置的數據庫方言)生成和底層數據庫平台相關的sql代碼
Hibernate實現原理中使用的技術有什么?
針對主流的XML文件配置方式,Hibernate實現原理中使用的關鍵技術主要有兩個。一是對XML文檔的解析——使用DOM(文檔對象模型)/SAX解析,Hibernate使用了常見的開源解析工具——dom4j(使用Java編寫,很流行),二是Java的反射技術,比如我可以通過一個Java類的對象,通過反射機制來獲取這個對象的類的屬性,方法……簡單說,就類似我自己照鏡子,通過鏡子,我可以看清楚我自己身體的各個部位。
當然了,還有基於注解的方式,那么就還要使用Java的注解技術,本質上大同小異,熟能生巧。
Java反射技術淺析
大白話就是:Java反射機制可以讓程序員在程序的運行期(Runtime)檢查類,接口,變量以及方法的信息,而檢查Java類的信息往往是在使用Java反射機制的時候所做的第一件事情,通過獲取類的信息可以獲取以下相關的內容:Class對象,類名,修飾符,包信息,父類,實現的接口,構造器,方法,變量,注解……除了這些內容,還有很多的信息可以通過反射機制獲得(查閱API即可)。進一步反射還可以讓程序員在運行期實例化對象,調用類的方法,通過調用get/set方法獲取變量的值等,所以,Java的反射機制功能非常強大而且非常實用。舉個例子,我可以用反射機制把Java對象映射到數據庫表(Hibernate的實現機制之一),或者把腳本中的一段語句在運行期映射到相應的對象調用方法上,就如解析配置腳本時所做的那樣。
Java反射機制的原理
這涉及到了Java的類加載機制和原理,稍后會專題總結。這里簡單說下,在說原理之前,必須先知道Java中一般經常用Class.forName(classname)來反射類。在之前的幾篇學習JVM總結隨筆中也有部分說到:JVM裝載某類時,類裝載器會定位相應的class文件,然后將其讀入到虛擬機中,並提取class中的類型信息,而Java中類的信息一般我們認為是存儲到JVM的方法區中了。
Java反射機制中涉及的類:
- Class:類的實例,表示正在運行的 Java 應用程序中的類和接口,這個是反射機制中最關鍵的一點,可以使用Object類的getClass()方法,Class類的getSuperClass()方法,Class類的靜態forName()方法,對於包裝器類型,通過類名.TYPE屬性得到類的Class類型信息。因為Class 類十分特殊,其實例用以表達Java程序運行時的類和接口。
- Field:提供有關類或接口的屬性的信息,以及對它的動態訪問權限
- Constructor:提供關於類的單個構造方法的信息以及對它的訪問權限
- Method:提供關於類或接口上單獨某個方法的信息
看個demo,新建一個類:dashuai.generics.Dog。該類做為我們的實驗類,通過反射機制創建該類的對象,並通過反射機制調用該類中的speak方法。
1 public class Dog{ 2 public void speak(String str) { 3 System.out.println("Dog speak! 汪汪" + str); 4 } 5 }
在main方法里通過反射機制創建Dog類的對象,並調用其方法
public class Main { public static void main(String[] args) { Object obj = null; try { Class clazz = Class.forName("dashuai.generics.Dog"); obj = clazz.newInstance(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } Dog dog = (Dog) obj; dog.speak(”Hi”); } }
上面代碼中,dog對象是通過Class類的forName方法創建的,再調用該對象的speak方法,在控制台打印一行字符串。注釋上面代碼最后一行:dog.speak(),我使用反射來調用dog對象的speak()方法。該類完整代碼如下:
1 public class Main { 2 public static void main(String[] args) { 3 Object obj = null; 4 5 try { 6 Class clazz = Class.forName("dashuai.generics.Dog"); 7 obj = clazz.newInstance(); 8 } catch (ClassNotFoundException e) { 9 e.printStackTrace(); 10 } catch (InstantiationException e) { 11 e.printStackTrace(); 12 } catch (IllegalAccessException e) { 13 e.printStackTrace(); 14 } 15 16 Dog dog = (Dog) obj; 17 Class<?>[] parameterTypes = new Class<?>[1]; 18 parameterTypes[0] = String.class; 19 20 try { 21 Method method = dog.getClass().getDeclaredMethod("speak", parameterTypes); 22 method.invoke(dog, new Object[] { "Hi" }); 23 } catch (SecurityException e1) { 24 e1.printStackTrace(); 25 } catch (NoSuchMethodException e1) { 26 e1.printStackTrace(); 27 } catch (IllegalArgumentException e) { 28 e.printStackTrace(); 29 } catch (IllegalAccessException e) { 30 e.printStackTrace(); 31 } catch (InvocationTargetException e) { 32 e.printStackTrace(); 33 } 34 } 35 }
通過java.lang.reflect.Method類來構建方法,再通過invoke方法執行dog對象的speak方法。
簡單說說反射的執行過程
JVM裝載類的目的就是把Java字節代碼轉換成JVM中的java.lang.Class類的對象。這樣Java就可以對該對象進行一系列操作,而上面的例子:Class.forName(classname)方法,實際上是調用了Class類中的 Class.forName(classname, true, currentLoader)方法。參數:name - 所需類的完全限定名;initialize - 是否必須初始化類;loader - 用於加載類的類加載器。currentLoader則是通過調用ClassLoader.getCallerClassLoader()獲取當前類加載器的。類要想使用,必須用類加載器加載,所以需要加載器。
還有一點:反射機制不是每次都去重新反射,而是提供了緩存,每次都需要類加載器去自己的緩存中查找,如果可以查到,則直接返回該類。Java類加載器大體分兩類:前三者是一類,分為BootStrap Class Loader(引導類加載器),Extensions Class Loader (擴展類加載器),App ClassLoader(或System Class Loader系統類加載器),最后一個是另一類叫用戶自定義類加載器。
類的加載過程有兩個比較重要的特征:層次組織結構和代理模式。
層次組織結構指的是每個類加載器都有一個父類加載器(除了引導類加載器之外),通過getParent()方法可以獲取到。類加載器通過這種父親-后代的方式組織在一起,形成樹狀層次結構。系統類加載器的父類加載器是擴展類加載器,而擴展類加載器的父類加載器是引導類加載器,對於開發人員編寫的類加載器來說,其父類加載器是加載此類加載器 Java 類的類加載器。因為類加載器 Java 類如同其它的 Java 類一樣,也是要由類加載器來加載的。一般來說,開發人員編寫的類加載器的父類加載器是系統類加載器。類加載器通過這種方式組織起來,形成樹狀結構。樹的根節點就是引導類加載器。如圖:

代理模式則指的是一個類加載器既可以自己完成Java類的定義工作,也可以代理給其它的類加載器來完成。由於代理模式的存在,啟動一個類的加載過程的類加載器和最終定義這個類的類加載器可能並不是一個。
Java類的加載過程:
1.通過類的全名產生對應類的二進制數據流。(如果沒找到對應類文件,只有在類實際使用時才拋出錯誤。)
2.分析並將這些二進制數據流轉換為方法區特定的數據結構(這些數據結構是實現有關的,不同 JVM 有不同實現)。這里處理了部分檢驗,比如類文件的魔數的驗證,檢查文件是否過長或者過短,確定是否有父類(除了 Obecjt 類)。
3.創建對應類的 java.lang.Class 實例(注意,有了對應的 Class 實例,並不意味着這個類已經完成了加載!)。
而JVM在整個加載過程中,會先檢查類是否被已加載,檢查順序是自底向上,從系統類加載器到引導類加載器逐層檢查,只要某個classloader已加載就視為已加載此類,保證此類只被所有ClassLoader加載一次。
但是加載的順序是自頂向下(和檢測順序反着,屬於父類優先的順序),也就是由上層來逐層嘗試加載此類。類加載器的詳細介紹后續專題總結。
只說一點,ClassLoader的加載類過程主要使用loadClass方法,該方法中封裝了加載機制:雙親委派模式。在forName方法中,就是調用了ClassLoader.loadClass方法來完成類的反射的,正如前面說的,JVM先檢查自己是否已經加載過該類,如果加載過,則直接返回該類,若沒有則調用父類的loadClass方法,如果父類中沒有,則執行findClass方法去嘗試加載此類,也就是我們通常所理解的片面的"反射"了。
這個過程主要通過ClassLoader.defineClass方法來完成。defineClass 方法將一個字節數組轉換為 Class 類的實例(任何類都是Class類的對象,在Java中,每個class都有一個相應的Class對象,也就是說,當我們編寫一個類.java文件,編譯完成后,在生成的.class文件中,就會產生一個Class對象,用於表示這個類的類型信息,既一切皆是對象)。這種新定義的類的實例需要使用 Class.newInstance 來創建,而不能使用new來實例化。
大白話:運行期間,如果我們要產生某個類的對象,JVM 會檢查該類型的Class對象是否已被加載。如果沒有被加載,JVM會根據類的名稱找到.class文件並加載它。一旦某個類型的Class對象已被加載到內存,就可以用它來產生該類型的所有對象。
再ps點:類加載器的用途
類加載器除了加載類信息,獲取類信息之外,還有一個重要用途是在JVM中為相同名稱的Java類創建隔離空間。在JVM中,判斷兩個類是否相同,不僅是根據該類的二進制名稱,還需要根據兩個類的定義類加載器。只有兩者完全一樣,才認為兩個類的是相同的。因此,即便是同樣的Java字節代碼,被兩個不同的類加載器定義之后,所得到的Java類也是不同的。如果試圖在兩個類的對象之間進行賦值操作,會拋出java.lang.ClassCastException。這個特性為同樣名稱的Java類在JVM中共存創造了條件。在實際的應用中,可能會要求同一名稱的Java類的不同版本在JVM中可以同時存在。通過類加載器就可以滿足這種需求。這種技術在OSGi中得到了廣泛的應用。
反射的應用
- 操作數據庫,動態創建SQL語句
- 解析XML,properties配置文件,動態生成對象
- Java的動態代理
- 框架中使用的最多:Struts框架、Spring框架、Hibernate框架、MyBatis框架……
反射的缺點
- 仿照Hibernate,也在項目根下定義個xml配置文件,我的叫Students.xml文件
- 再定義一個實體類Students,對照數據庫生成對應的字段和set,get方法。
- 創建一個Session接口,其中使用dom4j解析xml配置文件,解析的數據保存到變量中,核心是解析讀取類的對象的屬性和二維表的字段的對應關系,我使用map對象保存。之后讀取map對象,通過字符串的拼接等技巧,拼接一個插入數據到表的SQL語句—— insert into students(sname,sid) values (?,?)
- 編寫一個save方法,參數傳入需要保存的對象,比如student,這里面使用反射技術得到對象的類信息,再通過之前解析配置文件而得到的get方法名集合,利用反射得出get的返回類型,之后通過返回類型的判斷結果,得出我們需要插入到數據庫的數據是什么,再利用反射調用get方法,得到恰當的數據,依靠jdbc把數據保存到數據庫。這里本質是利用反射對jdbc進行封裝,這里當然是比較基礎的封裝,沒有涉及復雜的功能和其他可能的映射情景。
- 使用了log4j記錄日志
2016-03-08 22:28:51,424 | INFO | main | dao.Session.save(Session.java:188) | save
2016-03-08 22:28:51,428 | INFO | main | dao.Session.save(Session.java:190) | SQL: insert into students(sname,sid) values (?,?)
Process finished with exit code 0
打算一步步在總結框架的時候完善和重構一個能用的ORM框架。
設計思路:
第一點:連接數據庫。第一種是JDBC連接,第二種是采用數據源來連接(采用數據源連接的時候,可以采用任何的數據源,c3p0,dbcp。)。
第二點:操作數據:添加數據,刪除數據,修改數據。
第三點:查詢數據。
開發思路:
- 當JVM啟動的時候,首先讀取配置文件(讀取是一個效率很低的過程,需要考慮性能,避免重復讀取)。解析的數據變量是一個Map,Map中包含的數據類型,不是簡單的數據類型,而是對配置信息進行了封裝以后的對象。當讀取了配置文件之后,我就會知道是否配置了數據源,如果配置了數據源,那么就把配置的參數獲取到,然后實例化,在得到數據庫連接的時候,采用數據源來獲取連接。如果沒有配置數據源,那么就采用jdbc來連接。當兩種配置都存在的時候,肯定是采用數據源來連接。
- save(Object o):插入數據,insert into 表名(字段1,字段2,……) values(值1,值2,……),save方法當中,必須拼接一條SQL語句,然后放到數據庫中執行。
問題:拼接SQL語句的時候,表名從哪里來?字段從哪里來?值從哪里來?
<class name=”User” table=”user”>,name就代表了user這張表。table標簽對應的值,就是表名。我們在配置中,會對每一個字段進行配置,那么我當然可以取到字段的名字。最重要的是,值是怎么來的。前面說了,利用Java所提供的反射機制來獲取
- update,del更新,刪除,這兩個方法與上面的save方法類似。update方法的SQL語句是:update 表名 set 字段1=值1, 字段2=值2 where 條件,del對應的SQL語句:delete from 表名 where 條件。
問題:如果一個表中有4個字段,我只需修改1個字段,那么在修改的時候,只是針對於這一個字段給實體對象賦值,這個對象的其他的字段屬性,都是null,這個時候,怎么樣才能夠只修改對應的字段?
- 查詢數據:查詢是最困難的!
拼接一條SQL語句,比如說:form User。但是,這條語句數據庫是不認識的。數據庫認識的是這樣的:select * from 表名。當然,我也可以添加一些條件。把拼接好的SQL語句,放到關系數據庫中取執行,得到的是結果集:ResultSet。這個方法,返回給用戶的是一個List,是一個直接可以使用的列表,但是這個列表中會有很多很多的對象,每一個對象,又都有對應的值。當拿到結果集以后,遍歷結果集,然后根據上下文(比如表),把查詢出來的值,利用反射的方法設置到對象中,再把對象添加到列表中,最后返回列表。
這個方法中有很多的細節需要處理:比如說,當數據庫中的表,不是單一的表,是有連接關系的時候,拼接SQL語句會比較麻煩,而且在添加數據到列表中的時候,需要進行的處理也會特別的多。還有條件查詢的情景……
- 分頁的處理。
- 查詢的默認配置選擇(無連接的查詢),懶加載的設置。
- 緩存的設計(也是一個難點):想到可以使用靜態的map變量來模擬緩存功能,利用多線程控制緩存,還想到Java的軟引用是不是也能實現ORM的緩存?
- 要復合OOP的原則,盡量的抽象,抽象的層次越高,復用性就越強。
- 對一些設計模式的應用,比如單例模式,動態代理,享元模式,工廠模式,命令模式等。
