文章目錄
- 簡介
- 序列化簡介
- 注意serialVersionUID
- writeObject和readObject
- readResolve和writeReplace
- 不要序列化內部類
- 如果類中有自定義變數,那么不要使用默認的序列化
- 不要在readObject中呼叫可重寫的方法
簡介
序列化是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/
最通俗的解讀,最深刻的干貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/199805.html
標籤:其他
