在路上

 找回密码
 立即注册
在路上 站点首页 学习 查看内容

Java序列化示例教程

2016-12-20 13:13| 发布者: zhangjf| 查看: 495| 评论: 0

摘要: Java序列化是在JDK 1.1中引入的,是Java内核的重要特性之一。Java序列化API允许我们将一个对象转换为流,并通过网络发送,或将其存入文件或数据库以便未来使用,反序列化则是将对象流转换为实际程序中使用的Java对象 ...

Java序列化是在JDK 1.1中引入的,是Java内核的重要特性之一。Java序列化API允许我们将一个对象转换为流,并通过网络发送,或将其存入文件或数据库以便未来使用,反序列化则是将对象流转换为实际程序中使用的Java对象的过程。Java同步化过程乍看起来很好用,但它会带来一些琐碎的安全性和完整性问题,在文章的后面部分我们会涉及到,以下是本教程涉及的主题。

Java序列化接口 使用序列化和serialVersionUID进行类重构 Java外部化接口 Java序列化方法 序列化结合继承 序列化代理模式 Java序列化接口

如果你希望一个类对象是可序列化的,你所要做的是实现java.io.Serializable接口。序列化一种标记接口,不需要实现任何字段和方法,这就像是一种选择性加入的处理,通过它可以使类对象成为可序列化的对象。

序列化处理是通过ObjectInputStream和ObjectOutputStream实现的,因此我们所要做的是基于它们进行一层封装,要么将其保存为文件,要么将其通过网络发送。我们来看一个简单的序列化示例。

  1. package com.journaldev.serialization;
  2. import java.io.Serializable;
  3. public class Employee implements Serializable {
  4. // private static final long serialVersionUID = -6470090944414208496L;
  5. private String name;
  6. private int id;
  7. transient private int salary;
  8. // private String password;
  9. @Override
  10. public String toString(){
  11. return "Employee{name="+name+",id="+id+",salary="+salary+"}";
  12. }
  13. //getter and setter methods
  14. public String getName() {
  15. return name;
  16. }
  17. public void setName(String name) {
  18. this.name = name;
  19. }
  20. public int getId() {
  21. return id;
  22. }
  23. public void setId(int id) {
  24. this.id = id;
  25. }
  26. public int getSalary() {
  27. return salary;
  28. }
  29. public void setSalary(int salary) {
  30. this.salary = salary;
  31. }
  32. // public String getPassword() {
  33. // return password;
  34. // }
  35. //
  36. // public void setPassword(String password) {
  37. // this.password = password;
  38. // }
  39. }
复制代码

注意一下,这是一个简单的java bean,拥有一些属性以及getter-setter方法,如果你想要某个对象属性不被序列化成流,你可以使用transient关键字,正如示例中我在salary变量上的做法那样。

现在我们假设需要把我们的对象写入文件,之后从相同的文件中将其反序列化,因此我们需要一些工具方法,通过使用ObjectInputStream和ObjectOutputStream来达到序列化的目的。

  1. package com.journaldev.serialization;
  2. import java.io.FileInputStream;
  3. import java.io.FileOutputStream;
  4. import java.io.IOException;
  5. import java.io.ObjectInputStream;
  6. import java.io.ObjectOutputStream;
  7. /**
  8. * A simple class with generic serialize and deserialize method implementations
  9. *
  10. * @author pankaj
  11. *
  12. */
  13. public class SerializationUtil {
  14. // deserialize to Object from given file
  15. public static Object deserialize(String fileName) throws IOException,
  16. ClassNotFoundException {
  17. FileInputStream fis = new FileInputStream(fileName);
  18. ObjectInputStream ois = new ObjectInputStream(fis);
  19. Object obj = ois.readObject();
  20. ois.close();
  21. return obj;
  22. }
  23. // serialize the given object and save it to file
  24. public static void serialize(Object obj, String fileName)
  25. throws IOException {
  26. FileOutputStream fos = new FileOutputStream(fileName);
  27. ObjectOutputStream oos = new ObjectOutputStream(fos);
  28. oos.writeObject(obj);
  29. fos.close();
  30. }
  31. }
复制代码

注意一下,方法的参数是Object,它是任何Java类的基类,这样写法以一种很自然的方式保证了通用性。

现在我们来写一个测试程序,看一下Java序列化的实战。

  1. package com.journaldev.serialization;
  2. import java.io.IOException;
  3. public class SerializationTest {
  4. public static void main(String[] args) {
  5. String fileName="employee.ser";
  6. Employee emp = new Employee();
  7. emp.setId(100);
  8. emp.setName("Pankaj");
  9. emp.setSalary(5000);
  10. //serialize to file
  11. try {
  12. SerializationUtil.serialize(emp, fileName);
  13. } catch (IOException e) {
  14. e.printStackTrace();
  15. return;
  16. }
  17. Employee empNew = null;
  18. try {
  19. empNew = (Employee) SerializationUtil.deserialize(fileName);
  20. } catch (ClassNotFoundException | IOException e) {
  21. e.printStackTrace();
  22. }
  23. System.out.println("emp Object::"+emp);
  24. System.out.println("empNew Object::"+empNew);
  25. }
  26. }
复制代码

运行以上测试程序,可以得到以下输出。

  1. emp Object::Employee{name=Pankaj,id=100,salary=5000}
  2. empNew Object::Employee{name=Pankaj,id=100,salary=0}
复制代码

由于salary是一个transient变量,它的值不会被存入文件中,因此也不会在新的对象中被恢复。类似的,静态变量的值也不会被序列化,因为他们是属于类而非对象的。

使用序列化和serialVersionUID进行类重构

Java序列化允许java类中的一些变化,如果他们可以被忽略的话。一些不会影响到反序列化处理的变化有:

在类中添加一些新的变量。 将变量从transient转变为非tansient,对于序列化来说,就像是新加入了一个变量而已。 将变量从静态的转变为非静态的,对于序列化来说,就也像是新加入了一个变量而已。

不过这些变化要正常工作,java类需要具有为该类定义的serialVersionUID,我们来写一个测试类,只对之前测试类已经生成的序列化文件进行反序列化。

  1. package com.journaldev.serialization;
  2. import java.io.IOException;
  3. public class DeserializationTest {
  4. public static void main(String[] args) {
  5. String fileName="employee.ser";
  6. Employee empNew = null;
  7. try {
  8. empNew = (Employee) SerializationUtil.deserialize(fileName);
  9. } catch (ClassNotFoundException | IOException e) {
  10. e.printStackTrace();
  11. }
  12. System.out.println("empNew Object::"+empNew);
  13. }
  14. }
复制代码

现在,在Employee类中去掉”password”变量的注释和它的getter-setter方法,运行。你会得到以下异常。

  1. java.io.InvalidClassException: com.journaldev.serialization.Employee; local class incompatible: stream classdesc serialVersionUID = -6470090944414208496, local class serialVersionUID = -6234198221249432383
  2. at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:604)
  3. at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1601)
  4. at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1514)
  5. at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1750)
  6. at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1347)
  7. at java.io.ObjectInputStream.readObject(ObjectInputStream.java:369)
  8. at com.journaldev.serialization.SerializationUtil.deserialize(SerializationUtil.java:22)
  9. at com.journaldev.serialization.DeserializationTest.main(DeserializationTest.java:13)
  10. empNew Object::null
复制代码

原因很显然,上一个类和新类的serialVersionUID是不同的,事实上如果一个类没有定义serialVersionUID,它会自动计算出来并分配给该类。Java使用类变量、方法、类名称、包,等等来产生这个特殊的长数。如果你在任何一个IDE上工作,你都会得到警告“可序列化类Employee没有定义一个静态的final的serialVersionUID,类型为long”。

我们可以使用java工具”serialver”来产生一个类的serialVersionUID,对于Employee类,可以执行以下命令。

  1. SerializationExample/bin$serialver -classpath . com.journaldev.serialization.Employee
复制代码

记住,从程序本身生成序列版本并不是必须的,我们可以根据需要指定值,这个值的作用仅仅是告知反序列化处理机制,新的类是相同的类的新版本,应该进行可能的反序列化处理。

举个例子,在Employee类中仅仅将serialVersionUID字段的注释去掉,运行SerializationTest程序。现在再将Employee类中的password字段的注释去掉,运行DeserializationTest程序,你会看到对象流被成功地反序列化了,因为Employee类中的变动与序列化处理是相容的。

Java外部化接口

如果你在序列化处理中留个心,你会发现它是自动处理的。有时候我们想要去隐藏对象数据,来保持它的完整性,可以通过实现java.io.Externalizable接口,并提供writeExternal()和readExternal()方法的实现,它们被用于序列化处理。

  1. package com.journaldev.externalization;
  2. import java.io.Externalizable;
  3. import java.io.IOException;
  4. import java.io.ObjectInput;
  5. import java.io.ObjectOutput;
  6. public class Person implements Externalizable{
  7. private int id;
  8. private String name;
  9. private String gender;
  10. @Override
  11. public void writeExternal(ObjectOutput out) throws IOException {
  12. out.writeInt(id);
  13. out.writeObject(name+"xyz");
  14. out.writeObject("abc"+gender);
  15. }
  16. @Override
  17. public void readExternal(ObjectInput in) throws IOException,
  18. ClassNotFoundException {
  19. id=in.readInt();
  20. //read in the same order as written
  21. name=(String) in.readObject();
  22. if(!name.endsWith("xyz")) throw new IOException("corrupted data");
  23. name=name.substring(0, name.length()-3);
  24. gender=(String) in.readObject();
  25. if(!gender.startsWith("abc")) throw new IOException("corrupted data");
  26. gender=gender.substring(3);
  27. }
  28. @Override
  29. public String toString(){
  30. return "Person{id="+id+",name="+name+",gender="+gender+"}";
  31. }
  32. public int getId() {
  33. return id;
  34. }
  35. public void setId(int id) {
  36. this.id = id;
  37. }
  38. public String getName() {
  39. return name;
  40. }
  41. public void setName(String name) {
  42. this.name = name;
  43. }
  44. public String getGender() {
  45. return gender;
  46. }
  47. public void setGender(String gender) {
  48. this.gender = gender;
  49. }
  50. }
复制代码

注意,在将其转换为流之前,我已经更改了字段的值,之后读取时会得到这些更改,通过这种方式,可以在某种程度上保证数据的完整性,我们可以在读取流数据之后抛出异常,表明完整性检查失败。来看一个测试程序。

  1. package com.journaldev.externalization;
  2. import java.io.FileInputStream;
  3. import java.io.FileOutputStream;
  4. import java.io.IOException;
  5. import java.io.ObjectInputStream;
  6. import java.io.ObjectOutputStream;
  7. public class ExternalizationTest {
  8. public static void main(String[] args) {
  9. String fileName = "person.ser";
  10. Person person = new Person();
  11. person.setId(1);
  12. person.setName("Pankaj");
  13. person.setGender("Male");
  14. try {
  15. FileOutputStream fos = new FileOutputStream(fileName);
  16. ObjectOutputStream oos = new ObjectOutputStream(fos);
  17. oos.writeObject(person);
  18. oos.close();
  19. } catch (IOException e) {
  20. // TODO Auto-generated catch block
  21. e.printStackTrace();
  22. }
  23. FileInputStream fis;
  24. try {
  25. fis = new FileInputStream(fileName);
  26. ObjectInputStream ois = new ObjectInputStream(fis);
  27. Person p = (Person)ois.readObject();
  28. ois.close();
  29. System.out.println("Person Object Read="+p);
  30. } catch (IOException | ClassNotFoundException e) {
  31. e.printStackTrace();
  32. }
  33. }
  34. }
复制代码

运行以上测试程序,可以得到以下输出。

  1. Person Object Read=Person{id=1,name=Pankaj,gender=Male}
复制代码

那么哪个方式更适合被用来做序列化处理呢?实际上使用序列化接口更好,当你看到这篇教程的末尾时,你会知道原因的。

Java序列化方法

我们已经看到了,java的序列化是自动的,我们所要做的仅仅是实现序列化接口,其实现已经存在于ObjectInputStream和ObjectOutputStream类中了。不过如果我们想要更改存储数据的方式,比如说在对象中含有一些敏感信息,在存储/获取它们之前我们要进行加密/解密,这该怎么办呢?这就是为什么在类中我们拥有四种方法,能够改变序列化行为。

如果以下方法在类中存在,它们就会被用于序列化处理。

readObject(ObjectInputStream ois):如果这个方法存在,ObjectInputStream readObject()方法会调用该方法从流中读取对象。 writeObject(ObjectOutputStream oos):如果这个方法存在,ObjectOutputStream writeObject()方法会调用该方法从流中写入对象。一种普遍的用法是隐藏对象的值来保证完整性。 Object writeReplace():如果这个方法存在,那么在序列化处理之后,该方法会被调用并将返回的对象序列化到流中。 Object readResolve():如果这个方法存在,那么在序列化处理之后,该方法会被调用并返回一个最终的对象给调用程序。一种使用方法是在序列化类中实现单例模式,你可以从序列化和单例中读到更多知识。

通常情况下,当实现以上方法时,应该将其设定为私有类型,这样子类就无法覆盖它们了,因为它们本来就是为了序列化而建立的,设定为私有类型能避免一些安全性问题。

序列化结合继承

有时候我们需要对一个没有实现序列化接口的类进行扩展,如果依赖于自动化的序列化行为,而一些状态是父类拥有的,那么它们将不会被转换为流,因此以后也无法获取。

在此,readObject()和writeObject()就可以派上大用处了,通过提供它们的实现,我们可以将父类的状态存入流中,以便今后获取。我们来看一下实战。

  1. package com.journaldev.serialization.inheritance;
  2. public class SuperClass {
  3. private int id;
  4. private String value;
  5. public int getId() {
  6. return id;
  7. }
  8. public void setId(int id) {
  9. this.id = id;
  10. }
  11. public String getValue() {
  12. return value;
  13. }
  14. public void setValue(String value) {
  15. this.value = value;
  16. }
  17. }
复制代码

父类是一个简单的java bean,没有实现序列化接口。

  1. package com.journaldev.serialization.inheritance;
  2. import java.io.IOException;
  3. import java.io.InvalidObjectException;
  4. import java.io.ObjectInputStream;
  5. import java.io.ObjectInputValidation;
  6. import java.io.ObjectOutputStream;
  7. import java.io.Serializable;
  8. public class SubClass extends SuperClass implements Serializable, ObjectInputValidation{
  9. private static final long serialVersionUID = -1322322139926390329L;
  10. private String name;
  11. public String getName() {
  12. return name;
  13. }
  14. public void setName(String name) {
  15. this.name = name;
  16. }
  17. @Override
  18. public String toString(){
  19. return "SubClass{id="+getId()+",value="+getValue()+",name="+getName()+"}";
  20. }
  21. //adding helper method for serialization to save/initialize super class state
  22. private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
  23. ois.defaultReadObject();
  24. //notice the order of read and write should be same
  25. setId(ois.readInt());
  26. setValue((String) ois.readObject());
  27. }
  28. private void writeObject(ObjectOutputStream oos) throws IOException{
  29. oos.defaultWriteObject();
  30. oos.writeInt(getId());
  31. oos.writeObject(getValue());
  32. }
  33. @Override
  34. public void validateObject() throws InvalidObjectException {
  35. //validate the object here
  36. if(name == null || "".equals(name)) throw new InvalidObjectException("name can't be null or empty");
  37. if(getId() <=0) throw new InvalidObjectException("ID can't be negative or zero");
  38. }
  39. }
复制代码

注意,将额外数据写入流和读取流的顺序应该是一致的,我们可以在读与写之中添加一些逻辑,使其更安全。

同时还需要注意,这个类实现了ObjectInputValidation接口,通过实现validateObject()方法,可以添加一些业务验证来确保数据完整性没有遭到破坏。

以下通过编写一个测试类,看一下我们是否能够从序列化的数据中获取父类的状态。

  1. package com.journaldev.serialization.inheritance;
  2. import java.io.IOException;
  3. import com.journaldev.serialization.SerializationUtil;
  4. public class InheritanceSerializationTest {
  5. public static void main(String[] args) {
  6. String fileName = "subclass.ser";
  7. SubClass subClass = new SubClass();
  8. subClass.setId(10);
  9. subClass.setValue("Data");
  10. subClass.setName("Pankaj");
  11. try {
  12. SerializationUtil.serialize(subClass, fileName);
  13. } catch (IOException e) {
  14. e.printStackTrace();
  15. return;
  16. }
  17. try {
  18. SubClass subNew = (SubClass) SerializationUtil.deserialize(fileName);
  19. System.out.println("SubClass read = "+subNew);
  20. } catch (ClassNotFoundException | IOException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. }
复制代码

运行以上测试程序,可以得到以下输出。

  1. SubClass read = SubClass{id=10,value=Data,name=Pankaj}
复制代码

因此通过这种方式,可以序列化父类的状态,即便它没有实现序列化接口。当父类是一个我们无法改变的第三方的类,这个策略就有用武之地了。

序列化代理模式

Java序列化也带来了一些严重的误区,比如:

类的结构无法大量改变,除非中断序列化处理,因此即便我们之后已经不需要某些变量了,我们也需要保留它们,仅仅是为了向后兼容。 序列化会导致巨大的安全性危机,一个攻击者可以更改流的顺序,继而对系统造成伤害。举个例子,用户角色被序列化了,攻击者可以更改流的值为admin,再执行恶意代码。

序列化代理模式是一种使序列化能达到极高安全性的方式,在这个模式下,一个内部的私有静态类被用作序列化的代理类,该类的设计目的是用于保留主类的状态。这个模式的实现需要合理实现readResolve()和writeReplace()方法。

让我们先来写一个类,实现了序列化代码模式,之后再对其进行分析,以便更好的理解原理。

  1. package com.journaldev.serialization.proxy;
  2. import java.io.InvalidObjectException;
  3. import java.io.ObjectInputStream;
  4. import java.io.Serializable;
  5. public class Data implements Serializable{
  6. private static final long serialVersionUID = 2087368867376448459L;
  7. private String data;
  8. public Data(String d){
  9. this.data=d;
  10. }
  11. public String getData() {
  12. return data;
  13. }
  14. public void setData(String data) {
  15. this.data = data;
  16. }
  17. @Override
  18. public String toString(){
  19. return "Data{data="+data+"}";
  20. }
  21. //serialization proxy class
  22. private static class DataProxy implements Serializable{
  23. private static final long serialVersionUID = 8333905273185436744L;
  24. private String dataProxy;
  25. private static final String PREFIX = "ABC";
  26. private static final String SUFFIX = "DEFG";
  27. public DataProxy(Data d){
  28. //obscuring data for security
  29. this.dataProxy = PREFIX + d.data + SUFFIX;
  30. }
  31. private Object readResolve() throws InvalidObjectException {
  32. if(dataProxy.startsWith(PREFIX) && dataProxy.endsWith(SUFFIX)){
  33. return new Data(dataProxy.substring(3, dataProxy.length() -4));
  34. }else throw new InvalidObjectException("data corrupted");
  35. }
  36. }
  37. //replacing serialized object to DataProxy object
  38. private Object writeReplace(){
  39. return new DataProxy(this);
  40. }
  41. private void readObject(ObjectInputStream ois) throws InvalidObjectException{
  42. throw new InvalidObjectException("Proxy is not used, something fishy");
  43. }
  44. }
复制代码
Data和DataProxy类都应该实现序列化接口。 DataProxy应该能够保留Data对象的状态。 DataProxy是一个内部的私有静态类,因此其他类无法访问它。 DataProxy应该有一个单独的构造方法,接收Data作为参数。 Data类应该提供writeReplace()方法,返回DataProxy实例,这样当Data对象被序列化时,返回的流是属于DataProxy类的,不过DataProxy类在外部是不可见的,所有它不能被直接使用。 DataProxy应该实现readResolve()方法,返回Data对象,这样当Data类被反序列化时,在内部其实是DataProxy类被反序列化了,之后它的readResolve()方法被调用,我们得到了Data对象。 最后,在Data类中实现readObject()方法,抛出InvalidObjectException异常,防止黑客通过伪造Data对象的流并对其进行解析,继而执行攻击。

我们来写一个小测试,检查一下这样的实现是否能工作。

  1. package com.journaldev.serialization.proxy;
  2. import java.io.IOException;
  3. import com.journaldev.serialization.SerializationUtil;
  4. public class SerializationProxyTest {
  5. public static void main(String[] args) {
  6. String fileName = "data.ser";
  7. Data data = new Data("Pankaj");
  8. try {
  9. SerializationUtil.serialize(data, fileName);
  10. } catch (IOException e) {
  11. e.printStackTrace();
  12. }
  13. try {
  14. Data newData = (Data) SerializationUtil.deserialize(fileName);
  15. System.out.println(newData);
  16. } catch (ClassNotFoundException | IOException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. }
复制代码

运行以上测试程序,可以得到以下输出。

  1. Data{data=Pankaj}
复制代码

如果你打开data.ser文件,可以看到DataProxy对象已经被作为流存入了文件中。

这就是Java序列化的所有内容,看上去很简单但我们应当谨慎地使用它,通常来说,最好不要依赖于默认实现。你可以从上面的链接中下载项目,玩一玩,这能让你学到更多。

原文链接: Java Serialization Example Tutorial, Serializable, serialVersionUID 翻译: ImportNew.com - Justin Wu
译文链接: http://www.importnew.com/14465.html

最新评论

小黑屋|在路上 ( 蜀ICP备15035742号-1 

;

GMT+8, 2025-7-8 01:38

Copyright 2015-2025 djqfx

返回顶部