你知道序列化可使用代理吗?你知道序列化的安全性吗?每一个java程序员都据说过序列化,要存储对象须要序列化,要在网络上传输对象要序列化,看起来很简单的序列化其实里面还隐藏着不少小秘密,今天本文将会为你们一一揭秘。java
更多精彩内容且看:程序员
更多内容请访问 www.flydean.com
序列化就是将java对象按照必定的顺序组织起来,用于在网络上传输或者写入存储中。而反序列化就是从网络中或者存储中读取存储的对象,将其转换成为真正的java对象。算法
因此序列化的目的就是为了传输对象,对于一些复杂的对象,咱们可使用第三方的优秀框架,好比Thrift,Protocol Buffer等,使用起来很是的方便。安全
JDK自己也提供了序列化的功能。要让一个对象可序列化,则能够实现java.io.Serializable接口。网络
java.io.Serializable是从JDK1.1开始就有的接口,它其实是一个marker interface,由于java.io.Serializable并无须要实现的接口。继承java.io.Serializable就代表这个class对象是能够被序列化的。框架
@Data @AllArgsConstructor public class CustUser implements java.io.Serializable{ private static final long serialVersionUID = -178469307574906636L; private String name; private String address; }
上面咱们定义了一个CustUser可序列化对象。这个对象有两个属性:name和address。函数
接下看下怎么序列化和反序列化:性能
public void testCusUser() throws IOException, ClassNotFoundException { CustUser custUserA=new CustUser("jack","www.flydean.com"); CustUser custUserB=new CustUser("mark","www.flydean.com"); try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(custUserA); objectOutputStream.writeObject(custUserB); } try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); CustUser custUser1 = (CustUser) objectInputStream.readObject(); CustUser custUser2 = (CustUser) objectInputStream.readObject(); log.info("{}",custUser1); log.info("{}",custUser2); } }
上面的例子中,咱们实例化了两个CustUser对象,并使用objectOutputStream将对象写入文件中,最后使用ObjectInputStream从文件中读取对象。区块链
上面是最基本的使用。须要注意的是CustUser class中有一个serialVersionUID字段。this
serialVersionUID是序列化对象的惟一标记,若是class中定义的serialVersionUID和序列化存储中的serialVersionUID一致,则代表这两个对象是一个对象,咱们能够将存储的对象反序列化。
若是咱们没有显示的定义serialVersionUID,则JVM会自动根据class中的字段,方法等信息生成。不少时候我在看代码的时候,发现不少人都将serialVersionUID设置为1L,这样作是不对的,由于他们没有理解serialVersionUID的真正含义。
假如咱们有一个序列化的对象正在使用了,可是忽然咱们发现这个对象好像少了一个字段,要把他加上去,可不能够加呢?加上去以后原序列化过的对象能不能转换成这个新的对象呢?
答案是确定的,前提是两个版本的serialVersionUID必须同样。新加的字段在反序列化以后是空值。
有不少同窗在使用序列化的过程当中可能会这样想,序列化已经将对象变成了二进制文件,是否是说该对象已经被加密了呢?
这实际上是序列化的一个误区,序列化并非加密,由于即便你序列化了,仍是能从序列化以后的数据中知道你的类的结构。好比在RMI远程调用的环境中,即便是class中的private字段也是能够从stream流中解析出来的。
若是咱们想在序列化的时候对某些字段进行加密操做该怎么办呢?
这时候能够考虑在序列化对象中添加writeObject和readObject方法:
private String name; private String address; private int age; private void writeObject(ObjectOutputStream stream) throws IOException { //给age加密 age = age + 2; log.info("age is {}", age); stream.defaultWriteObject(); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); log.info("age is {}", age); //给age解密 age = age - 2; }
上面的例子中,咱们为CustUser添加了一个age对象,并在writeObject中对age进行了加密(加2),在readObject中对age进行了解密(减2)。
注意,writeObject和readObject都是private void的方法。他们的调用是经过反射来实现的。
上面的例子, 咱们只是对age字段进行了加密,若是咱们想对整个对象进行加密有没有什么好的处理办法呢?
JDK为咱们提供了javax.crypto.SealedObject 和java.security.SignedObject来做为对序列化对象的封装。从而将整个序列化对象进行了加密。
仍是举个例子:
public void testCusUserSealed() throws IOException, ClassNotFoundException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException { CustUser custUserA=new CustUser("jack","www.flydean.com"); Cipher enCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); Cipher deCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKey secretKey = new SecretKeySpec("saltkey111111111".getBytes(), "AES"); IvParameterSpec iv = new IvParameterSpec("vectorKey1111111".getBytes()); enCipher.init(Cipher.ENCRYPT_MODE, secretKey, iv); deCipher.init(Cipher.DECRYPT_MODE,secretKey,iv); SealedObject sealedObject= new SealedObject(custUserA, enCipher); try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(sealedObject); } try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); SealedObject custUser1 = (SealedObject) objectInputStream.readObject(); CustUser custUserV2= (CustUser) custUser1.getObject(deCipher); log.info("{}",custUserV2); } }
上面的例子中,咱们构建了一个SealedObject对象和相应的加密解密算法。
SealedObject就像是一个代理,咱们写入和读取的都是这个代理的加密对象。从而保证了在数据传输过程当中的安全性。
上面的SealedObject实际上就是一种代理,考虑这样一种状况,若是class中的字段比较多,而这些字段均可以从其中的某一个字段中自动生成,那么咱们其实并不须要序列化全部的字段,咱们只把那一个字段序列化就能够了,其余的字段能够从该字段衍生获得。
在这个案例中,咱们就须要用到序列化对象的代理功能。
首先,序列化对象须要实现writeReplace方法,表示替换成真正想要写入的对象:
public class CustUserV3 implements java.io.Serializable{ private String name; private String address; private Object writeReplace() throws java.io.ObjectStreamException { log.info("writeReplace {}",this); return new CustUserV3Proxy(this); } }
而后在Proxy对象中,须要实现readResolve方法,用于从系列化过的数据中重构序列化对象。以下所示:
public class CustUserV3Proxy implements java.io.Serializable{ private String data; public CustUserV3Proxy(CustUserV3 custUserV3){ data =custUserV3.getName()+ "," + custUserV3.getAddress(); } private Object readResolve() throws java.io.ObjectStreamException { String[] pieces = data.split(","); CustUserV3 result = new CustUserV3(pieces[0], pieces[1]); log.info("readResolve {}",result); return result; } }
咱们看下怎么使用:
public void testCusUserV3() throws IOException, ClassNotFoundException { CustUserV3 custUserA=new CustUserV3("jack","www.flydean.com"); try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(custUserA); } try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); CustUserV3 custUser1 = (CustUserV3) objectInputStream.readObject(); log.info("{}",custUser1); } }
注意,咱们写入和读出的都是CustUserV3对象。
最后咱们讲下Externalizable和Serializable的区别。Externalizable继承自Serializable,它须要实现两个方法:
void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
何时须要用到writeExternal和readExternal呢?
使用Serializable,Java会自动为类的对象和字段进行对象序列化,可能会占用更多空间。而Externalizable则彻底须要咱们本身来控制如何写/读,比较麻烦,可是若是考虑性能的话,则可使用Externalizable。
另外Serializable进行反序列化不须要执行构造函数。而Externalizable须要执行构造函数构造出对象,而后调用readExternal方法来填充对象。因此Externalizable的对象须要一个无参的构造函数。
本文详细分析了序列化对象在多种状况下的使用,并讲解了Serializable和Externalizable的区别,但愿你们可以喜欢。
本文做者:flydean程序那些事本文连接:http://www.flydean.com/java-serialization/
本文来源:flydean的博客
欢迎关注个人公众号:程序那些事,更多精彩等着您!