简介
序列化是java中一个十分罕用又会被人漠视的性能,咱们将对象写入文件须要序列化,同时,对象如果想要在网络上传输也须要进行序列化。
序列化的目标就是保障对象能够正确的传输,那么咱们在序列化的过程中须要留神些什么问题呢?
一起来看看吧。
序列化简介
如果一个对象要想实现序列化,只须要实现Serializable接口即可。
奇怪的是Serializable是一个不须要任何实现的接口。如果咱们implements Serializable然而不重写任何办法,那么将会应用JDK自带的序列化格局。
然而如果class发送变动,比方减少了字段,那么默认的序列化格局就满足不了咱们的需要了,这时候咱们须要思考应用本人的序列化形式。
如果类中的字段不想被序列化,那么能够应用transient关键字。
同样的,static示意的是类变量,也不须要被序列化。
留神serialVersionUID
serialVersionUID 示意的是对象的序列ID,如果咱们不指定的话,是JVM主动生成的。在反序列化的过程中,JVM会首先判断serialVersionUID 是否统一,如果不统一,那么JVM会认为这不是同一个对象。
如果咱们的实例在前期须要被批改的话,留神肯定不要应用默认的serialVersionUID,否则前期class发送变动之后,serialVersionUID也会同样的发生变化,最终导致和之前的序列化版本不兼容。
writeObject和readObject
如果要本人实现序列化,那么能够重写writeObject和readObject两个办法。
留神,这两个办法是private的,并且是non-static的:
private void writeObject(final ObjectOutputStream stream) throws IOException { stream.defaultWriteObject();} private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject();}
如果不是private和non-static的,那么JVM就不可能发现这两个办法,就不会应用他们来做自定义序列化。
readResolve和writeReplace
如果class中的字段比拟多,而这些字段都能够从其中的某一个字段中主动生成,那么咱们其实并不需要序列化所有的字段,咱们只把那一个字段序列化就能够了,其余的字段能够从该字段衍生失去。
readResolve和writeReplace就是序列化对象的代理性能。
首先,序列化对象须要实现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对象。
不要序列化外部类
所谓外部类就是未显式或隐式申明为动态的嵌套类,为什么咱们不要序列化外部类呢?
- 序列化在非动态上下文中申明的外部类,该外部类蕴含对关闭类实例的隐式非瞬态援用,从而导致对其关联的外部类实例的序列化。
- Java编译器对内部类的实现在不同的编译器之间可能有所不同。从而导致不同版本的兼容性问题。
- 因为Externalizable的对象须要一个无参的构造函数。然而外部类的构造函数是和外部类的实例相关联的,所以它们无奈实现Externalizable。
所以上面的做法是正确的:
public class OuterSer implements Serializable { private int rank; class InnerSer { protected String name; }}
如果你真的想序列化外部类,那么把外部类置为static吧。
如果类中有自定义变量,那么不要应用默认的序列化
如果是Serializable的序列化,在反序列化的时候是不会执行构造函数的。所以,如果咱们在构造函数或者其余的办法中对类中的变量有肯定的束缚范畴的话,反序列化的过程中也必须要加上这些束缚,否则就会导致歹意的字段范畴。
咱们举几个例子:
public class SingletonObject implements Serializable { private static final SingletonObject INSTANCE = new SingletonObject (); public static SingletonObject getInstance() { return INSTANCE; } private SingletonObject() { } public static Object deepCopy(Object obj) { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); new ObjectOutputStream(bos).writeObject(obj); ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray()); return new ObjectInputStream(bin).readObject(); } catch (Exception e) { throw new IllegalArgumentException(e); } } public static void main(String[] args) { SingletonObject singletonObject= (SingletonObject) deepCopy(SingletonObject.getInstance()); System.out.println(singletonObject == SingletonObject.getInstance()); }}
下面是一个singleton对象的例子,咱们在其中定义了一个deepCopy的办法,通过序列化来对对象进行拷贝,然而拷贝进去的是一个新的对象,只管咱们定义的是singleton对象,最初运行的后果还是false,这就意味着咱们的系统生成了一个不一样的对象。
怎么解决这个问题呢?
加上一个readResolve办法就能够了:
protected final Object readResolve() throws NotSerializableException { return INSTANCE; }
在这个readResolve办法中,咱们返回了INSTANCE,以确保其是同一个对象。
还有一种状况是类中字段是有范畴的。
public class FieldRangeObject implements Serializable { private int age; public FieldRangeObject(int age){ if(age < 0 || age > 100){ throw new IllegalArgumentException("age范畴不对"); } this.age=age; }}
下面的类在反序列化中会有什么问题呢?
因为下面的类在反序列化的过程中,并没有对age字段进行校验,所以,恶意代码可能会生成超出范围的age数据,当反序列化之后就溢出了。
怎么解决呢?
很简略,咱们在readObject办法中进行范畴的判断即可:
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { ObjectInputStream.GetField fields = s.readFields(); int age = fields.get("age", 0); if (age > 100 || age < 0) { throw new InvalidObjectException("age范畴不对!"); } this.age = age; }
不要在readObject中调用可重写的办法
为什么呢?readObject实际上是反序列化的构造函数,在readObject办法没有完结之前,对象是没有构建实现,或者说是局部构建实现。如果readObject调用了可重写的办法,那么恶意代码就能够在办法的重写中获取到还未齐全实例化的对象,可能造成问题。
本文的代码:
learn-java-base-9-to-20/tree/master/security
本文已收录于 http://www.flydean.com/java-security-code-line-serialization/最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!
欢送关注我的公众号:「程序那些事」,懂技术,更懂你!