本文共 11652 字,大约阅读时间需要 38 分钟。
在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。
序列化:指将Java对象数据保存到磁盘文件中或者传递给其他网络的节点(在网络上传输)。
反序列化:指将磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象的过程为反序列化。
对象序列化机制允许把内存中的Java对象转换成与平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。其他程序一旦获得了这种二进制流,都可以将这种二进制流恢复成原来的Java对象。
1、为什么要做序列化?
1)在分布式系统中,需要共享数据的JavaBean对象,都得做序列化,此时需要把对象在网络上传输,就得把对象数据转换为二进制形式(只有实现序列化接口的类,才能做序列化操作)。
2)服务钝化:如果服务发现某些对象好久都没有活动了,此时服务器就会把这些内存中的对象,持久化在本地磁盘文件中(Java对象-->二进制文件)。 如果某些对象需要活动的时候,先在内存中去寻找,找到就使用,找不到再去磁盘文件中,找到反序列化得对象数据,恢复成Java对象。
2、Java序列化对象版本号--serialVersionUID
1)随着项目的升级,系统的class文件也会改变(如增加/删除一个字段),如何保证两个class文件的兼容性?
Java的序列化机制是通过在运行时判断类的serialVersionUID(序列化版本号)来验证版本的一致性。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。
如果不显示定义 serialVersionUID类变量,该类变量的值由JVM根据类相关信息计算,而修改后的类的计算方式和之前往往不同,从而造成了对象反序列化因为版本不兼容而失败的问题。所以, 解决方案:在类中提供一个固定的 serialVersionUID 值。
2)显式地定义 serialVersionUID 有两种用途
(1)在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。
(2)如果不设置serialVersionUID, 当序列化了一个类实例后,如果更改一个字段或添加一个字段, 对类实例所做的任何更改都将导致无法反序化旧有实例,并在反序列化时抛出一个异常。
如果设置了serialVersionUID,在反序列旧有实例时,新添加或更改的字段值将设为初始化值(对象型为null,基本类型为其初始默认值),字段被删除将不设置初始化值。
3、序列化需要注意的几个问题
static 和 transient 修饰的字段是不会被序列化的。字段的值被设为初始值,(对象型为null,基本类型为其初始默认值),静态成员属于类级别的,所以不能序列化。参考文章:
1)Transient 关键字
Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,
Transient 关键字只能用于修饰Field,不可修饰Java程序中的其他成分。
2)Java对象的class文件
必须确保该读取程序的 CLASSPATH 中包含有 Java对象的class文件,否则会抛出 ClassNotFoundException。
3)字段为引用对象时
需要序列化的Java对象和字段为引用对象的这两个都必须是可序列化的,否则Java对象将不可序列化。
简单来说,Java 对象序列化就是把对象写入到输出流中,用来存储或传输;反序列化就是从输入流中读取对象。
如果需要让某个java对象支持序列化机制,实现方式有两种。
注意:
1)对象的序列化是基于字节的流,不能使用基于字符的流。
2)自定义的枚举类是直接可以被序列化和反序列化。因为每个枚举类都会默认继承java.lang.Enum类,而Enum类实现了Serializable接口,所以枚举类型对象都是默认可以被序列化的。
方式一:实现Serializable接口,通过序列化流
java.io.Serializable接口是个标志接口,用于标识该类可以被序列化,没有抽象方法。在Java中大多数类都已经实现Serializable接口。底层会判断,如果当前对象是Serializable的实例,才允许做序列化.。 boolean ret = Java对象 instanceof Serializable;
public class User implements Serializable { private static final long serialVersionUID = 5301525230834919001L; private Long id; private String username; transient private String passwoord; private int age; public User() { System.out.println("调用无参数构造器"); } public User(Long id, String username, String passwoord, int age) { this.id = id; this.username = username; this.passwoord = passwoord; this.age = age; System.out.println("调用有参数构造器"); }...}
1、实例demo
需要做序列化的java对象实现Serializable接口,然后通过对象字节流将对象序列化和反序列化。
public static void main(String[] args) { User user = new User(); user.setId(1L); user.setUsername("lisi"); user.setPasswoord("123456"); ObjectOutputStream objectOutputStream = null; ObjectInputStream objectInputStream = null; try { // 将 user对象序列化到 user.txt文件(二进制数据) File file = new File("D:/E/user.txt"); objectOutputStream = new ObjectOutputStream(new FileOutputStream(file)); objectOutputStream.writeObject(user); // 从user.txt文件反序列化输出 user对象 objectInputStream = new ObjectInputStream(new FileInputStream(file)); User user1 = (User) objectInputStream.readObject(); System.out.println(user1); } catch (Exception e) { e.printStackTrace(); } finally { if(objectOutputStream != null){ try { objectOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
方式二:实现Externalizable接口,重写writeExternal和readExternal方法
java.io.Externalizable接口继承了Serializable接口,使用Externalizable接口需要实现writeExternal(用于序列化)以及readExternal(用于反序列化)方法。
注意:这种方式 transient修饰词将失去作用,即使你使用transient修饰属性,只要在writeExternal方法中序列化了该属性,照样也会进行序列化。
1、实例demo
public class User implements Externalizable { private static final long serialVersionUID = 5301525230834919001L; private Long id; private String username; transient private String passwoord; private int age; @Override public void writeExternal(ObjectOutput out) throws IOException { // 需要序列化的字段 out.writeObject(username); out.writeObject(passwoord); out.writeInt(age); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { // 被序列化的字段,注意:必须和序列化的字段顺序保持一致 username = (String) in.readObject(); passwoord = (String) in.readObject(); age = in.readInt(); }...}
main 中流处理同上
注意:使用 Externalizable 接口进行序列化时,读取对象会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中,所以,实现 Externalizable 接口的类必须要提供一个无参构造器,且它的访问权限为public。
如果只想将Java对象中部分属性进行序列化,可以使用使用Serializable接口和transient关键字配合使用,也可以使用Externalizable接口,重写writeExternal和readExternal方法。这也是它们的区别。
1、readResolve()方法——单例模式的反序列化(了解)
当使用Singleton单例模式时,某个类的实例是唯一的,但如果该类是可序列化的,那么情况可能略有不同。反序列化的时候为创建对象,所以是不唯一的。如果在序列化过程仍要保持单例的特性,可以在Java对象中添加一个readResolve()方法,在该方法中直接返回Person的单例对象即可。
原理就是当从 I/O 流中读取对象时,ObjectInputStream 类里有 readResolve() 方法,该方法会被自动调用,然后经过种种逻辑,最后会调用到可序列化类里的 readResolve()方法,这样可以用 readResolve() 中返回的单例对象直接替换在反序列化过程中创建的对象,实现单例特性。也就是说,无论如何,反序列化都会额外创建对象,只不过使用 readResolve() 方法可以替换之。
public class User implements Serializable { private static final long serialVersionUID = 5301525230834919001L; private Long id; private String username; transient private String passwoord; private int age; private User() { System.out.println("调用无参数构造器"); } public static final User INSTANCE = new User(); public static User getInstance(){ return INSTANCE; }// 在该方法中直接返回类的单例对象// public Object readResolve(){// return INSTANCE;// }...}
实例demo
public static void main(String[] args) { User user = User.getInstance(); user.setId(1L); user.setUsername("lisi"); user.setPasswoord("123456"); ObjectOutputStream objectOutputStream = null; ObjectInputStream objectInputStream = null; try { // 将 user对象序列化到 user.txt文件(二进制数据) File file = new File("D:/E/user.txt"); objectOutputStream = new ObjectOutputStream(new FileOutputStream(file)); objectOutputStream.writeObject(user); // 从user.txt文件反序列化输出 user对象 objectInputStream = new ObjectInputStream(new FileInputStream(file)); User user1 = (User) objectInputStream.readObject(); System.out.println(user1); System.out.println("反序列化后的对象是不是前面的单例user对象:" + (user == user1)); } catch (Exception e) { e.printStackTrace(); } finally { if(objectOutputStream != null){ try { objectOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
服务器端给客户端发送序列化对象数据,序列化二进制格式的数据写在文档中,并且完全可逆。要是被别人抓包就能获取数据内容。所以,在序列化与反序列化时,可以对数据进行加解密操作,从而一定程度上保证序列化对象的数据安全。
Java提供了对整个对象进行加密和签名的方式。就是将Java对象包装在 javax.crypto.SealedObject 或 java.security.SignedObject中,然后进行序列化机制。
在 SealedObject(一个秘钥) 与SignedObject(一对秘钥) 中,指明使用哪种加密算法,然后通过秘钥对实现加解密操作,从而校验数据是否被人篡改和序列化的安全性。
demo:序列化之后篡改一下数据,然后反序列化,看结果。
1、使用 SealedObject, 算法用 DESede
public static void main(String[] args) throws IOException { User user = User.getInstance(); user.setId(1L); user.setUsername("lisi"); user.setPasswoord("123456"); ObjectOutputStream objectOutputStream = null; ObjectInputStream objectInputStream = null; ObjectOutputStream objoutEncryptKey = null; ObjectInputStream objGetEncryptKey = null; try { // 将 user对象序列化到 user.txt文件(二进制数据),加密 File file = new File("D:/E/user.txt"); /* objectOutputStream = new ObjectOutputStream(new FileOutputStream(file)); KeyGenerator keyGenerator = KeyGenerator.getInstance("DESede"); // 获取秘钥 SecretKey encryptKey = keyGenerator.generateKey(); System.out.println(encryptKey); Cipher cipher = Cipher.getInstance("DESede"); cipher.init(Cipher.ENCRYPT_MODE, encryptKey); SealedObject sealedObject = new SealedObject(user, cipher); objectOutputStream.writeObject(sealedObject); //将秘钥保存 objoutEncryptKey = new ObjectOutputStream(new FileOutputStream(new File("D:/E/encryptKey.txt"))); objoutEncryptKey.writeObject(encryptKey);*/ // 从user.txt文件反序列化输出 user对象,解密 objectInputStream = new ObjectInputStream(new FileInputStream(file)); SealedObject sealedObjectResult = (SealedObject) objectInputStream.readObject(); objGetEncryptKey = new ObjectInputStream(new FileInputStream(new File("D:/E/encryptKey.txt"))); SecretKey openKey = (SecretKey) objGetEncryptKey.readObject(); User user1 = (User) sealedObjectResult.getObject(openKey); System.out.println(user1); } catch (Exception e) { e.printStackTrace(); } finally { objectOutputStream.close(); objectInputStream.close(); objoutEncryptKey.close(); objGetEncryptKey.close(); } }
2、使用 SignedObject, 算法用 DSA
public static void main(String[] args) throws IOException { User user = User.getInstance(); user.setId(1L); user.setUsername("lisi"); user.setPasswoord("123456"); ObjectOutputStream objectOutputStream = null; ObjectInputStream objectInputStream = null; ObjectOutputStream objoutEncryptKey = null; ObjectInputStream objGetEncryptKey = null; try { // 将 user对象序列化到 user.txt文件(二进制数据),私钥加密 File file = new File("D:/E/user.txt"); objectOutputStream = new ObjectOutputStream(new FileOutputStream(file)); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); keyPairGenerator.initialize(1024); KeyPair keyPair = keyPairGenerator.generateKeyPair(); // 获取秘钥对 PrivateKey privateKey = keyPair.getPrivate(); PublicKey publicKey = keyPair.getPublic(); Signature signature = Signature.getInstance("DSA"); SignedObject signedObject = new SignedObject(user, privateKey, signature); objectOutputStream.writeObject(signedObject); //将公钥保存,供客户端使用 objoutEncryptKey = new ObjectOutputStream(new FileOutputStream(new File("D:/E/publicKey.txt"))); objoutEncryptKey.writeObject(publicKey); // 从user.txt文件反序列化输出 user对象,公钥解密 objectInputStream = new ObjectInputStream(new FileInputStream(file)); SignedObject signedObjectResult = (SignedObject) objectInputStream.readObject(); objGetEncryptKey = new ObjectInputStream(new FileInputStream(new File("D:/E/publicKey.txt"))); PublicKey openKey = (PublicKey) objGetEncryptKey.readObject(); //方法verify,判断盒子里的对象有没有没篡改 Signature verifySignature = Signature.getInstance("DSA"); if (signedObjectResult.verify(openKey, verifySignature)) { //内容没被篡改 System.out.println("内容没被篡改"); User user1 = (User) signedObjectResult.getObject(); System.out.println(user1); }else{ System.out.println("内容被篡改过!"); } } catch (Exception e) { e.printStackTrace(); } finally { objectOutputStream.close(); objectInputStream.close(); objoutEncryptKey.close(); objGetEncryptKey.close(); } }
—— Stay Hungry. Stay Foolish. 求知若饥,虚心若愚。
转载地址:http://hgdgn.baihongyu.com/