讀古今文學網 > 編寫高質量代碼:改善Java程序的151個建議 > 建議14:使用序列化類的私有方法巧妙解決部分屬性持久化問題 >

建議14:使用序列化類的私有方法巧妙解決部分屬性持久化問題

部分屬性持久化問題看似很簡單,只要把不需要持久化的屬性加上瞬態關鍵字(transient關鍵字)即可。這是一種解決方案,但有時候行不通。例如一個計稅系統和人力資源系統(HR系統)通過RMI(Remote Method Invocation,遠程方法調用)對接,計稅系統需要從HR系統獲得人員的姓名和基本工資,以作為納稅的依據,而HR系統的工資分為兩部分:基本工資和績效工資,基本工資沒什麼秘密,根據工作崗位和年限自己都可以計算出來,但績效工資卻是保密的,不能洩露到外系統,很明顯這是兩個相互關聯的類。先來看薪水類Salary類的代碼:


public class Salary implements Serializable{

private static final long serialVersionUID=44663L;

//基本工資

private int basePay;

//績效工資

private int bonus;

public Salary(int_basePay, int_bonus){

basePay=_basePay;

bonus=_bonus;

}

/*getter/setter方法省略*/

}


Peron類與Salary類是關聯關係,代碼如下:


public class Person implements Serializable{

private static final long serialVersionUID=60407L;

//姓名

private String name;

//薪水

private Salary salary;

public Person(String_name, Salary_salary){

name=_name;

salary=_salary;

}

/*getter/setter方法省略*/

}


這是兩個簡單的JavaBean,都實現了Serializable接口,都具備了持久化條件。首先計稅系統請求HR系統對某一個Person對像進行序列化,把人員和工資信息傳遞到計稅系統中,代碼如下:


public class Serialize{

public static void main(Stringargs){

//基本工資1000元,績效工資2500元

Salary salary=new Salary(1000,2500);

//記錄人員信息

Person person=new Person(\"張三\",salary);

//HR系統持久化,並傳遞到計稅系統

SerializationUtils.writeObject(person);

}

}


在通過網絡傳送到計稅系統後,進行反序列化,代碼如下:


public class Deserialize{

public static void main(Stringargs){

//技術系統反序列化,並打印信息

Person p=(Person)SerializationUtils.readObject();

StringBuffer sb=new StringBuffer();

sb.append(\"姓名:\"+p.getName());

sb.append(\"t基本工資:\"+p.getSalary().getBasePay());

sb.append(\"t績效工資:\"+p.getSalary().getBonus());

System.out.println(sb);

}

}


打印出的結果很簡單:

姓名:張三 基本工資:1000 績效工資:2500。

但是這不符合需求,因為計稅系統只能從HR系統中獲得人員姓名和基本工資,而績效工資是不能獲得的,這是個保密數據,不允許發生洩露。怎麼解決這個問題呢?你可能馬上會想到四種方案:

(1)在bonus前加上transient關鍵字

這是一個方法,但不是一個好方法,加上transient關鍵字就標誌著Salary類失去了分佈式部署的功能,它可是HR系統最核心的類了,一旦遭遇性能瓶頸,想再實現分佈式部署就不可能了,此方案否定。

(2)新增業務對像

增加一個Person4Tax類,完全為計稅系統服務,就是說它只有兩個屬性:姓名和基本工資。符合開閉原則,而且對原系統也沒有侵入性,只是增加了工作量而已。這是個方法,但不是最優方法。

(3)請求端過濾

在計稅系統獲得Person對像後,過濾掉Salary的bonus屬性,方案可行但不合規矩,因為HR系統中的Salary類安全性竟然讓外系統(計稅系統)來承擔,設計嚴重失職。

(4)變更傳輸契約

例如改用XML傳輸,或者重建一個Web Service服務。可以做,但成本太高。

可能有讀者會說了,你都在說別人的方案不好,你提供個優秀的方案看看!好的,這就展示一個優秀的方案。其中,實現了Serializable接口的類可以實現兩個私有方法:writeObject和readObject,以影響和控制序列化和反序列化的過程。我們把Person類稍做修改,看看如何控制序列化和反序列化,代碼如下:


public class Person implements Serializable{

private static final long serialVersionUID=60407L;

//姓名

private String name;

//薪水

private transient Salary salary;

public Person(String_name, Salary_salary){

name=_name;

salary=_salary;

}

//序列化委託方法

private void writeObject(java.io.ObjectOutputStream out)throws IOException{

out.defaultWriteObject();

out.writeInt(salary.getBasePay());

}

//反序列化時委託方法

private void readObject(java.io.ObjectInputStream in)throws IOException, Class-

NotFoundException{

in.defaultReadObject();

salary=new Salary(in.readInt(),0);

}

}


其他代碼不做任何改動,我們先運行看看,結果為:

姓名:張三 基本工資:1000 績效工資:0。

我們在Person類中增加了writeObject和readObject兩個方法,並且訪問權限都是私有級別,為什麼這會改變程序的運行結果呢?其實這裡使用了序列化獨有的機制:序列化回調。Java調用ObjectOutputStream類把一個對像轉換成流數據時,會通過反射(Reflection)檢查被序列化的類是否有writeObject方法,並且檢查其是否符合私有、無返回值的特性。若有,則會委託該方法進行對像序列化,若沒有,則由ObjectOutputStream按照默認規則繼續序列化。同樣,在從流數據恢復成實例對像時,也會檢查是否有一個私有的readObject方法,如果有,則會通過該方法讀取屬性值。此處有幾個關鍵點要說明:

(1)out. defaultWriteObject()

告知JVM按照默認的規則寫入對象,慣例的寫法是寫在第一句話裡。

(2)in. defaultReadObject()

告知JVM按照默認規則讀入對象,慣例的寫法也是寫在第一句話裡。

(3)out. writeXX和in.readXX

分別是寫入和讀出相應的值,類似一個隊列,先進先出,如果此處有複雜的數據邏輯,建議按封裝Collection對像處理。

可能有讀者會提出,這似乎不是一種優雅的處理方案呀,為什麼JDK沒有對此提供一個更好的解決辦法呢?比如訪問者模式,或者設置鉤子函數(Hook),完全可以更優雅地解決此類問題。我查閱了大量的文檔,得出的結論是:無解,只能說這是一個可行的解決方案而已。

再回到我們的業務領域,通過上述方法重構後,其代碼的修改量減少了許多,也優雅了許多。可能你又要反問了:如此一來,Person類也失去了分佈式部署的能力啊。確實是,但是HR系統的難點和重點是薪水計算,特別是績效工資,它所依賴的參數很複雜(僅從數量上說就有上百甚至上千種),計算公式也不簡單(一般是引入腳本語言,個性化公式定制),而相對來說Person類基本上都是「靜態」屬性,計算的可能性不大,所以即使為性能考慮,Person類為分佈式部署的意義也不大。