Java 高級特性之使用反射實現(xiàn)萬能序列化
很多時候我們需要將一個類的實例變成二進制數(shù)據(jù)存儲或是通過網(wǎng)絡發(fā)送,這個過程叫序列化。如果將二進制數(shù)據(jù)解析成位于內(nèi)存中的類實例或是相關數(shù)據(jù)結構,那叫反序列化。所有的序列化算法都遵循一定的套路,例如:
- class A {
- public int a = 1;
- public int b = 2;
- protected B b = new B();
- private float c = 3.0;
- }
如果要序列化類A的實例,那么通常需要將變量a,b的數(shù)值對應的二進制數(shù)寫入,然后獲得類B實例序列化后的二進制數(shù)據(jù),最后將變量c的數(shù)值的二進制數(shù)據(jù),這里可以體會到,序列化其實有一種遞歸的性質,在序列化過程中如果遇到的是基礎類型,那么可以直接獲取其對應的二進制數(shù)據(jù),如果遇到類實例,那么需要先序列化它,取得對應二進制數(shù)據(jù)。
而序列化過程中需要你了解對應類的定義,但如果我們不知道要序列化的對象,例如我們看不到類A的定義,我們只拿到了A對應的一個實例對象,那此時怎么序列化呢。這就需要用到java語言的反射特性,java編譯器在編譯類A時,不僅僅將它為它的各個字段分配了內(nèi)存,而且還為類A的相關信息進行了設置和存儲,例如A里面有多少字段,字段的類型是int, float, stirng,還是特定類對象,這些信息都一并設置并存儲了起來,只要我們使用java反射提供的API就能獲得這些信息,從而就能對任意類實現(xiàn)序列化。
因此序列化的萬能套路是:
1,獲得要序列化的類實例;
2,獲得類中各個字段的屬性,類型等相關信息。
3,如果字段屬于基礎數(shù)據(jù),那么獲得其數(shù)值的二進制數(shù)據(jù)。
4,如果對應字段是一個類實例,那么先遞歸的序列化該實例
根據(jù)以上步驟,當我們需要序列化任意一個類實例時,首先通過getClass獲得其對應的Class類實例,然后調用getDeclaredFields()接口獲得該實例所有的字段,其中包括public,protected,private,或者調用getFields()獲得類實例聲明或繼承的公有字段。在序列化中,我們不能忘了序列化當前類實例的父類,因此可以調用getSuperClass()來獲得當前實例的父類,這個過程會不斷進行直到抵達根類為止。
每個字段都會對應一個元類叫Field,通過該類相關接口能獲得字段的值。獲取字段的數(shù)據(jù)首先需要確定字段的類型,如果是Boolean類型,那么可以調用Field類的getBoolean接口獲得數(shù)據(jù),如果是int類型,那么可以通過getInteger()接口獲得數(shù)據(jù),如果字段是類對象,那么就得遞歸的去獲得其二進制數(shù)據(jù),如果字段是基礎類型,那么通過調用其getString()就能獲得其數(shù)值的字符串形式。
在獲取字段類型前,我們還需要知道字段的修飾屬性,例如是public還是private,是不是static等,這些屬性通過Field類的接口getModifier()獲得,調用它會返回一個整形值,該值在相關比特位上設置1或0來表示修飾屬性。在java語法中共有11種修飾屬性,因此有11個比特位來對應,但我們不需要分析哪個比特位設置為1來獲取字段屬性,java反射提供了一個特定類Modifier,通過getModifier返回的數(shù)值可以輸入Modifier類的isPublic, isPrivate等接口來查詢字段對應的修飾屬性。
在序列化時,我們要忽略掉static屬性的字段,因此他們是寫死的,因此通過Modifier.isStatic(field.getModifier())所得結果就能進行字段的static屬性判斷??偹苤?,對于protected 或是private類型的字段,外部是不能直接讀取的,但是序列化必須要能讀取這類字段的值,要不然序列化就無法進行,F(xiàn)ield類提供了setAccessible(true)來打破這個限制。
此外還需要考慮的一個因素是,如果字段是數(shù)組類型的情況。java反射提供了元類Array來應對,假設實例對象obj是一個數(shù)組,那么Array.getLength(obj)就能獲得數(shù)組的長度,Array.get(obj, i)就能獲得第i個元素對象。
最后我們需要考慮序列化后的文件格式,我們使用xml格式來存儲序列化的結果,例如在上面例子中,字段a在序列化后對應為”\1\“,具體的情況我們在后續(xù)代碼中慢慢來觀察。
首先我們使用IntelliJ 創(chuàng)建一個maven項目,由于我們需要將數(shù)據(jù)序列化成XML文件,因此需要使用JDOM接口,于是在pom.xml中添加如下依賴:
- <!-- https://mvnrepository.com/artifact/org.jdom/jdom -->
- <dependency>
- <groupId>org.jdom</groupId>
- <artifactId>jdom</artifactId>
- <version>2.0.2</version>
- </dependency>
然后點擊一下Maven命令面板的下載按鈕下載jdom包。然后我們創(chuàng)建一個實現(xiàn)文件叫ReflectionSerilization,然后先完成一些骨架基礎:
- import org.jdom2.Document;
- import org.jdom2.Element;
- import org.jdom2.output.XMLOutputter;
- import java.lang.reflect.*;
- import java.util.IdentityHashMap;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Properties;
- import java.util.Map;
- public class ReflectionSerialization {
- public Document doSerilizeObject(Object objectToSerilized) throws Exception{
- return recursiveSerilizeObject(objectToSerilized, new Document(new Element("serialized")),
- new IdentityHashMap());
- }
- private Document recursiveSerilizeObject(Object objToSerilized, Document target, Map table) throws Exception{
- String id = Integer.toString(table.size()); //為當前序列化的對象設置id標號
- table.put(objToSerilized, id);
- Class objClass = objToSerilized.getClass();
- Element elem = new Element("object");
- elem.setAttribute("class", objClass.getName());
- elem.setAttribute("id", id);
- target.getRootElement().addContent(elem);
- /*
- 判斷當前要序列化的對象是否是數(shù)組類型,如果不是,那么先遍歷該對象所有字段,然后遞歸的序列化對應字段,因為字段有可能是類對象,
- 如果是數(shù)組類型,那么遍歷其中每個元素,然后針對每個元素進行序列化
- */
- if (objClass.isArray()) {
- //TODO
- } else {
- //TODO
- }
- }
- public static void main(String[] args) {
- }
- }
接下來我們針對兩個TODO進行實現(xiàn),如果當前要序列化的對象不是數(shù)組,那么需要遍歷其所有字段,然后序列化各個字段,如果字段是類對象類型,那么還得遞歸的對他進行處理,我們看代碼實現(xiàn):
- /*
- 判斷當前要序列化的對象是否是數(shù)組類型,如果不是,那么先遍歷該對象所有字段,然后遞歸的序列化對應字段,因為字段有可能是類對象,
- 如果是數(shù)組類型,那么遍歷其中每個元素,然后針對每個元素進行序列化
- */
- if (objClass.isArray()) {
- handleNoArrayField(objToSerilized, objClass, target, table, elem);
- } else {
- //TODO
- }
- private void handleNoArrayField(Object objToSeerilized, Class cls, Document target, Map table, Element parent) throws Exception {
- Field[] fields = this.iterateClassFields(cls);
- for (int i = 0; i < fields.length; i++) {
- if (!Modifier.isPublic(fields[i].getModifiers())) {
- fields[i].setAccessible(true); //如果不是公有字段,那么需要設置它的可讀取性
- }
- Element fElt = new Element("field"); //針對該字段插入xml元素
- fElt.setAttribute("name", fields[i].getName());
- Class declClass = fields[i].getDeclaringClass(); //獲取字段對應的類
- fElt.setAttribute("declaringclass", declClass.getName());
- Class fieldType = fields[i].getType(); //獲得該字段類型對應的元類
- Object child = fields[i].get(objToSeerilized); //獲得字段對應的實例對象
- if (Modifier.isTransient(fields[i].getModifiers())) {
- child = null;
- }
- fElt.addContent(extractContentFromField(fieldType, child, target, table));
- parent.addContent(fElt);
- }
- }
- private Element extractContentFromField(Class fieldType, Object child, Document target, Map table) throws Exception{
- //將字段對應的數(shù)據(jù)抽取出來
- if (child == null) {
- return new Element("null");
- }
- else if (!fieldType.isPrimitive()) {
- Element reference = new Element("reference");
- if (table.containsKey(child)) {
- reference.setText(table.get(child).toString()); //任何基礎類型都繼承自Object,他們都支持toString來將自身對應的數(shù)據(jù)進行字符串表達
- }
- else {
- reference.setText(Integer.toString(table.size()));
- recursiveSerilizeObject(child, target, table); //如果不是基礎類型,那么就遞歸的進行序列化
- }
- }
- }
- private Field[] iterateClassFields(Class cls) {
- List fieldsList = new LinkedList(); //用隊列存儲對象所有字段
- while (cls != null) {
- Field[] fields = cls.getDeclaredFields(); //獲得當前實例對應類所聲明的所有字段
- for (int i = 0; i < fields.length; i++) {
- if (!Modifier.isStatic(fields[i].getModifiers())) {
- fieldsList.add(fields[i]); //如果字段不是static修飾那么就加入隊列
- }
- }
- cls = cls.getSuperclass(); //獲取父類然后遞歸的獲取字段
- }
- Field[] retValue = new Field[fieldsList.size()];
- return (Field[])fieldsList.toArray();
- }
我們先看第一種情況的實現(xiàn),首先遍歷當前實例對應類聲明的所有字段,將所有字段放入到一個隊列中然后再一一取出來進行處理,這個功能的實現(xiàn)就在函數(shù)iterateClassFields,然后對取出的字段進行判斷,看它是否具備public屬性,如果不具備,那么要想讀取它的內(nèi)容,我們需要調用setAccessible進行設置,接下來還有判斷其是否是Transient類型,如果不是,那么就通過extractContentFromField來讀取字段包含的數(shù)據(jù)。
在extractContentFromField中,先判斷字段是否為基礎數(shù)據(jù)類型,如果是,由于基礎數(shù)據(jù)類型都實現(xiàn)了toString方法,于是我們可以用該方法獲得數(shù)據(jù)的字符串對應內(nèi)容,然后寫入到xml文件中,如果它不是基礎類型,那么我們就調用recursiveSerilizeObject遞歸的去對他進行序列化。
由于內(nèi)容相對燒腦,因此我們先在這里暫停,消化一下后再處理下一步,也就是應對字段是數(shù)組類型的情況。