為什么遠程傳輸對象要序列化?
01、背景介紹
序列化和反序列化幾乎是工程師們每天都需要面對的事情,尤其是當前流行的微服務開發。
光看定義上,對于初學者來說,可能很難一下子理解序列化的意義,尤其是面對這種特別學術詞語的時候,內心會不由自主的發問:它到底是啥,用來干嘛的?
如果用通俗的方式來理解,你可以用變魔術的方式來理解它,就好比你想把一件鐵器從一個地方運往到另一個地方,在出發的時候,通過魔術方式將這個東西融化成一桶鐵水,當到達目的地之后,又通過變魔術的方式,將這桶鐵水還原成一件鐵器。當鐵器變成鐵水的過程,可以理解為序列化;從鐵水變成鐵器,可以理解為反序列化。
站在程序世界的角度看,我們都知道計算機之間傳遞信息的最小單元是字節流,序列化其實就是將一個對象變成所有的計算機都能識別的字節流;反序列化就是將接受到的字節流還原成一個程序能識別的對象。
簡單的說,序列化最終的目的是為了對象可以更方面的進行跨平臺存儲和進行網絡傳輸。
基本上只要是涉及到跨平臺存儲或者進行網絡傳輸的數據,都需要進行序列化。
互聯網早期的序列化方式主要有COM和CORBA。
COM主要用于Windows平臺,并沒有真正實現跨平臺,另外COM的序列化的原理利用了編譯器中虛表,使得其學習成本巨大(想一下這個場景, 工程師需要是簡單的序列化協議,但卻要先掌握語言編譯器)。由于序列化的數據與編譯器緊耦合,擴展屬性非常麻煩。
CORBA是早期比較好的實現了跨平臺,跨語言的序列化協議。COBRA的主要問題是參與方過多帶來的版本過多,版本之間兼容性較差,以及使用復雜晦澀。這些政治經濟,技術實現以及早期設計不成熟的問題,最終導致COBRA的漸漸消亡。J2SE 1.3之后的版本提供了基于CORBA協議的RMI-IIOP技術,這使得Java開發者可以采用純粹的Java語言進行CORBA的開發。
隨著軟件技術的快速發展,之后逐漸出現了比較流行的序列化方式,例如:XML、JSON、Protobuf、Thrift 和 Avro等等。
這些序列化方式各有千秋,不能簡單的說哪一種序列化方式是最好的,只能從你的當時環境下去選擇最適合你的序列化方式,如果你要為你的公司項目進行序列化技術的選型,主要可以從以下幾個方面進行考慮:
- 是否支持跨平臺:尤其是多種語言混合開發的項目,是否支持跨平臺直接決定了系統開發難度
- 序列化的速度:速度快的方式會為你的系統性能提升不少
- 序列化出來的大小:數據越小越好,小的數據傳輸快,也不占帶寬,也能整體提升系統的性能
BB了這么多,作為一名 java 程序員,我們應該如何使用序列化呢,以及序列化的過程中應該需要注意的問題。
下面,我們一起來了解一下!
02、代碼實踐
java 實現序列化方式非常簡單,只需要實現Serializable接口即可,例如下面這個類。
public class Student implements Serializable {
/**
* 用戶名
*/
private String name;
/**
* 年齡
*/
private Integer age;
public Student(String name, Integer age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student1{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
我們來測試一下,將Student對象進行二進制的數據存儲后,并從文件中讀取數據出來轉成Student對象,這個過程其實就是一個序列化和反序列化的過程。
public class ObjectMainTest {
public static void main(String[] args) throws Exception {
//序列化
serializeAnimal();
//反序列化
deserializeAnimal();
}
private static void serializeAnimal() throws Exception {
Student black = new Student("張三", 20);
System.out.println(black.toString());
System.out.println("=================開始序列化================");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.log"));
oos.writeObject(black);
oos.flush();
oos.close();
}
private static void deserializeAnimal() throws Exception {
System.out.println("=================開始反序列化================");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.log"));
Student black = (Student) ois.readObject();
ois.close();
System.out.println(black.toString());
}
}
輸出結果:
Student{name='張三', age=20}
=================開始序列化================
=================開始反序列化================
Student{name='張三', age=20}
看起來是不是超級簡單,但是請你別大意,這里面的坑還真不少,請看下面的問題匯總!
03、序列化問題匯總
3.1、static 屬性不能被序列化
實際在序列化的時候,被static修飾的屬性字段是不能被序列化進去的,因為靜態變量屬于類的狀態,序列化并不保存靜態變量!
3.2、Transient 屬性不會被序列化
被Transient修飾的屬性無法被序列化,眼見為實,我們給Student類的name字段加一個transient修飾符。
public class Student implements Serializable {
/**
* 用戶名
*/
private transient String name;
//...省略
}
運行測試方法,輸出結果如下:
Student{name='張三', age=20}
=================開始序列化================
=================開始反序列化================
Student{name='null', age=20}
很明顯,被transient修飾的name屬性,反序列化后的結果為null。
3.3、序列化版本號 serialVersionUID 問題
只要是實現了Serializable接口的類都會有一個版本號,如果我們沒有定義,JDK 工具會按照我們對象的屬性生成一個對應的版本號,當然我們還可以自定義,例如給Student類自定義一個序列化版本號,操作如下。
public class Student implements Serializable {
//自定義序列化版本號
private static final long serialVersionUID = 1l;
//...省略
}
如何驗證這一點呢?
首先,我們先序列化一個Student對象,里面沒有自定義版本號,然后在反序列化的時候,我們給這個對象自定義一個版本號,運行測試程序,看能不能反序列化成功?
Exception in thread "main" java.io.InvalidClassException: com.example.java.serializable.test1.entity.Student; local class incompatible: stream classdesc serialVersionUID = 821478144412499207, local class serialVersionUID = 1
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
答案很明顯,反序列化失敗!
分析原因:Student對象序列化時的版本號是821478144412499207,反序列化時的版本號是1,兩者不一致,導致無法反序列化成功!
當我們沒有顯式的自定義序列化版本號時,JDK 會根據當前對象的屬性自動生成一個對象的版本號,只要對象的屬性不會發生變化,這個版本號也基本上不會發生變化,但是當對象的屬性發生了變化,對應的反序列化對象沒有跟著一起變化,大概率會出現反序列化失敗!
為了眼見為實,我們繼續以實際案例給大家演示一下。
還是以上面那個為主,我們先序列化一個Student對象,里面沒有自定義版本號,然后在反序列化操作的時候,我們給Student對象新增一個屬性email,同時也不自定義版本號。
public class Student implements Serializable {
/**
* 用戶名
*/
private String name;
/**
* 年齡
*/
private Integer age;
/**
* 郵箱
*/
private String email;
//省略set、get...
}
看看運行效果:
Exception in thread "main" java.io.InvalidClassException: com.example.java.serializable.test1.entity.Student; local class incompatible: stream classdesc serialVersionUID = 821478144412499207, local class serialVersionUID = -5996907635197467174
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
答案很顯然,反序列化報錯了!兩者的版本號不一致!
在平時開發的過程中,實體類的屬性難免會發生改動,我們有些同學啊,在寫代碼的時候只是把序列化的接口實現了,但是沒有自定義版本號,在這點上,我強烈建議大家一定要給每個實現了Serializable接口的類,自定義一個版本號,即使對象的屬性發生了變化,也不會影響到數據的序列化和反序列化操作!
操作很簡單,直接在實體類里面加上這個靜態變量即可!
//自定義序列化版本號
private static final long serialVersionUID = 1l;
3.4、父類、子類序列化問題
在實際的開發過程中,尤其是實體類,為了對象屬性的復用,我們往往會采用繼承的方式來處理。
使用了繼承之后,父類屬性是否可以正常被序列化呢?下面我們一起來看看!
- 父類沒有實現序列化,子類實現序列化
首先我們創建兩個類Parent和Child,Child繼承自Parent。
public class Parent {
private String name;
public String getName() {
return name;
}
public Parent setName(String name) {
this.name = name;
return this;
}
}
public class Child extends Parent implements Serializable{
private static final long serialVersionUID = 1l;
private String id;
public String getId() {
return id;
}
public Child setId(String id) {
this.id = id;
return this;
}
}
編寫測試類,先序列化,然后再反序列化!
public class ObjectMainTest {
public static void main(String[] args) throws Exception {
serializeAnimal();
deserializeAnimal();
}
private static void serializeAnimal() throws Exception {
Child black = new Child();
black.setId("123");
black.setName("張三");
System.out.println("id:" + black.getId() + ",name:" + black.getName());
System.out.println("=================開始序列化================");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.log"));
oos.writeObject(black);
oos.flush();
oos.close();
}
private static void deserializeAnimal() throws Exception {
System.out.println("=================開始反序列化================");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.log"));
Child black = (Child) ois.readObject();
ois.close();
System.out.println("id:" + black.getId() + ",name:" + black.getName());
}
}
運行結果如下:
id:123,name:張三
=================開始序列化================
=================開始反序列化================
id:123,name:null
結果很明顯,父類的屬性沒有被序列化進去!
我們在來試試,另一種常見
- 父類實現序列化,子類不實現序列化
public class Parent implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public String getName() {
return name;
}
public Parent setName(String name) {
this.name = name;
return this;
}
}
public class Child extends Parent {
private String id;
public String getId() {
return id;
}
public Child setId(String id) {
this.id = id;
return this;
}
}
接著運行一次程序,結果如下!
id:123,name:張三
=================開始序列化================
=================開始反序列化================
id:123,name:張三
結果很明顯,父類的屬性被序列化進去!
假如,子類和父類,都實現了序列化,并且序列化版本號都不一樣,會不會出現問題呢?
- 父類實現序列化,子類實現序列化
public class Parent implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public String getName() {
return name;
}
public Parent setName(String name) {
this.name = name;
return this;
}
}
public class Child extends Parent implements Serializable{
private static final long serialVersionUID = 2l;
private String id;
public String getId() {
return id;
}
public Child setId(String id) {
this.id = id;
return this;
}
}
運行一次程序,結果如下!
id:123,name:張三
=================開始序列化================
=================開始反序列化================
id:123,name:張三
父類的屬性序列化依然成功,當父、子類都實現了序列化,并且定義了不同的版本號,這種情況下,版本號是跟著子類的版本號走的!
總結起來,當父類實現序列化時,子類所有的屬性也會全部被序列化;但是當父類沒有實現序列化,子類在序列化時,父類屬性并不會被序列化!
3.5、自定義序列化過程
Serializable接口內部序列化是 JVM 自動實現的,但是在某些少數的場景下,你可能想自定義序列化和反序列化的內容,但是又不想改實體類屬性,這個時候你可以采用自定義序列化的實現方式。
自定義序列化方式,其實也很簡單,只需要實現 JDK 自身提供的Externalizable接口就行,里面有兩個核心方法,一個是數據寫入,另一個是數據的讀取。
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
Externalizable接口的實現過程也很簡單,我們創建一個Person,實現自Externalizable的兩個方法。
public class Person implements Externalizable {
private static final long serialVersionUID = 1l;
private String name;
private int age;
/**
* 實現了Externalizable這個接口時需要提供無參構造,在反序列化時會檢測
*/
public Person() {
System.out.println("Person: empty");
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("person writeExternal...");
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException {
System.out.println("person readExternal...");
name = (String) in.readObject();
age = in.readInt();
}
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
測試Person對象的序列化和反序列化。
public class ExternalizableMain {
public static void main(String[] args) throws IOException, ClassNotFoundException {
serializable();
deserializable();
}
private static void serializable() throws IOException {
Person person = new Person("張三", 15);
System.out.println(person.toString());
System.out.println("=================開始序列化================");
FileOutputStream boas = new FileOutputStream("person.log");
ObjectOutputStream oos = new ObjectOutputStream(boas);
oos.writeObject(person);
oos.close();
boas.close();
}
private static void deserializable() throws IOException, ClassNotFoundException {
System.out.println("============反序列化=============");
ObjectInputStream bis = new ObjectInputStream(new FileInputStream("person.log"));
Person person = (Person)bis.readObject();
System.out.println(person.toString());
}
}
運行結果如下:
Person{name='張三', age=15}
=================開始序列化================
person writeExternal...
============反序列化=============
Person: empty
person readExternal...
Person{name='張三', age=15}
04、小結
對象的序列化,在實際的開發過程中,使用的非常頻繁,尤其是微服務開發,如果你用的是SpringBoot + Dubbo組合的框架,那么在通過rpc調用的時候,如果傳輸的對象沒有實現序列化,會直接報錯!
在使用序列化的時候,坑點還不少,尤其是版本號的問題,這個很容易被忽略,大家在實際開發的時候,強烈推薦自定義版本號,這樣可以避免傳輸的對象屬性發生變化的時候,接口反序列化出錯的概率!
05、參考
1、https://www.huaweicloud.com/articles/6b6d1d97c0a9155899f0f7354c86610d.html
2、https://zhuanlan.zhihu.com/p/40462507
3、https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html