很多时候我们需要将一个类的实例变成二进制数据存储或是通过网络发送,这个过程叫序列化。如果将二进制数据解析成位于内存中的类实例或是相关数据结构,那叫反序列化。所有的序列化算法都遵循一定的套路,例如:
- class A {
- public int a = 1;
- public int b = 2;
- protected B b = new B();
- private float c = 3.0;
- }
如果要序列化类A的实例,那么通常需要将变量a,b的数值对应的二进制数写入,然后获得类B实例序列化后的二进制数据,最后将变量c的数值的二进制数据,这里可以体会到,序列化其实有一种递归的性质,在序列化过程中如果遇到的是基础类型,那么可以直接获取其对应的二进制数据,如果遇到类实例,那么需要先序列化它,取得对应二进制数据。
而序列化过程中需要你了解对应类的定义,但如果我们不知道要序列化的对象,例如我们看不到类A的定义,我们只拿到了A对应的一个实例对象,那此时怎么序列化呢。这就需要用到java语言的反射特性,java编译器在编译类A时,不仅仅将它为它的各个字段分配了内存,而且还为类A的相关信息进行了设置和存储,例如A里面有多少字段,字段的类型是int, float, stirng,还是特定类对象,这些信息都一并设置并存储了起来,只要我们使用java反射提供的API就能获得这些信息,从而就能对任意类实现序列化。
因此序列化的万能套路是:
1,获得要序列化的类实例;
2,获得类中各个字段的属性,类型等相关信息。
3,如果字段属于基础数据,那么获得其数值的二进制数据。
4,如果对应字段是一个类实例,那么先递归的序列化该实例
根据以上步骤,当我们需要序列化任意一个类实例时,首先通过getClass获得其对应的Class类实例,然后调用getDeclaredFields()接口获得该实例所有的字段,其中包括public,protected,private,或者调用getFields()获得类实例声明或继承的公有字段。在序列化中,我们不能忘了序列化当前类实例的父类,因此可以调用getSuperClass()来获得当前实例的父类,这个过程会不断进行直到抵达根类为止。
每个字段都会对应一个元类叫Field,通过该类相关接口能获得字段的值。获取字段的数据首先需要确定字段的类型,如果是Boolean类型,那么可以调用Field类的getBoolean接口获得数据,如果是int类型,那么可以通过getInteger()接口获得数据,如果字段是类对象,那么就得递归的去获得其二进制数据,如果字段是基础类型,那么通过调用其getString()就能获得其数值的字符串形式。
在获取字段类型前,我们还需要知道字段的修饰属性,例如是public还是private,是不是static等,这些属性通过Field类的接口getModifier()获得,调用它会返回一个整形值,该值在相关比特位上设置1或0来表示修饰属性。在java语法中共有11种修饰属性,因此有11个比特位来对应,但我们不需要分析哪个比特位设置为1来获取字段属性,java反射提供了一个特定类Modifier,通过getModifier返回的数值可以输入Modifier类的isPublic, isPrivate等接口来查询字段对应的修饰属性。
在序列化时,我们要忽略掉static属性的字段,因此他们是写死的,因此通过Modifier.isStatic(field.getModifier())所得结果就能进行字段的static属性判断。总所周知,对于protected 或是private类型的字段,外部是不能直接读取的,但是序列化必须要能读取这类字段的值,要不然序列化就无法进行,Field类提供了setAccessible(true)来打破这个限制。
此外还需要考虑的一个因素是,如果字段是数组类型的情况。java反射提供了元类Array来应对,假设实例对象obj是一个数组,那么Array.getLength(obj)就能获得数组的长度,Array.get(obj, i)就能获得第i个元素对象。
最后我们需要考虑序列化后的文件格式,我们使用xml格式来存储序列化的结果,例如在上面例子中,字段a在序列化后对应为”\1\“,具体的情况我们在后续代码中慢慢来观察。
首先我们使用IntelliJ 创建一个maven项目,由于我们需要将数据序列化成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包。然后我们创建一个实现文件叫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);
- /*
- 判断当前要序列化的对象是否是数组类型,如果不是,那么先遍历该对象所有字段,然后递归的序列化对应字段,因为字段有可能是类对象,
- 如果是数组类型,那么遍历其中每个元素,然后针对每个元素进行序列化
- */
- if (objClass.isArray()) {
- //TODO
- } else {
- //TODO
- }
- }
- public static void main(String[] args) {
- }
- }
接下来我们针对两个TODO进行实现,如果当前要序列化的对象不是数组,那么需要遍历其所有字段,然后序列化各个字段,如果字段是类对象类型,那么还得递归的对他进行处理,我们看代码实现:
- /*
- 判断当前要序列化的对象是否是数组类型,如果不是,那么先遍历该对象所有字段,然后递归的序列化对应字段,因为字段有可能是类对象,
- 如果是数组类型,那么遍历其中每个元素,然后针对每个元素进行序列化
- */
- 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{
- //将字段对应的数据抽取出来
- 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来将自身对应的数据进行字符串表达
- }
- 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();
- }
我们先看第一种情况的实现,首先遍历当前实例对应类声明的所有字段,将所有字段放入到一个队列中然后再一一取出来进行处理,这个功能的实现就在函数iterateClassFields,然后对取出的字段进行判断,看它是否具备public属性,如果不具备,那么要想读取它的内容,我们需要调用setAccessible进行设置,接下来还有判断其是否是Transient类型,如果不是,那么就通过extractContentFromField来读取字段包含的数据。
在extractContentFromField中,先判断字段是否为基础数据类型,如果是,由于基础数据类型都实现了toString方法,于是我们可以用该方法获得数据的字符串对应内容,然后写入到xml文件中,如果它不是基础类型,那么我们就调用recursiveSerilizeObject递归的去对他进行序列化。
由于内容相对烧脑,因此我们先在这里暂停,消化一下后再处理下一步,也就是应对字段是数组类型的情况。