Java中的RPC(遠程服務調用)可以通過Serializable的方式進行具帮。
序(序列化和反序列化)
是什么?為啥用?怎么用蜂厅?——靈魂三連
-
序列化和反序列化是什么匪凡?
序列化:把
對象
轉變?yōu)?code>字節(jié)序列的過程稱為對象的序列化。反序列化:把
字節(jié)序列
恢復為對象
的過程稱為對象的反序列化掘猿。
-
- 將內存中對象的字節(jié)持久化到硬盤中的時候病游;
- 當使用Socket在網絡上傳輸對象的時候;
- 當使用RMI(遠程方法調用)傳輸對象的時候稠通;
1. Serializable序列化
類的序列化是實現(xiàn)java.io.Serializable
接口啟動的衬衬,不實現(xiàn)此接口的類將不會有任何狀態(tài)的序列化和反序列化。序列化接口沒有方法或字段改橘,僅用于標識序列化的語義滋尉。
1.1 Serializable序列化的注意事項
1.1.1 序列化ID問題
Intellij IDEA生成serialVersionUID
虛擬機是否允許反序列化,不僅取決于類路徑和功能代碼是否一致飞主,還取決于兩個類序列化ID是否一致
(ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;)
兼砖。
如果可序列化類沒有顯示聲明SerialVersionUID
,則序列化運行時將根據(jù)Java
對象序列化規(guī)范中所述的類的各方面計算該類的默認SerialVersionUID
既棺。但是強烈建議所有可序列化的類都明確聲明serialVersionUID值。因為默認得UID計算對類詳細信息非常敏感懒叛,這可能因編譯器實現(xiàn)而異丸冕,可能會導致反序列化InvalidClassException
。
序列化和反序列化代碼詳見JAVA BIO體系——ObjectInputStream/ObjectOutputStream對象流的使用
1. 反序列化不同的類路徑導致ClassCastException
異常
Exception in thread "main" java.lang.ClassCastException:
com.JsonSerializer.User cannot be cast to com.IODemo.BIODemo.User
at com.IODemo.BIODemo.ObjectOut.main(ObjectOut.java:12)
2. 反序列化不同的UID導致InvalidClassException
異常
java.io.InvalidClassException: com.JsonSerializer.User; local class incompatible:
stream classdesc serialVersionUID = 4731277808546534921,
local class serialVersionUID = 4731277808546534920
序列化ID一般有兩種生成規(guī)則薛窥,一種是固定的1L胖烛,一種是隨機生成一個不重復long類型數(shù)據(jù)。
如果是沒有特殊需求诅迷,就用默認的1L就可以佩番,這樣就可以確保代碼一致時反序列化成功;
隨機生成的序列化ID有什么用呢罢杉?有些時候趟畏,通過改變序列ID可以用來限制某些用戶的使用;
1.1.2 特殊變量序列化
1. 靜態(tài)變量的序列化
序列化并不保存靜態(tài)變量滩租,序列化保存的是對象的狀態(tài)赋秀,而靜態(tài)變量是類的狀態(tài)。
2. Transient關鍵字
transient
([?tr?nzi?nt]
臨時態(tài))關鍵字的作用就是控制變量的序列化律想,在變量聲明前加上該關鍵字猎莲,可以阻止該變量序列化到文件中,在反序列化后技即,transient
變量會被設為初始值著洼,如int型的為0,對象型的為null。
3. 父類的序列化特性
如果子類實現(xiàn)了Serializable
接口而父類沒有實現(xiàn)身笤,那么父類不會被序列化豹悬,但是父類必須有默認的無參構造方法,否則會拋出InvalidClassException
異常展鸡。如下圖所示
解決方案:想要將父類對象也序列化屿衅,就需要讓父類也實現(xiàn)Serializable
接口;如果父類不實現(xiàn)的話莹弊,就需要有默認的無參構造函數(shù)涤久,并且父類的變量值都是默認聲明的值。
在父類沒有實現(xiàn)Serializable
接口時忍弛,虛擬機不會序列化父對象响迂,而一個Java對象的初始化必須先初始化父對象,再初始化子對象细疚,反序列化也不例外蔗彤。所以在反序列化時,為了構造父對象疯兼,只能調用父類對象的無參構造函數(shù)作為默認的父對象然遏。因此當我們取父對象的變量值時,它的值是調用父類無參構造函數(shù)后的值吧彪。
使用
Transient
關鍵字可以使得字段不被序列化待侵,還有別的方法嗎?
根據(jù)父類對象序列化的規(guī)則姨裸,可以將不需要被序列化的字段抽取出來放到父類中秧倾,子類實現(xiàn)Serializable
接口,父類不實現(xiàn)傀缩,根據(jù)父類序列化規(guī)則那先,父類的字段數(shù)據(jù)將不會被序列化。
4. 定制序列化方法
在序列化過程中赡艰,虛擬機會試圖調用對象類中的
writeObject
和readObject
方法售淡,進行用戶自定義的序列化和反序列化,如果沒有這樣的方法慷垮,則默認調用defaultWriteObject
方法以及defaultReadObject
方法勋又。用戶自定義的writeObject
和readObject
方法運允許用戶控制序列化過程。比如可以在序列化過程中動態(tài)的改變序列化的數(shù)值换帜⌒ㄈ溃基于這個原理,可以在實際應用中得到使用惯驼,用于敏感字段的加密工作蹲嚣。
ObjectOutputStream使用getPrivateMethod
方法writeObject
可以自定義用戶的序列化過程递瑰,如果聲明了private void writeObject()
,它將會被ObjectOutputStream
調用隙畜。盡管它們被外部類調用但是他們實際上是private
的方法抖部。
writeObject
和readObject
既不存在于java.lang.Object
中,也沒有在Serializable
中聲明议惰,那么ObjectOutputStream
如何調用他們的慎颗?
ObjectOutputStream
使用了反射尋找是否聲明了這兩個方法。并且ObjectOutputStream
使用getPrivateMethod
言询,所以這些方法必須聲明為private
以至于可以被ObjectOutputStream
調用俯萎。
在兩個方法的開始處,你會發(fā)現(xiàn)調用了defaultWriteObject()
和defaultReadObject()
运杭。它們的作用就是默認序列化進程夫啊,就像寫/讀所有的no-transient
和non-static
字段。通常來說辆憔,所有我們需要處理的字段都應該聲明為transient
撇眯,這樣的話,defaultWriteObject/defaultReadObject
便可以專注于其余字段虱咧,而我們則為特定的字段定制序列化熊榛。但是使用默認序列化方法并不是強制的。
需要注意的是:序列化和反序列化的writeXXX()
和readXXX()
的順序需要對應腕巡。比如有多個字段都用writeInt()
——序列化来候,那么readInt()
需要按照順序將其賦值。
4.1. 使用transient和defaultWriteObject()定制序列化
public class EncryptUser implements Serializable {
private static final long serialVersionUID = 1L;
private String userName;
transient private String password; //不進行序列化逸雹,需要自己手動處理的
transient private String sex;
//為節(jié)省篇幅 省略get/set/toString()方法
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
String password = this.password + ":加密";
oos.writeUTF(password); //將密碼手動處理加密后序列化
System.out.println("EntryUser序列化成功:" + toString());
}
private void readObject(ObjectInputStream ios) throws IOException, ClassNotFoundException {
ios.defaultReadObject();
password = ios.readUTF() + "解密";
System.out.println("EntryUser反序列化成功:" + toString());
}
}
4.2. ObjectOutputStream.PutField定制序列化
public class EncryptUser implements Serializable {
private static final long serialVersionUID = 1L;
private String userName;
private String password; //不進行序列化,需要自己手動處理的
//為節(jié)省篇幅 省略get/set/toString()方法
private void writeObject(ObjectOutputStream oos) throws IOException {
ObjectOutputStream.PutField putField = oos.putFields();//檢索寫入流的字段
password = "加密:" + password; //模擬加密
//設置寫入流的字段
putField.put("password", password);
//將字段寫入流
oos.writeFields();
}
private void readObject(ObjectInputStream ios) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField getField = ios.readFields();
Object encryptPassword = getField.get("password", "");
System.out.println("加密的字符串:" + encryptPassword);
password = encryptPassword + "解密";
}
}
4.3. 測試方法
private static void writeObject() {
try {
// 檢索用于緩沖要寫入流的持久性字段的對象云挟。 當調用writeFields方法時梆砸,字段將被寫入流。
EncryptUser encryptUser = new EncryptUser();
encryptUser.setUserName("tom");
encryptUser.setPassword("tom245");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./EncryptUser.txt"));
objectOutputStream.writeObject(encryptUser);
objectOutputStream.flush();
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("./EncryptUser.txt"));
EncryptUser readObject = (EncryptUser)objectInputStream.readObject();
System.out.println(readObject);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
注:因為JDK1.7之后ObjectOutputStream
實現(xiàn)了AutoCloseable
接口园欣,會在try方法結束之后帖世,自動關閉資源。
5. 對象屬性序列化
如果一個類有引用類型的實例變量沸枯,那么這個引用也要實現(xiàn)Serializable
接口日矫,否則會出現(xiàn):
可以使用transient
關鍵字阻止該變量的序列化。
1.1.3 序列化的存儲
Java序列化機制為了節(jié)省磁盤空間绑榴,具有特定的存儲規(guī)則:當寫入文件為同一個對象時哪轿,并不會將對象的內容進行存儲,而是再次存儲一份引用翔怎。反序列化時窃诉,恢復引用關系杨耙。
序列化同一對象
public class RuleSerializable {
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.inf"));
User user = new User();
user.setName("tom");
oos.writeObject(user);
oos.flush();
System.out.println("第一次讀取的長度:" + new File("user.inf").length());
//第二次序列化后修改數(shù)據(jù)
user.setName("lili");
oos.writeObject(user);
oos.flush();
System.out.println("第二次讀取的長度:" + new File("user.inf").length());
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("user.inf"));
//反序列化
User user1 = (User) objectInputStream.readObject();
User user2 = (User) objectInputStream.readObject();
System.out.println("兩個對象是否相等:" + (user1 == user2));
System.out.println("反序列化的用戶名:"+user1.getName());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
我們看到最后的結果是輸出
tom
,原因是第一次寫入對象以后飘痛,第二次在試圖寫入的時候硕旗,虛擬機根據(jù)引用關系知道已經有一個對象內容寫入文件阴绢,因此只保存第二次寫的引用。所以在讀取時,獲取的是第一次保存的對象立砸。
2. Protostuff序列化
我們看到Java內置的序列化API
Serializable
,但是效率不是很高的积仗。Google提供了一個效率很高的序列化APIProtobuf
擅编,但是使用過于復雜。開源社區(qū)在Protobuf
的基礎上封裝出Protostuff
萌庆,在不丟失效率的前提上溶褪,使用更加簡單。一般情況下践险,protostuff
序列化后的數(shù)據(jù)大小是Serializable
的1/10之一猿妈,速度更是兩個量級以上。
2.1 protostuff序列化簡單使用
MAVEN依賴
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.6.0</version>
</dependency>
序列化:
public static <T> byte[] serializer(T obj) {
Class<T> clazz = (Class<T>) obj.getClass();
//本質上是一個數(shù)組對象
LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
try {
//獲取模板
Schema<T> schema = RuntimeSchema.getSchema(clazz);
//將Object對象裝換按照schema對象巍虫,轉化成byte[]對象
byte[] bytes = ProtostuffIOUtil.toByteArray(obj, schema, buffer);
return bytes;
} catch (Exception e) {
throw new RuntimeException("序列化失敗...");
} finally {
buffer.clear();
}
}
- 獲取傳入對象的
class
對象彭则; - 獲取一個
byte[]
緩沖數(shù)組LinkBuffer
; - 根據(jù)
class
對象獲取Schema
對象 - 將
Object
對象序列化成byte[]數(shù)組占遥;
反序列化:
public static <T> T deserializer(byte[] data, Class<T> clazz) {
if (data == null || data.length == 0) {
throw new RuntimeException("反序列化失敗俯抖,byte[]不能為空");
}
T obj = null;
try {
obj = clazz.newInstance();
Schema<T> schema = RuntimeSchema.getSchema(clazz);
ProtostuffIOUtil.mergeFrom(data, obj, schema);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return obj;
}
- 傳入
byte[]
數(shù)組和class
對象; - 通過反射初始化
class
對象瓦胎; - 獲取
Schema<T>
對象芬萍; - 將
byte[]
數(shù)組反序列化為Object對象;
注:
RuntimeSchema.getSchema(clazz);
實際上會將Schema<T>
對象緩存搔啊。
2.2 protostuff定制開發(fā)
- 使用
transient
修飾就不用進行序列化柬祠; - 定制序列化時,即用戶判斷什么情況下才進行序列化负芋,可以使用自定義
Schema
進行實現(xiàn)漫蛔。
1. 定義Java Bean類
此處使用了lombok
插件,@Data
標簽即實現(xiàn)get()
和set()
方法旧蛾;@Builder
標簽實現(xiàn)了建造者設計模式莽龟,即靜態(tài)內部類實現(xiàn)建造者角色,客戶端進行導演者角色锨天。
//地址類
@Builder(toBuilder = true)
@Data
public class Address {
private String address;
private String phone;
}
//用戶類
@Data
@Builder
public class Person {
private String name;
private Integer age;
//表明該字段不進行序列化
private transient String password;
private List<Address> addressList;
}
2. 自定義Address的Schema
用戶定制化的開發(fā)毯盈,此處實現(xiàn)簡單,當address為null時病袄,不進行序列化奶镶。
//自定義序列化模板
public class AddressSchema implements Schema<Address> {
@Override
public String getFieldName(int number) {
String ret = "";
switch (number) {
case 1:
ret = "address";
break;
case 2:
ret = "phone";
break;
default:
break;
}
return ret;
}
@Override
public int getFieldNumber(String name) {
if ("address".equals(name)) {
return 1;
} else if ("phone".equals(name)) {
return 2;
}
return 0;
}
//若是地址為null的話迟赃,不允許序列化
@Override
public boolean isInitialized(Address message) {
if (message == null) {
return false;
}
return false;
}
@Override
public Address newMessage() {
return Address.builder().build();
}
@Override
public String messageName() {
return Address.class.getSimpleName();
}
@Override
public String messageFullName() {
return Address.class.getName();
}
@Override
public Class<? super Address> typeClass() {
return Address.class;
}
//反序列化(輸入流中讀取數(shù)據(jù),寫入到message中)
@Override
public void mergeFrom(Input input, Address message) throws IOException {
//在流中讀取數(shù)據(jù)(while循環(huán))
while (true) {
int number = input.readFieldNumber(this);//傳入的是模板文件
switch (number) {
case 0:
return;
case 1:
message.setAddress(input.readString()); //設置address值
break;
case 2:
message.setPhone(input.readString()); //設置phone值
break;
default:
input.handleUnknownField(number, this);
}
}
}
//序列化(將對象設置到序列化的輸出流中)
@Override
public void writeTo(Output output, Address message) throws IOException {
if (message.getAddress() == null) {
throw new UninitializedMessageException(message, this);
}
//屬性序號厂镇、屬性內容纤壁,是否允許重復
output.writeString(1, message.getAddress(), false);
if (null != message.getPhone()) {
output.writeString(2, message.getPhone(), false);
}
}
}
3. 編寫測試代碼
當序列化bjAddress時,因為address字段為null捺信,禁止其序列化酌媒。
public class ProtoTest {
public static void main(String[] args) {
Address shAddress = Address.builder().address("上海").phone("123123").build();
Address bjAddress = Address.builder().phone("XXX").build();
Person person = Person.builder().name("yxr").password("123").age(25).
addressList(Arrays.asList(shAddress, bjAddress)).build();
//序列化
Schema<Person> schema = RuntimeSchema.createFrom(Person.class);
//創(chuàng)建緩沖區(qū)
LinkedBuffer buffer = LinkedBuffer.allocate(1024);
//直接序列化數(shù)組
byte[] bytes = ProtostuffIOUtil.toByteArray(person, schema, buffer);
System.out.println("序列化:" + Arrays.toString(bytes));
//反序列化
Schema<Person> newSchema = RuntimeSchema.getSchema(Person.class);
Person newPerson = newSchema.newMessage(); //創(chuàng)建了一個person對象
ProtostuffIOUtil.mergeFrom(bytes, newPerson, newSchema);
System.out.println("反序列化:" + newPerson);
buffer.clear(); //釋放資源
//創(chuàng)建自定義的Schema對象
Schema<Address> addressSchema = new AddressSchema();
byte[] bjArray = ProtostuffIOUtil.toByteArray(bjAddress, addressSchema, buffer);
System.out.println("Address序列化:" + bjAddress);
buffer.clear();
byte[] shArray = ProtostuffIOUtil.toByteArray(shAddress, addressSchema, buffer);
System.out.println(shAddress);
Address newAddress = addressSchema.newMessage();
ProtostuffIOUtil.mergeFrom(shArray, newAddress, addressSchema);
System.out.println("Address反序列化:" + newAddress);
buffer.clear();
}
}
推薦參考: