一、什么是序列化和反序列化?
序列化 (Serialization)是将对象的信息转换为可以存储或传输形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区(例如磁盘)。在次以后可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。这个过程可以通过下图来描述:
二、如何序列化和反序列化?
2.1.JDK的原生序列化
Java 的JDK提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。如果将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化。
序列化将对象转化为字节序列的过程,它是一个写过程(写入到某个存储介质中),需要使用到ObjectOutputStream的writeObject方法:
public final void writeObject(Object x) throws IOException |
该方法将对象序列化为字节序列,并将字节序列(序列化结果)发送到输出流。在序列化之前,必须让所序列化对象,否则在执行writeObject()方法时会抛出java.io.NotSerializableException这样的异常。例如,下面是一个将对象序列化到本地磁盘上的代码示例:
package com.yzh.demo; import java.io.Serializable; /** * @description:学生信息实体 * @author: yzh * @date: 2022-02-13 22:47 */ public class Student implements Cloneable, Serializable { private static final long serialVersionUID = 3824077755396751995L; private Long id; private String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + '}'; } @Override public Student clone() throws CloneNotSupportedException { return (Student) super.clone(); } } package com.yzh.demo.serialization; import com.yzh.demo.Student; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.OutputStream; /** * @description: 序列化对象并存储到本地磁盘 * @author: yzh * @date: 2022-02-20 0:39 */ public class SerializationDemo { /** * 序列化对象本地存储文件 */ private static final String SERIALIZE_LOCAL_OBJECT_TXT = "D:\\development_project\\testmavenproject\\userloginmodule\\src\\main\\resources\\object.txt"; public static void main(String[] args) { Student stu = new Student(); stu.setId(1001L); stu.setName("张三"); try ( OutputStream out = new FileOutputStream(SERIALIZE_LOCAL_OBJECT_TXT); ObjectOutputStream objOut = new ObjectOutputStream(out)){ objOut.writeObject(stu); } catch (IOException e) { e.printStackTrace(); } } } |
执行上述程序后,会将stu对象序列化为字节序列并存储到指定的磁盘目录下。
2.2.JDK的原生反序列化
反序列化是将字节序列转化为对象的过程,即序列化的逆过程,它是一个读取的过程,需要使用到ObjectInputStream的readObject方法:
public final Object readObject() throws IOException, ClassNotFoundException |
该方法从输入流中读取字节序列,将字节序列反序列化并返回目标对象。例如,下面是读取本地磁盘文件中的字节序列并反序列化为目标对象的代码示例:
package com.yzh.demo.serialization; import com.yzh.demo.Student; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; /** * @description: 反序列化案例 * @author: yzh * @date: 2022-02-20 0:39 */ public class SerializationDemo2 { /** * 序列化对象本地存储文件 */ private static final String SERIALIZE_LOCAL_OBJECT_TXT = "D:\\development_project\\testmavenproject\\userloginmodule\\src\\main\\resources\\object.txt"; public static void main(String[] args) { try ( InputStream in = new FileInputStream(SERIALIZE_LOCAL_OBJECT_TXT); ObjectInputStream objIn = new ObjectInputStream(in)){ Student student = (Student)objIn.readObject(); System.out.println(student); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } //~output:Student{id=1001, name='张三'} |
值得注意的是,如果POJO类中包含了引用属性,那么引用属性类型也必须实现java.io.Serializable接口,否则会抛出序列化异常。因为writeObject()方法不仅会对实体类本身序列化,还会遍历实体类的所有引用属性,并判断引用属性的类型是否实现了java.io.Serializable接口,如果未实现则抛出序列化异常。例如:
package com.yzh.demo; /** * @company: xxx科技有限公司 * @description: * @author: yzh * @date: 2022-02-20 15:56 */ public class StudentDO { private Double score; private Long id; private Object pkId; // 添加transient关键字,避免数据被序列化 private transient String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getScore() { return score; } public void setScore(Double score) { this.score = score; } public Object getPkId() { return pkId; } public void setPkId(Object pkId) { this.pkId = pkId; } @Override public String toString() { return "Student{" + "score=" + score + ", id=" + id + ", name='" + name + '\'' + '}'; } } package com.yzh.demo; import java.io.Serializable; /** * @description: * @author: yzh * @date: 2022-02-20 15:55 */ public class ClassDO implements Serializable { private static final long serialVersionUID = -7503113583733695100L; private Long id; private String name; private StudentDO student; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public StudentDO getStudent() { return student; } public void setStudent(StudentDO student) { this.student = student; } @Override public String toString() { return "ClassDO{" + "id=" + id + ", name='" + name + '\'' + ", student=" + student + '}'; } } package com.yzh.demo.utils; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; /** * @description: * @author: yzh * @date: 2022-02-20 16:00 */ public final class SerializationUtil { public static void serialize(Object object, String filePath) { try ( OutputStream out = new FileOutputStream(filePath); ObjectOutputStream objOut = new ObjectOutputStream(out)) { objOut.writeObject(object); } catch (IOException e) { e.printStackTrace(); } } public static <T> T getObject(String filePath, Class<T> clazz) throws IOException, ClassNotFoundException { try ( InputStream in = new FileInputStream(filePath); ObjectInputStream objIn = new ObjectInputStream(in)) { return (T) objIn.readObject(); } catch (IOException e) { e.printStackTrace(); throw e; } catch (ClassNotFoundException e) { throw e; } } } import com.yzh.demo.ClassDO; import com.yzh.demo.StudentDO; import com.yzh.demo.utils.SerializationUtil; import java.io.IOException; /** * @description: 序列化,反序列化案例 * @author: yzh * @date: 2022-02-20 0:39 */ public class SerializationDemo3 { /** * 序列化对象本地存储文件 */ private static final String SERIALIZE_LOCAL_OBJECT_TXT = "D:\\development_project\\testmavenproject\\userloginmodule\\src\\main\\resources\\object.txt"; public static void main(String[] args) { try { StudentDO student = new StudentDO(); student.setId(1001L); student.setName("张三"); student.setPkId(1001L); student.setScore(699.0d); ClassDO classObj = new ClassDO(); classObj.setId(10001L); classObj.setName("C518"); classObj.setStudent(student); SerializationUtil.serialize(classObj, SERIALIZE_LOCAL_OBJECT_TXT); ClassDO newStu = SerializationUtil.getObject(SERIALIZE_LOCAL_OBJECT_TXT, ClassDO.class); System.out.println(newStu); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } // ~output:java.io.NotSerializableException: com.yzh.demo.StudentDO |
解决办法:让ClassDO的引用属性类型StudentDO实现java.io.Serializable接口:
三、为什么要序列化?
- 一个原因是将对象的状态保持在存储媒体中,以便可以在以后重新创建精确的副本。
例如,当我们需要深克隆一个对象,通过新克隆对象进行操作,避免原对象受到影响,此时可以通过序列化一个对象为字节序列保存在存储介质中,然后在通过反序列化技术将保存在存储介质中的字节序列解析重组为一个克隆对象。尽管这种情况下我们不需要序列化也能实现,但实现起来很麻烦而且很容易出错(需要跟踪对象的属性层次结构,会变得相当复杂),
- 通过序列化技术来支撑RPC通信,也就是序列化是实现远程过程调用的基础。
我们知道网络传输的数据必须是二进制数据,但调用方请求的出入参数都是对象,而对象是不能直接在网络中传输,所以我们必须提前把它转成可传输的二进制数据,并且要求转换算法是可逆的,这个过程我们一般叫做“序列化”,而RPC框架识别到远程请求后,根据请求类型和序列化类型,把二进制的消息体逆向还原成请求对象,这个过程我们称之为“反序列化”。通过这种方式,可以保证数据传输的安全性,以免发生意外时数据丢失。在这个过程中,通过序列化技术将对象转成字节序列然后在进行某种协议的传输。
四、为什么RPC框架要使用到序列化技术?
我们通过RPC的通信流程图就能明白这一点:
因为网络传输的数据必须是二进制数据,所以在 RPC 调用中,对入参对象与返回值对象进行序列化与反序列化是一个必要的过程。
五、序列化还有哪些实现方式?
答:JSON和Hessian。
5.1.JSON
5.1.1.JSON的概念
JSON 是典型的 Key-Value 格式的序列化方式,没有数据类型,是一种文本型序列化框架。无论是前台 Web 用 Ajax 调用、用磁盘存储文本类型的数据,还是基于HTTP协议的RPC框架通信,都会涉及JSON格式。
5.1.2.经典案例—Long类型反序列化后变为Integer类型
第一步:引入Maven依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.5</version> </dependency> |
第二步:实际案例
package com.yzh.demo; /** * @description: * @author: yzh * @date: 2022-02-20 15:55 */ public class ClassDO { private Long id; private Object pkId; private String name; private StudentDO student; public Object getPkId() { return pkId; } public void setPkId(Object pkId) { this.pkId = pkId; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public StudentDO getStudent() { return student; } public void setStudent(StudentDO student) { this.student = student; } @Override public String toString() { return "ClassDO{" + "id=" + id + ", pkId=" + pkId + ", name='" + name + '\'' + ", student=" + student + '}'; } } package com.yzh.demo; /** * @description: * @author: yzh * @date: 2022-02-20 15:56 */ public class StudentDO /*implements Serializable */{ //private static final long serialVersionUID = -59848443221520951L; private Double score; private Long id; private Object pkId; // 添加transient关键字,避免数据被序列化 private transient String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getScore() { return score; } public void setScore(Double score) { this.score = score; } public Object getPkId() { return pkId; } public void setPkId(Object pkId) { this.pkId = pkId; } @Override public String toString() { return "Student{" + "score=" + score + ", id=" + id + ", name='" + name + '\'' + '}'; } } package com.yzh.demo.serialization; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.serializer.SerializerFeature; import com.yzh.demo.ClassDO; import com.yzh.demo.StudentDO; /** * @description: 序列化,反序列化案例 * @author: yzh * @date: 2022-02-20 0:39 */ public class SerializationDemo { public static void main(String[] args) { StudentDO student = new StudentDO(); student.setId(1001L); student.setName("张三"); student.setPkId(1001L); student.setScore(699.0d); String stuJsonStr = JSONObject.toJSONString(student); StudentDO stu = JSONObject.parseObject(stuJsonStr, StudentDO.class); Object pkId = stu.getPkId(); System.out.println(pkId instanceof Long); System.out.println(pkId instanceof Integer); ClassDO classDo = new ClassDO(); classDo.setId(10001L); classDo.setPkId(1001L); classDo.setName("C518"); classDo.setStudent(student); String classJsonStr = JSONObject.toJSONString(classDo); ClassDO newClassObj = JSONObject.parseObject(classJsonStr, ClassDO.class); StudentDO stuObj = newClassObj.getStudent(); stuObj.setName("李四"); System.out.println(classDo.getStudent()); System.out.println(newClassObj.getStudent()); } } //~output: false true Student{score=699.0, id=1001, name='张三'} Student{score=699.0, id=1001, name='李四'} |
从结果可以看出,使用fastjson框架,同样可实现对对象的深拷贝,但当POJO类的属性为Object类型时(pkId属性),虽然set值为Long类型,通过toJSONString()方法转换、parseObject方法解析后,Object类型的pkId属性存储的Long类型值被底层优化为Integer类型,究其原因是Long类型数据存储在Object类型的属性中,由于toJSONString()方法转换得到的是一个JSON格式的字符串,此时无法通过一个字符串来判断Object类型的pkId属性究竟是何种类型,因此通过parseObject()方法将1001L对应的字符串”1001”转化为1001L时,采取缩写范围适配法适配到的是Integer类型属性值,所以最终得到Integer类型的属性值,故与原值类型存在差异。解决办法主要有两个:
- 在POJO类中,禁止将属性定义为Object类型,要明确定义好属性类型;
- 在序列化时,指定序列化策略为SerializerFeature.WriteClassName,该策略表示在序列化时写入类型信息,以便在反序列化时能根据这些类型信息反向匹配属性值的具体类型。
常见的SerializerFeature枚举介绍如下表。
因此,上述案例可以这样来实现:
StudentDO student = new StudentDO(); student.setId(1001L); student.setName("张三"); student.setPkId(1001L); student.setScore(699.0d); String stuJsonStr = JSONObject.toJSONString(student, SerializerFeature.WriteClassName); StudentDO stu = JSONObject.parseObject(stuJsonStr, StudentDO.class); Object pkId = stu.getPkId(); System.out.println(pkId instanceof Long); System.out.println(pkId instanceof Integer); ClassDO classDo = new ClassDO(); classDo.setId(10001L); classDo.setPkId(1001L); classDo.setName("C518"); classDo.setStudent(student); String classJsonStr = JSONObject.toJSONString(classDo, SerializerFeature.WriteClassName); ClassDO newClassObj = JSONObject.parseObject(classJsonStr, ClassDO.class); StudentDO stuObj = newClassObj.getStudent(); stuObj.setName("李四"); System.out.println(classDo.getStudent()); System.out.println(newClassObj.getStudent()); //~output: true false Student{score=699.0, id=1001, name='张三'} Student{score=699.0, id=1001, name='李四'} |
这种类型反序列化错误的情况在Gson类中也同样存在:
package com.yzh.demo.serialization; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.TypeReference; import com.alibaba.fastjson.serializer.SerializerFeature; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.yzh.demo.ClassDO; import com.yzh.demo.StudentDO; import java.util.LinkedHashMap; import java.util.Map; /** * @description: 序列化,反序列化案例 * @author: yzh * @date: 2022-02-20 0:39 */ public class SerializationDemo4 { public static void main(String[] args) { StudentDO student = new StudentDO(); student.setId(1001L); student.setName("张三"); student.setPkId(1001L); student.setScore(699.0d); ClassDO classDo = new ClassDO(); classDo.setId(10001L); classDo.setPkId(1001L); classDo.setName("C518"); classDo.setStudent(student);
Map<String, LinkedHashMap<String, ClassDO>> map = new LinkedHashMap<>(16); LinkedHashMap<String, ClassDO> subMap = new LinkedHashMap<>(16); subMap.put("key", classDo); map.put("key", subMap); Gson gson = new Gson(); String newJsonStr = gson.toJson(map, new TypeToken<Map<String, LinkedHashMap<String, ClassDO>>>() { }.getType()); Map<String, LinkedHashMap<String, ClassDO>> newMap2 = parseJsonStr(newJsonStr); LinkedHashMap<String, ClassDO> newSubMap2 = newMap2.get("key"); ClassDO classDO2 = newSubMap2.get("key"); StudentDO newStu2 = classDO2.getStudent(); Object newPkId2 = newStu2.getPkId(); System.out.println(newPkId2 instanceof Long); System.out.println(newPkId2 instanceof Integer); // Object类型的pkId属性保存的Long类型值反序列化时变为Double类型 System.out.println(newPkId2 instanceof Double); } private static Map<String, LinkedHashMap<String, ClassDO>> parseJsonStr(String calcFieldStr) { if(calcFieldStr == null || "".equals(calcFieldStr)) { return new LinkedHashMap<>(16); } Gson gson = new Gson(); return gson.fromJson(calcFieldStr, new TypeToken<Map<String, LinkedHashMap<String, ClassDO>>>() { }.getType()); } } // ~output: false false true |
说明:当使用Object类型的pkId属性存储Long类型数据,在使用Gson的fromJson方法反序列化时容易被解析为Double类型,我们姑且可以认为这是Gson类的一个缺陷。而使用fastjson的JSONObject则很容易解决这个问题:
package com.yzh.demo.serialization; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.TypeReference; import com.alibaba.fastjson.serializer.SerializerFeature; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.yzh.demo.ClassDO; import com.yzh.demo.StudentDO; import java.util.LinkedHashMap; import java.util.Map; /** * @description: 序列化,反序列化案例 * @author: yzh * @date: 2022-02-20 0:39 */ public class SerializationDemo4 { public static void main(String[] args) { StudentDO student = new StudentDO(); student.setId(1001L); student.setName("张三"); student.setPkId(1001L); student.setScore(699.0d); ClassDO classDo = new ClassDO(); classDo.setId(10001L); classDo.setPkId(1001L); classDo.setName("C518"); classDo.setStudent(student); Map<String, LinkedHashMap<String, ClassDO>> map = new LinkedHashMap<>(16); LinkedHashMap<String, ClassDO> subMap = new LinkedHashMap<>(16); subMap.put("key", classDo); map.put("key", subMap); // 指定序列化策略为SerializerFeature.WriteClassName,该策略表示在序列化时写入类型信息 String jsonStr = JSONObject.toJSONString(map, SerializerFeature.WriteClassName); // 使用parseObject & TypeReference反序列化复杂的泛型数据结构对象的对应json Map<String, LinkedHashMap<String, ClassDO>> newMap = JSONObject.parseObject(jsonStr, new TypeReference<Map<String, LinkedHashMap<String, ClassDO>>>(){}.getType()); LinkedHashMap<String, ClassDO> newSubMap = newMap.get("key"); ClassDO classDO = newSubMap.get("key"); StudentDO newStu = classDO.getStudent(); Object newPkId = newStu.getPkId(); System.out.println(newPkId instanceof Long); System.out.println(newPkId instanceof Integer); } } // ~output: true false |
我们看到,fastjson的JSONObject不仅能对复杂的泛型对象对应json有着良好的解析能力,并且对字段值及其类型也有着优秀的还原能力。
5.1.3.—new TypeReference<Map<String,LinkedHashMap<String, ClassDO>>>(){}.getType() 为什么要有 {}?
为什么这样就可以new一个不能访问的类对象呢?按照理解,不再同一个包下(fastjson下的类)是不能直接new对象的。但是有趣地是只要加一个{}就可以了。其实是这样的这里new的并不是TypeToken对象,而是在此处定义了一个匿名类,而类体 {} 中没有实现任何内容。因为我们只需能够调用父类的一个getType()方法而已。由此可见,当两个类位于不同包下时,其中某个类通过匿名类创建对象的最低要求是构造器的访问修饰控制符为protected(如果两个类位于同一包下,则其中某个类通过匿名类创建对象的最低要求是构造器的访问修饰控制符为缺省)。我们可以验证下这个结论:
package com.yzh.demo; /** * @description: * @author: yzh * @date: 2022-02-20 19:47 */ public class MyTypeToken { // 设置构造器的访问控制修饰符为protected protected MyTypeToken(){} } // 在另一个包下 package com.yzh.demo.serialization; /** * @author: yzh * @date: 2022-02-20 0:39 */ public class SerializationDemo4 { public static void main(String[] args) { new MyTypeToken(){}; } } |
5.1.4.—为什么TypeReference的构造器要设计成protected方法,而不是public方法?
- 如果直接通过new TypeReference<T>()的方式创建其实例来调用方法,由于Java在编译期间,所有的泛型信息都会被擦掉,通过实例无法直接在运行期间获取<T>泛型信息。
下面这个案例展示了MyTypeToken<T>无法通过父类Object获取泛型<T>的过程:
package com.yzh.demo; /** * @description: * @author: yzh * @date: 2022-02-20 19:47 */ public class MyTypeToken<T> { public MyTypeToken(){} } // Java在编译期间,泛型信息<ClassDO>都会被擦掉,也就是说对于JVM而言,在运行期间看到的是MyTypeToken类型,而不是MyTypeToken<ClassDO> MyTypeToken<ClassDO> myTypeToken = new MyTypeToken<>(); // 如果MyTypeToken父类是Object,那么我们无法通过MyTypeToken的父类来获取MyTypeToken的泛型类型 System.out.println(myTypeToken.getClass().getSuperclass()); // 获取带有泛型信息的父类 Type genericSuperclass = myTypeToken.getClass().getGenericSuperclass(); System.out.println(genericSuperclass.getTypeName()); // 获取MyTypeToken的泛型类型 Type actualTypeArgument2 = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0]; System.out.println(actualTypeArgument2.getTypeName()); // ~output: java.lang.Object Exception in thread "main" java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType |
因此,通过 new TypeReference<T>()的方式来获取泛型<T>是行不通的,所以TypeReference的构造器要设计成protected方法,防止外部包的开发通过new TypeReference<T>()的方式(只有public修饰的构造器才能在其他包中通过new创建对象)来调用getType()方法作为JSONObject.parseObject()方法的入参。
- 通过TypeReference<T>父类来获取TypeReference<T>的泛型类型<T>。
那么有什么解决办法呢?实际上,在反射中提供了getClass().getGenericSuperclass()方法来获取父类及父类的泛型信息,因此我们可以通过新建一个SuperTypeToken<T>类,让MyTypeToken<T>继承SuperTypeToken<T>,再借助MyTypeToken的父类来获取MyTypeToken的泛型信息:
package com.yzh.demo.serialization; /** * @description: * @author: yzh * @date: 2022-02-20 19:47 */ public class SuperTypeToken<T> { public SuperTypeToken(){} } package com.yzh.demo.serialization; import com.yzh.demo.ClassDO; import com.yzh.demo.StudentDO; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; /** * @description: * @author: yzh * @date: 2022-02-20 19:47 */ public class MyTypeToken<T> extends SuperTypeToken<StudentDO> { // 设置构造器的访问控制修饰符为protected public MyTypeToken(){} public static void main(String[] args) { // 解决办法:创建SuperTypeToken<T>,让MyTypeToken<T>继承SuperTypeToken<T>,再通过MyTypeToken父类获取MyTypeToken的泛型信息 MyTypeToken<ClassDO> myTypeToken = new MyTypeToken<>(); System.out.println(myTypeToken.getClass().getSuperclass()); // 获取带有泛型信息的父类 Type genericSuperclass = myTypeToken.getClass().getGenericSuperclass(); System.out.println(genericSuperclass.getTypeName()); // 获取MyTypeToken的泛型类型 Type actualTypeArgument2 = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0]; System.out.println(actualTypeArgument2.getTypeName()); } } //~output: class com.yzh.demo.serialization.SuperTypeToken com.yzh.demo.serialization.SuperTypeToken<com.yzh.demo.StudentDO> com.yzh.demo.StudentDO |
有没有更优雅的方式呢?答案是肯定的。在前面,我们已经知道,匿名类的父类就是其本身,例如MyTypeToken匿名类的父类就是MyTypeToken。基于这个原理,我们就可以这样来设计了:
package com.yzh.demo; /** * @description: * @author: yzh * @date: 2022-02-20 19:47 */ public class MyTypeToken<T> { // 设置构造器的访问控制修饰符为protected,防止在其他包下通过new对象的方式来获取实例,即对外部关闭对象的创建 protected MyTypeToken(){} } package com.yzh.demo.serialization; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.TypeReference; import com.alibaba.fastjson.serializer.SerializerFeature; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.yzh.demo.ClassDO; import com.yzh.demo.MyTypeToken; import com.yzh.demo.StudentDO; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.LinkedHashMap; import java.util.Map; /** * @description: 序列化,反序列化案例 * @author: yzh * @date: 2022-02-20 0:39 */ public class SerializationDemo { public static void main(String[] args) { // 有什么办法解决呢?即通过什么方式获取泛型信息呢?显然通过new对象这种方式是无法实现的,就只能借助于匿名类{}这种结构, // 同时为了防止开发人员通过创建对象来获取调用内部方法,故应MyTypeToken类的构造器将设计为protected修饰 Class<? extends MyTypeToken<ClassDO>> aClass = new MyTypeToken<ClassDO>() { }.getClass(); Class<?> superclass = aClass.getSuperclass(); // MyTypeToken匿名类的父类就是MyTypeToken,因此我们可以通过MyTypeToken的父类来获取MyTypeToken的泛型类型 System.out.println(superclass); // 获取带泛型类型的父类类型 Type superClass = aClass.getGenericSuperclass(); System.out.println(superClass.getTypeName()); // 获取MyTypeToken的泛型类型 Type actualTypeArgument = ((ParameterizedType) superClass).getActualTypeArguments()[0]; System.out.println(actualTypeArgument.getTypeName()); } } // ~output: class com.yzh.demo.MyTypeToken com.yzh.demo.MyTypeToken<com.yzh.demo.ClassDO> com.yzh.demo.ClassDO |
但为什么MyTypeToken的构造器不能是私有构造器和缺省修饰符修饰的构造器呢?原因是对第三方调用者要提供创建匿名类的权限,而私有构造器和缺省修饰符修饰的构造器是无法创建(new)匿名类的。
5.1.5.设计一个获取当前类泛型类型的工具类
package com.yzh.demo.serialization; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; /** * @description: * @author: yzh * @date: 2022-02-20 22:48 */ public class RefTypeToken<T> { protected RefTypeToken(){} public Type getType(){ // 获取带泛型的父类 Type genericSuperclass = this.getClass().getGenericSuperclass(); // 获取泛型类型<T> Type actualTypeArgument = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0]; return actualTypeArgument; } } package com.yzh.demo.utils; import com.yzh.demo.StudentDO; import java.lang.reflect.Type; import java.util.List; import java.util.Map; /** * @description: * @author: yzh * @date: 2022-02-20 22:44 */ public class GenericTypeUtil { public static Type getGenericType(RefTypeToken refTypeToken) { return refTypeToken.getType(); } public static void main(String[] args) { Type genericType = GenericTypeUtil.getGenericType( new RefTypeToken<List<Map<String, Map<Long, StudentDO>>>>(){}); System.out.println(genericType.getTypeName()); } } // ~output: java.util.List<java.util.Map<java.lang.String, java.util.Map<java.lang.Long, com.yzh.demo.StudentDO>>> |
5.1.6.JSON常见的框架有哪些?说说它们的特点和适用场景
答:常见的JSON框架有阿里巴巴的fastjson,Google-Gson类库和Jackson。它们的特点和适用场景如下表:
JSON框架名称 | 特点 | 适用场景 | 备注 |
com.alibaba.fastjson | (1)在服务器端和android客户端中都可以提供最佳性能,在解析相对不大的数据量时号称json解析最快框架; (2)提供简单的toJSONString()和parseObject()方法,便于Java对象与Json对象自由转换; (3)允许将现有的不可修改对象与JSON相互转换; (4)Java泛型的支持 (5)允许对象自定义表示; (6)支持任意复杂的对象(具有深层继承层次结构和泛型类型)。 | (1)对JSON解析有较好的性能要求的场景; (2)复杂泛型嵌套结构的Java对象与JSON字符串间的转换。 | fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。 |
Gson | (1)提供简单的toJson()和fromJson()方法,将Java对象转换为JSON; (2)允许将现有的不可修改对象与JSON相互转换; (3)Java泛型支持; (4)允许对象的自定义表示; (5)支持任意复杂的对象(具有深层继承层次结构和泛型类型)。 | 复杂泛型嵌套结构的Java对象与JSON字符串间的转换(存在一定缺陷,例子:Object类型属性接收的Long类型值反序列化后变为Double,而fastjson能很好地解决这个问题) | Gson是一个Google开源的一个Java库,可用于将Java对象转换Gson可以处理任意Java对象,包括您没有源代码的现有对象。 |
Jackson | (1)与其他Java的JSON框架相比,Jackson解析大的JSON文件速度比较快; (2)Jackson运行时占用内存比较低,性能比较好; (3)Jackson有灵活的API可以很容易进行扩展和定制; (4)功能比其他Java的JSON框架更加强大。 | 解析大数据场景下的JSON文件 | jackson也是一个知名的基于Java平台的JSON库,Jackson不仅支持流式处理json,还支持数据绑定(POJO和JSON之间的相互转化),甚至还拓展了很多其他第三方库所支持的数据格式(如:CSV, (Java) Properties, XML和YAML)等。jackson的三个核心模块:jackson-core定义了低级的流式API,包括了JSON处理细节,jackson-annotations包含了Jackson的注解,jackson-databind实现了对象和JSON之间的转换。相对于其他JSON框架,jackson提供了更加强大更多的功能。 |
总体来说,遇到Java对象与JSON格式字符串之间的转换时,由于fastjson的优秀的性能、对复杂的泛型对象对应json有着良好的解析能力和精准的对象还原能力,因此首选fastjson。
5.1.7.注意事项
JSON进行序列化有这样两个问题,我们需要格外注意:
- JSON进行序列化的额外空间开销比较大,对于大数据量服务这意味着需要巨大的内存和磁盘开销;
- JSON没有类型,但像Java这种强类型语言,需要通过反射统一解决,所以性能不佳。
所以如果RPC框架选用 JSON 序列化,服务提供者与服务调用者之间传输的数据量要相对较小,否则将严重影响性能。
5.2.Hessian
Hessian 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。Hessian 协议要比 JDK、JSON 更加紧凑,性能上要比 JDK、JSON 序列化高效很多,而且生成的字节数也更小。使用代码示例如下:
Student student = new Student(); student.setNo(101); student.setName("HESSIAN"); // 把student对象转化为byte数组 ByteArrayOutputStream bos = new ByteArrayOutputStream(); Hessian2Output output = new Hessian2Output(bos); output.writeObject(student); output.flushBuffer(); byte[] data = bos.toByteArray(); bos.close(); // 把刚才序列化出来的byte数组转化为student对象 ByteArrayInputStream bis = new ByteArrayInputStream(data); Hessian2Input input = new Hessian2Input(bis); Student deStudent = (Student) input.readObject(); input.close(); System.out.println(deStudent); |
相对于 JDK、JSON,由于 Hessian 更加高效,生成的字节数更小,有非常好的兼容性和稳定性,所以 Hessian 更加适合作为 RPC 框架远程通信的序列化协议。但Hessian本身也有问题,官方版本对 Java 里面一些常见对象的类型不支持,比如:
- Linked系列,LinkedHashMap、LinkedHashSet 等,但是可以通过扩展CollectionDeserializer 类修复;
- Locale 类,可以通过扩展 ContextSerializerFactory 类修复;
- Byte/Short 反序列化的时候变成 Integer。
以上这些情况,在实践时需要格外注意。
六、面试题集锦
6.1.经典面试题—如何避免POJO类的属性被序列化?
答:使用瞬时关键字,即在不需要序列化的属性前添加transient修饰。例如:
package com.yzh.demo; import java.io.Serializable; /*** * @description: * @author: yzh * @date: 2022-02-13 22:47 */ public class Student implements Cloneable, Serializable { private static final long serialVersionUID = 3824077755396751995L; private Long id; // 添加transient关键字,避免数据被序列化 private transient String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + '}'; } @Override public Student clone() throws CloneNotSupportedException { return (Student) super.clone(); } } // 测试程序 // (1)执行序列化 package com.yzh.demo.serialization; import com.yzh.demo.Student; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.OutputStream; /** * @description: 序列化对象并存储到本地磁盘 * @author: yzh * @date: 2022-02-20 0:39 */ public class SerializationDemo { /** * 序列化对象本地存储文件 */ private static final String SERIALIZE_LOCAL_OBJECT_TXT = "D:\\development_project\\testmavenproject\\userloginmodule\\src\\main\\resources\\object.txt"; public static void main(String[] args) { Student stu = new Student(); stu.setId(1001L); stu.setName("张三"); try ( OutputStream out = new FileOutputStream(SERIALIZE_LOCAL_OBJECT_TXT); ObjectOutputStream objOut = new ObjectOutputStream(out)){ objOut.writeObject(stu); } catch (IOException e) { e.printStackTrace(); } } } // (2)执行反序列化 package com.yzh.demo.serialization; import com.yzh.demo.Student; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; /** * @description: 反序列化案例 * @author: yzh * @date: 2022-02-20 0:39 */ public class SerializationDemo2 { /** * 序列化对象本地存储文件 */ private static final String SERIALIZE_LOCAL_OBJECT_TXT = "D:\\development_project\\testmavenproject\\userloginmodule\\src\\main\\resources\\object.txt"; public static void main(String[] args) { try ( InputStream in = new FileInputStream(SERIALIZE_LOCAL_OBJECT_TXT); ObjectInputStream objIn = new ObjectInputStream(in)){ Student student = (Student)objIn.readObject(); System.out.println(student); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } // ~output: Student{id=1001, name='null'} |
说明:
- 如果是引用类型(包括字符串、包装类型)被transient修饰,则序列化、反序列化后得到的结果为null,注意上述结果中字符串为‘null’是重写toString()的结果;
- 如果是基本数据类型属性被transient修饰,则序列化、反序列化后得到的结果为0或0.0;
- 如果属性存在初始化值,如图:
无论该属性是否提供并调用了getter/setter方法
- 当属性是成员属性时,则序列化、反序列化后得到的结果为null或0或0.0;
- 当属性是静态变量或常量(被static final 修饰,注意被final修饰的变量不能重新set设值)时,则序列化、反序列化后得到的结果为初始化值。
添加瞬时关键字transient的目的主要有两个:
- 无关紧要不需要持久化的字段;
- 出于安全考虑,不想将隐私或重要信息发送给第三方外部系统(远程调用的底层原理就是使用了序列化)。
- 因为克隆技术不受transient瞬时关键字的影响,要注意隐私信息的保护,防止因实现了Cloneable接口和重写了Object的clone方法导致数据外泄。
6.2.经典面试题—为什么序列化对象的类必须实现Serializable接口?
答:除Enum、数组外(参阅java.io.ObjectOutputStream#writeObject0源码),如果被序列化的类没有实现Serializable接口,则序列化时(即调用ObjectOutputException的writeObject方法)会抛出java.io.NotSerializabledExceptionyi异常,为什么会抛出该异常呢?通过阅读源码发现:
如果被序列化的类没有实现java.io.Serializable接口,会抛出相应的异常,并且这种接口没有定义任何抽象方法,而仅仅用于控制流程的异常抛出,可见Serializable与Cloneable一样,是一种标识接口。
在上述案例中要求POJO类实现Serializable接口的是JDK原生序列化技术,并不是所有用到序列化技术的都要求POJO类实现Serializable接口,因为实现序列化不单单只有JDK原生序列化技术,还有其他很多实现手段,因此某些RPC框架并没有要求POJO类实现Serializable接口。
6.3.经典面试题—说说Java序列化中serialVersionUID版本号的作用
答:serialVersionUID是为每个序列化类分配的版本标识,用来保证类在序列化(发送方)和反序列化(接收方)前后的兼容性,当序列化(发送方)与反序列化(接收方)的类的版本号不一致时,会抛出相应的java.io.InvalidClassException异常。例如:
(1)创建一个POJO,让其实现java.io.Serializable接口
package com.yzh.demo; import java.io.Serializable; /** * @description: * @author: yzh * @date: 2022-02-13 22:47 */ public class Student implements Cloneable, Serializable { private Double score; private Long id; // 添加transient关键字,避免数据被序列化 private transient String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getScore() { return score; } public void setScore(Double score) { this.score = score; } @Override public String toString() { return "Student{" + "score=" + score + ", id=" + id + ", name='" + name + '\'' + '}'; } @Override public Student clone() throws CloneNotSupportedException { return (Student) super.clone(); } } |
(2)执行序列化操作
package com.yzh.demo.serialization; import com.yzh.demo.Student; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.OutputStream; /** * @description: 序列化对象并存储到本地磁盘 * @author: yzh * @date: 2022-02-20 0:39 */ public class SerializationDemo { /** * 序列化对象本地存储文件 */ private static final String SERIALIZE_LOCAL_OBJECT_TXT = "D:\\development_project\\testmavenproject\\userloginmodule\\src\\main\\resources\\object.txt"; public static void main(String[] args) { Student stu = new Student(); stu.setId(1001L); stu.setName("张三"); stu.setScore(120.0d); try ( OutputStream out = new FileOutputStream(SERIALIZE_LOCAL_OBJECT_TXT); ObjectOutputStream objOut = new ObjectOutputStream(out)){ objOut.writeObject(stu); } catch (IOException e) { e.printStackTrace(); } } } |
说明:在执行序列化操作时,系统会根据该类的类型信息(如类名,方法和属性等参数)生成的 hash 值作为系统默认的serialVersionUID版本号(即系统计算的理论值),这里假设为x。序列化操作完成后,由于使用了文件输出流,因此会将该类的字节序列连同系统计算的serialVersionUID版本号x一起写入了磁盘中。
(3)执行反序列化操作
在执行反序列化前,添加或修改一个属性,此时系统会根据该类的类型信息(如类名,方法和属性等参数)重新生成的 hash 值作为系统默认的serialVersionUID版本号,这里假设为y,由于修改或增加了属性,显然该serialVersionUID版本号y与序列化并持久化到磁盘中的字节序列对应的serialVersionUID版本号x不一致。然后再执行反序列化操作:
package com.yzh.demo.serialization; import com.yzh.demo.Student; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; /** * @description: 反序列化案例 * @author: yzh * @date: 2022-02-20 0:39 */ public class SerializationDemo2 { /** * 序列化对象本地存储文件 */ private static final String SERIALIZE_LOCAL_OBJECT_TXT = "D:\\development_project\\testmavenproject\\userloginmodule\\src\\main\\resources\\object.txt"; public static void main(String[] args) { try ( InputStream in = new FileInputStream(SERIALIZE_LOCAL_OBJECT_TXT); ObjectInputStream objIn = new ObjectInputStream(in)){ Student student = (Student)objIn.readObject(); System.out.println(student); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } //~output: java.io.InvalidClassException: com.yzh.demo.Student; local class incompatible: stream classdesc serialVersionUID = 2949823666973194440, local class serialVersionUID = 9029558274943953237 |
为什么会抛出该异常呢?原来系统会将序列化对象的serialVersionUID版本号和反序列化对象的当前类型信息重新计算的serialVersionUID版本号作对比,如果不一致时,就会抛出java.io.InvalidClassException异常。显然抛出该异常的本质原因是序列化和反序列化前后的serialVersionUID版本号不一致所致,因此以下情况会抛出该异常:
(1)类声明了serialVersionUID版本号,但反序列化时更改了该类的serialVersionUID版本号;
(2)类未声明serialVersionUID版本号,但反序列化前修改了类型信息,如增加或删除方法、属性、方法参数等;
(3)不同版本的JDK编译很可能会生成不同的 serialVersionUID 默认值。如何避免这种情况呢?其实很简单,我们只需要让被序列化的类显式声明serialVersionUID即可,就像下面这样:
private static final long serialVersionUID = 3824077755396751995L; |
需要注意的是,serialVersionUID版本号的修饰符为static final long,该属性名称是系统约定不能修改的,一旦显显式声明了serialVersionUID版本号就要禁止修改它。这样一来,序列化和反序列化前后都会获取这个显式声明的serialVersionUID作为类版本标识,因此也就不会再抛出java.io.InvalidClassException这样的异常。
6.4.对于JDK原生序列化技术,POJO的属性类型为Object类型,如果序列化时属性值为Long类型,反序列化时是否会出现安全问题?
例如:
package com.yzh.demo; import java.io.Serializable; /** * @description: * @author: yzh * @date: 2022-02-13 22:47 */ public class Student implements Cloneable, Serializable { private static final long serialVersionUID = 3824077755396751995L; private Double score; private Long id; private Object pkId; // 添加transient关键字,避免数据被序列化 private transient String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getScore() { return score; } public void setScore(Double score) { this.score = score; } public Object getPkId() { return pkId; } public void setPkId(Object pkId) { this.pkId = pkId; } @Override public String toString() { return "Student{" + "score=" + score + ", id=" + id + ", name='" + name + '\'' + '}'; } @Override public Student clone() throws CloneNotSupportedException { return (Student) super.clone(); } } package com.yzh.demo.serialization; import com.yzh.demo.Student; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.OutputStream; /** * @description: 序列化对象并存储到本地磁盘 * @author: yzh * @date: 2022-02-20 0:39 */ public class SerializationDemo { /** * 序列化对象本地存储文件 */ private static final String SERIALIZE_LOCAL_OBJECT_TXT = "D:\\development_project\\testmavenproject\\userloginmodule\\src\\main\\resources\\object.txt"; public static void main(String[] args) { Student stu = new Student(); stu.setId(1001L); stu.setPkId(1001L); stu.setName("张三"); stu.setScore(120.0d); try ( OutputStream out = new FileOutputStream(SERIALIZE_LOCAL_OBJECT_TXT); ObjectOutputStream objOut = new ObjectOutputStream(out)){ objOut.writeObject(stu); } catch (IOException e) { e.printStackTrace(); } } } package com.yzh.demo.serialization; import com.yzh.demo.Student; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; /** * @description: 反序列化案例 * @author: yzh * @date: 2022-02-20 0:39 */ public class SerializationDemo2 { /** * 序列化对象本地存储文件 */ private static final String SERIALIZE_LOCAL_OBJECT_TXT = "D:\\development_project\\testmavenproject\\userloginmodule\\src\\main\\resources\\object.txt"; public static void main(String[] args) { try ( InputStream in = new FileInputStream(SERIALIZE_LOCAL_OBJECT_TXT); ObjectInputStream objIn = new ObjectInputStream(in)){ Student student = (Student)objIn.readObject(); System.out.println(student); Object id = student.getPkId(); System.out.println(id instanceof Long); System.out.println(id instanceof Integer); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } // ~output: Student{score=120.0, id=1001, name='null'} true false |
因此从结果来看,对于JDK原生序列化技术,即便是POJO的类型为Object,则反序列化时得到的结果也与序列化前设置的pkId值1001L的类型保持一致。
结论:POJO的属性类型为Object,如果序列化时添加的是Long类型,反序列化时的字段值类型仍然是Long,不会导致出现反序列化为Integer的情况。
七、Java序列化的实际应用场景(或序列化的作用)
- 深克隆一个对象
说明:到底是使用序列化还是克隆技术(实现Cloneable接口和Object的clone方法),取决于实际应用场景,一般情况下,强烈推荐通过序列化实现对象的深克隆,因为clone()实现的是浅克隆,想要实现深克隆十分麻烦;当序列化受到局限时,比如无法实现被transient瞬时关键字修饰的属性的序列化,但确实希望能克隆这个属性值时,可以考虑通过克隆技术来实现,因为克隆技术不受transient瞬时关键字的影响,否则优先选择序列化实现对象的深克隆。例如:
package com.yzh.demo.serialization; import com.yzh.demo.Student; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.OutputStream; /** * @description: 序列化对象并存储到本地磁盘 * @author: yzh * @date: 2022-02-20 0:39 */ public class SerializationDemo { /** * 序列化对象本地存储文件 */ private static final String SERIALIZE_LOCAL_OBJECT_TXT = "D:\\development_project\\testmavenproject\\userloginmodule\\src\\main\\resources\\object.txt"; public static void main(String[] args) throws CloneNotSupportedException { Student stu = new Student(); stu.setId(1001L); stu.setPkId(1001L); stu.setName("张三"); stu.setScore(120.0d); // 克隆技术不受transient瞬时关键字的影响 Student cloneStu = stu.clone(); System.out.println(cloneStu); try ( OutputStream out = new FileOutputStream(SERIALIZE_LOCAL_OBJECT_TXT); ObjectOutputStream objOut = new ObjectOutputStream(out)){ objOut.writeObject(stu); } catch (IOException e) { e.printStackTrace(); } } } //~output:Student{score=120.0, id=1001, name='张三'} |
- 通过其他技术,配合序列化可以将对象的字节序列持久化-保存在内存、文件、数据库中;
如上面的JDK原生序列化的案例就是将对象序列化后通过文件输出流持久化到了本地磁盘上。
- 网络传输对象的二进制数据(字节序列)
我们知道网络传输的数据必须是二进制数据,但调用方请求的出入参数都是对象,而对象是不能直接在网络中传输,所以我们必须提前把它转成可传输的二进制数据,并且要求转换算法是可逆的,这个过程我们一般叫做“序列化”。
- RPC远程过程调用
因为网络传输的数据必须是二进制数据,所以在 RPC 调用中,对入参对象与返回值对象进行序列化与反序列化是一个必要的过程。