Java中的序列化 - Java序列化
Java中的序列化是在JDK 1.1中引入的,它是Core Java的重要特性之一。
Java中的序列化
Java中的序列化允许我们将Object转换为可以通过网络发送的流,或者将其保存为文件或存储在DB中以供以后使用。反序列化是将对象流转换为在我们的程序中使用的实际Java对象的过程。Java中的序列化起初看起来非常容易使用,但它带来了一些简单的安全性和完整性问题,我们将在本文的后面部分介绍这些问题。我们将在本教程中研究以下主题。
可在Java中序列化
如果您希望类对象可序列化,那么您只需要实现该java.io.Serializable
接口即可。java中的Serializable是一个标记接口,没有要实现的字段或方法。这就像一个Opt-In过程,通过它我们可以将类序列化。
java中的序列化是由ObjectInputStream
和实现的ObjectOutputStream
,因此我们需要的是它们的包装器,以将其保存到文件或通过网络发送。让我们在java程序示例中看到一个简单的序列化。
package com.journaldev.serialization;import java.io.Serializable;public class Employee implements Serializable {// private static final long serialVersionUID = -6470090944414208496L; private String name; private int id; transient private int salary;// private String password; @Override public String toString(){ return "Employee{name="+name+",id="+id+",salary="+salary+"}"; } //getter and setter methods public String getName() { return name; } public void setName(String name) { this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public int getSalary() { return salary; } public void setSalary(int salary) { this.salary = salary; }// public String getPassword() {// return password;// }//// public void setPassword(String password) {// this.password = password;// } }
请注意,它是一个带有一些属性和getter-setter方法的简单java bean。如果您希望将对象属性序列化为流,则可以像使用薪水变量一样使用transient关键字。
现在假设我们要将对象写入文件,然后从同一文件反序列化它。因此,我们需要使用ObjectInputStream
和ObjectOutputStream
用于序列化的实用方法。
package com.journaldev.serialization;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;/** * A simple class with generic serialize and deserialize method implementations * * @author pankaj * */public class SerializationUtil { // deserialize to Object from given file public static Object deserialize(String fileName) throws IOException, ClassNotFoundException { FileInputStream fis = new FileInputStream(fileName); ObjectInputStream ois = new ObjectInputStream(fis); Object obj = ois.readObject(); ois.close(); return obj; } // serialize the given object and save it to file public static void serialize(Object obj, String fileName) throws IOException { FileOutputStream fos = new FileOutputStream(fileName); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(obj); fos.close(); }}
请注意,方法参数使用Object,它是任何java对象的基类。它以这种方式编写,本质上是通用的。
现在让我们编写一个测试程序来查看Java Serialization的实际运行情况。
package com.journaldev.serialization;import java.io.IOException;public class SerializationTest { public static void main(String[] args) { String fileName="employee.ser"; Employee emp = new Employee(); emp.setId(100); emp.setName("Pankaj"); emp.setSalary(5000); //serialize to file try { SerializationUtil.serialize(emp, fileName); } catch (IOException e) { e.printStackTrace(); return; } Employee empNew = null; try { empNew = (Employee) SerializationUtil.deserialize(fileName); } catch (ClassNotFoundException | IOException e) { e.printStackTrace(); } System.out.println("emp Object::"+emp); System.out.println("empNew Object::"+empNew); }}
当我们在java上运行上面的序列化测试程序时,我们得到以下输出。
emp Object::Employee{name=Pankaj,id=100,salary=5000}empNew Object::Employee{name=Pankaj,id=100,salary=0}
由于薪水是一个瞬态变量,因此它的值未保存到文件中,因此无法在新对象中检索。类似地,静态变量值也不是序列化的,因为它们属于类而不是对象。
使用序列化和serialVersionUID进行类重构
如果可以忽略java中的序列化,则允许在java类中进行一些更改。类中的一些不会影响反序列化过程的更改是:
在类中添加新变量
将变量从瞬态更改为非瞬态,对于序列化,就像拥有一个新字段一样。
将变量从静态更改为非静态,对于序列化,就像拥有一个新字段一样。
但是要使所有这些更改生效,java类应该为该类定义serialVersionUID。让我们编写一个测试类,用于反序列化先前测试类中已经序列化的文件。
package com.journaldev.serialization;import java.io.IOException;public class DeserializationTest { public static void main(String[] args) { String fileName="employee.ser"; Employee empNew = null; try { empNew = (Employee) SerializationUtil.deserialize(fileName); } catch (ClassNotFoundException | IOException e) { e.printStackTrace(); } System.out.println("empNew Object::"+empNew); }}
现在取消注释“password”变量及其来自Employee类的getter-setter方法并运行它。你会得到以下异常;
java.io.InvalidClassException: com.journaldev.serialization.Employee; local class incompatible: stream classdesc serialVersionUID = -6470090944414208496, local class serialVersionUID = -6234198221249432383 at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:604) at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1601) at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1514) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1750) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1347) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:369) at com.journaldev.serialization.SerializationUtil.deserialize(SerializationUtil.java:22) at com.journaldev.serialization.DeserializationTest.main(DeserializationTest.java:13)empNew Object::null
原因很清楚,前一个类和新类的serialVersionUID是不同的。实际上,如果该类没有定义serialVersionUID,它将自动计算并分配给该类。Java使用类变量,方法,类名,包等来生成这个唯一的长数。如果您正在使用任何IDE,您将自动收到“可序列化类Employee未声明long类型的静态最终serialVersionUID字段”的警告。
我们可以使用java实用程序“serialver”生成类serialVersionUID,对于Employee类,我们可以使用下面的命令运行它。
SerializationExample/bin$serialver -classpath . com.journaldev.serialization.Employee
请注意,不需要从该程序本身生成串行版本,我们可以根据需要分配此值。它只需要在那里让反序列化过程知道新类是同一类的新版本,并且应该反序列化。
例如,仅从Employee
类中取消注释serialVersionUID字段并运行SerializationTest
程序。现在取消注释Employee类中的密码字段并运行该DeserializationTest
程序,您将看到对象流已成功反序列化,因为Employee类中的更改与序列化过程兼容。
Java可外化接口
如果你注意到java序列化过程,它会自动完成。有时我们想隐藏对象数据以保持其完整性。我们可以通过实现java.io.Externalizable
接口来实现这一点,并提供writeExternal()和readExternal()方法的实现,以便在序列化过程中使用。
package com.journaldev.externalization;import java.io.Externalizable;import java.io.IOException;import java.io.ObjectInput;import java.io.ObjectOutput;public class Person implements Externalizable{ private int id; private String name; private String gender; @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(id); out.writeObject(name+"xyz"); out.writeObject("abc"+gender); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { id=in.readInt(); //read in the same order as written name=(String) in.readObject(); if(!name.endsWith("xyz")) throw new IOException("corrupted data"); name=name.substring(0, name.length()-3); gender=(String) in.readObject(); if(!gender.startsWith("abc")) throw new IOException("corrupted data"); gender=gender.substring(3); } @Override public String toString(){ return "Person{id="+id+",name="+name+",gender="+gender+"}"; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; }}
请注意,我在将字段值转换为Stream之前更改了字段值,然后在读取时反转了更改。通过这种方式,我们可以保持某些类型的数据完整性。如果在读取流数据后,我们可以抛出异常,完整性检查失败。让我们编写一个测试程序来查看它的实际运行情况。
package com.journaldev.externalization;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;public class ExternalizationTest { public static void main(String[] args) { String fileName = "person.ser"; Person person = new Person(); person.setId(1); person.setName("Pankaj"); person.setGender("Male"); try { FileOutputStream fos = new FileOutputStream(fileName); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(person); oos.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } FileInputStream fis; try { fis = new FileInputStream(fileName); ObjectInputStream ois = new ObjectInputStream(fis); Person p = (Person)ois.readObject(); ois.close(); System.out.println("Person Object Read="+p); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } }}
当我们运行上面的程序时,我们得到以下输出。
Person Object Read=Person{id=1,name=Pankaj,gender=Male}
那么哪一个更适合用于java中的序列化。实际上最好使用Serializable接口,当我们到文章末尾时,你会知道原因。
Java序列化方法
我们已经看到java中的序列化是自动的,我们需要的只是实现Serializable接口。该实现存在于ObjectInputStream和ObjectOutputStream类中。但是,如果我们想要改变保存数据的方式,例如我们在对象中有一些敏感信息,在保存/检索之前我们想要加密/解密它。这就是为什么我们可以在类中提供四种方法来改变序列化行为。
如果类中存在这些方法,则它们用于序列化目的。
readObject(ObjectInputStream ois):如果该方法存在于类中,ObjectInputStream readObject()方法将使用此方法从流中读取对象。
writeObject(ObjectOutputStream oos):如果该方法存在于类中,ObjectOutputStream writeObject()方法将使用此方法将对象写入流中。常见的用法之一是模糊对象变量以维护数据完整性。
Object writeReplace():如果存在此方法,则在序列化过程之后调用此方法,并将返回的对象序列化到流中。
Object readResolve():如果存在此方法,则在反序列化过程之后,将调用此方法以将最终对象返回给调用者程序。此方法的一个用法是使用序列化类实现Singleton模式。阅读Serialization和Singleton的更多内容。
通常在实现上述方法时,它保持为私有,以便子类不能覆盖它们。它们仅用于序列化目的,并将它们保密,可避免任何安全问题。
继承序列化
有时我们需要扩展一个不实现Serializable接口的类。如果我们依赖于自动序列化行为并且超类具有某种状态,那么它们将不会转换为流,因此以后不会被检索。
这是一个地方,其中readObject()和writeObject()方法确实有帮助。通过提供它们的实现,我们可以将超类状态保存到流中,然后再检索它。让我们看看这个在行动。
package com.journaldev.serialization.inheritance;public class SuperClass { private int id; private String value; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } }
SuperClass是一个简单的java bean,但它没有实现Serializable接口。
package com.journaldev.serialization.inheritance;import java.io.IOException;import java.io.InvalidObjectException;import java.io.ObjectInputStream;import java.io.ObjectInputValidation;import java.io.ObjectOutputStream;import java.io.Serializable;public class SubClass extends SuperClass implements Serializable, ObjectInputValidation{ private static final long serialVersionUID = -1322322139926390329L; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString(){ return "SubClass{id="+getId()+",value="+getValue()+",name="+getName()+"}"; } //adding helper method for serialization to save/initialize super class state private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{ ois.defaultReadObject(); //notice the order of read and write should be same setId(ois.readInt()); setValue((String) ois.readObject()); } private void writeObject(ObjectOutputStream oos) throws IOException{ oos.defaultWriteObject(); oos.writeInt(getId()); oos.writeObject(getValue()); } @Override public void validateObject() throws InvalidObjectException { //validate the object here if(name == null || "".equals(name)) throw new InvalidObjectException("name can't be null or empty"); if(getId() <=0) throw new InvalidObjectException("ID can't be negative or zero"); } }
请注意,写入和读取流中的额外数据的顺序应该相同。我们可以在读取和写入数据时加入一些逻辑以使其安全。
还要注意该类正在实现ObjectInputValidation
接口。通过实现validateObject()方法,我们可以进行一些业务验证,以确保数据完整性不会受到损害。
让我们编写一个测试类,看看我们是否可以从序列化数据中检索超类状态。
package com.journaldev.serialization.inheritance;import java.io.IOException;import com.journaldev.serialization.SerializationUtil;public class InheritanceSerializationTest { public static void main(String[] args) { String fileName = "subclass.ser"; SubClass subClass = new SubClass(); subClass.setId(10); subClass.setValue("Data"); subClass.setName("Pankaj"); try { SerializationUtil.serialize(subClass, fileName); } catch (IOException e) { e.printStackTrace(); return; } try { SubClass subNew = (SubClass) SerializationUtil.deserialize(fileName); System.out.println("SubClass read = "+subNew); } catch (ClassNotFoundException | IOException e) { e.printStackTrace(); } }}
当我们上课时,我们得到以下输出。
SubClass read = SubClass{id=10,value=Data,name=Pankaj}
因此,通过这种方式,我们可以序列化超类状态,即使它没有实现Serializable接口。当超级类是我们无法改变的第三方类时,这种策略很方便。
序列化代理模式
java中的序列化带来了一些严重的缺陷,例如;
在不破坏java序列化过程的情况下,类结构不能改变很多。因此,即使我们以后不需要一些变量,我们也需要保留它们以便向后兼容。
序列化会带来巨大的安全风险,攻击者可能会改变流序列并对系统造成伤害。例如,用户角色被序列化,攻击者更改流值以使其成为管理员并运行恶意代码。
Java Serialization Proxy模式是一种通过Serialization实现更高安全性的方法。在此模式中,内部私有静态类用作序列化目的的代理类。此类的设计方式是维护主类的状态。通过正确实现readResolve()和writeReplace()方法来实现此模式。
让我们首先编写一个实现序列化代理模式的类,然后我们将对其进行分析以便更好地理解。
package com.journaldev.serialization.proxy;import java.io.InvalidObjectException;import java.io.ObjectInputStream;import java.io.Serializable;public class Data implements Serializable{ private static final long serialVersionUID = 2087368867376448459L; private String data; public Data(String d){ this.data=d; } public String getData() { return data; } public void setData(String data) { this.data = data; } @Override public String toString(){ return "Data{data="+data+"}"; } //serialization proxy class private static class DataProxy implements Serializable{ private static final long serialVersionUID = 8333905273185436744L; private String dataProxy; private static final String PREFIX = "ABC"; private static final String SUFFIX = "DEFG"; public DataProxy(Data d){ //obscuring data for security this.dataProxy = PREFIX + d.data + SUFFIX; } private Object readResolve() throws InvalidObjectException { if(dataProxy.startsWith(PREFIX) && dataProxy.endsWith(SUFFIX)){ return new Data(dataProxy.substring(3, dataProxy.length() -4)); }else throw new InvalidObjectException("data corrupted"); } } //replacing serialized object to DataProxy object private Object writeReplace(){ return new DataProxy(this); } private void readObject(ObjectInputStream ois) throws InvalidObjectException{ throw new InvalidObjectException("Proxy is not used, something fishy"); }}
两者
Data
和DataProxy
类都应该实现Serializable接口。DataProxy
应该能够维护Data对象的状态。DataProxy
是内部私有静态类,因此其他类无法访问它。DataProxy
应该有一个将Data作为参数的构造函数。Data
class应该提供writeReplace()方法返回DataProxy
实例。因此,当序列化Data对象时,返回的流是DataProxy类。但是DataProxy类在外部不可见,因此无法直接使用。DataProxy
class应该实现readResolve()方法返回Data
对象。因此,当反序列化Data类时,内部DataProxy被反序列化,当调用readResolve()方法时,我们得到Data对象。最后在Data类中实现readObject()方法并抛出
InvalidObjectException
以避免黑客攻击尝试构造Data对象流并解析它。
让我们写一个小测试来检查实现是否有效。
package com.journaldev.serialization.proxy;import java.io.IOException;import com.journaldev.serialization.SerializationUtil;public class SerializationProxyTest { public static void main(String[] args) { String fileName = "data.ser"; Data data = new Data("Pankaj"); try { SerializationUtil.serialize(data, fileName); } catch (IOException e) { e.printStackTrace(); } try { Data newData = (Data) SerializationUtil.deserialize(fileName); System.out.println(newData); } catch (ClassNotFoundException | IOException e) { e.printStackTrace(); } }}
当我们在上面运行时,我们在控制台中得到低于输出。
Data{data=Pankaj}
如果要打开data.ser文件,则可以看到DataProxy对象在文件中保存为流。
这就是Java中的序列化,它看起来很简单,但我们应该明智地使用它,最好不要依赖默认实现。从上面的链接下载项目并使用它来了解更多信息。