在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,基本类型为其初始默认值),静态成员属于类级别的,所以不能序列化。参考文章:JAVA中序列化和反序列化中的静态成员问题
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. 求知若饥,虚心若愚。