1. 程式人生 > >使用序列化類的私有方法巧妙解決部分屬性持久化問題

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

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

public class Salary implements Serializable {
    private static final long serialVersionUID = 2706085398747859680L;
    // 基本工資
    private int basePay;
    // 績效工資
    private int bonus;

    public Salary(int _basepay, int _bonus) {
        this.basePay = _basepay;
        this.bonus = _bonus;
    }
//Setter和Getter方法略

}

Person類和Salary類是關聯關係,程式碼如下: 
public class Person implements Serializable {

    private static final long serialVersionUID = 9146176880143026279L;

    private String name;

    private Salary salary;

    public Person(String _name, Salary _salary) {
        this.name = _name;
        this.salary = _salary;
    }

    //Setter和Getter方法略

}

這是兩個簡單的JavaBean,都實現了Serializable介面,具備了序列化的條件。首先計稅系統請求HR系統對一個Person物件進行序列化,把人員資訊和工資資訊傳遞到計稅系統中,程式碼如下: 
public class Serialize {
    public static void main(String[] args) {
        // 基本工資1000元,績效工資2500元
        Salary salary = new Salary(1000, 2500);
        // 記錄人員資訊
        Person person = new Person("張三", salary);
        // HR系統持久化,並傳遞到計稅系統
        SerializationUtils.writeObject(person);
    }
}

在通過網路傳輸到計稅系統後,進行反序列化,程式碼如下:
public class Deserialize {
    public static void main(String[] args) {
        Person p = (Person) SerializationUtils.readObject();
        StringBuffer buf = new StringBuffer();
        buf.append("姓名: "+p.getName());
        buf.append("\t基本工資: "+p.getSalary().getBasePay());
        buf.append("\t績效工資: "+p.getSalary().getBonus());
        System.out.println(buf);
    }
}

打印出的結果為:姓名: 張三    基本工資: 1000    績效工資: 2500

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

  1. 在bonus前加上關鍵字transient:這是一個方法,但不是一個好方法,加上transient關鍵字就標誌著Salary失去了分散式部署的功能,它可能是HR系統核心的類了,一旦遭遇效能瓶頸,再想實現分散式部署就可能了,此方案否定;
  2. 新增業務物件:增加一個Person4Tax類,完全為計稅系統服務,就是說它只有兩個屬性:姓名和基本工資。符合開閉原則,而且對原系統也沒有侵入性,只是增加了工作量而已。但是這個方法不是最優方法;
  3. 請求端過濾:在計稅系統獲得Person物件後,過濾掉Salary的bonus屬性,方案可行但不符合規矩,因為HR系統中的Salary類安全性竟然然外系統(計稅系統來承擔),設計嚴重失職;
  4. 變更傳輸契約:例如改用XML傳輸,或者重建一個WebSerive服務,可以做但成本很高。

下面展示一個優秀的方案,其中實現了Serializable介面的類可以實現兩個私有方法:writeObject和readObject,以影響和控制序列化和反序列化的過程。我們把Person類稍作修改,看看如何控制序列化和反序列化,程式碼如下:

public class Person implements Serializable {

    private static final long serialVersionUID = 9146176880143026279L;

    private String name;

    private transient Salary salary;

    public Person(String _name, Salary _salary) {
        this.name = _name;
        this.salary = _salary;
    }
    //序列化委託方法
    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();
        oos.writeInt(salary.getBasePay());
    }
    //反序列化委託方法
    private void readObject(ObjectInputStream input)throws ClassNotFoundException, IOException {
        input.defaultReadObject();
        salary = new Salary(input.readInt(), 0);
    }
}

其它程式碼不做任何改動,執行之後結果為:姓名: 張三    基本工資: 1000    績效工資: 0

在Person類中增加了writeObject和readObject兩個方法,並且訪問許可權都是私有級別,為什麼會改變程式的執行結果呢?其實這裡用了序列化的獨有機制:序列化回撥。Java呼叫ObjectOutputStream類把一個物件轉換成資料流時,會通過反射(Refection)檢查被序列化的類是否有writeObject方法,並且檢查其是否符合私有,無返回值的特性,若有,則會委託該方法進行物件序列化,若沒有,則由ObjectOutputStream按照預設規則繼續序列化。同樣,在從流資料恢復成例項物件時,也會檢查是否有一個私有的readObject方法,如果有,則會通過該方法讀取屬性值,此處有幾個關鍵點需要說明:

  1. oos.defaultWriteObject():告知JVM按照預設的規則寫入物件,慣例的寫法是寫在第一行。
  2. input.defaultReadObject():告知JVM按照預設規則讀入物件,慣例的寫法是寫在第一行。
  3. oos.writeXX和input.readXX

分別是寫入和讀出相應的值,類似一個佇列,先進先出,如果此處有複雜的資料邏輯,建議按封裝Collection物件處理。大家可能注意到上面的方式也是Person失去了分散式部署的能了,確實是,但是HR系統的難點和重點是薪水的計算,特別是績效工資,它所依賴的引數很複雜(僅從數量上說就有上百甚至上千種),計算公式也不簡單(一般是引入指令碼語言,個性化公式定製)而相對來說Person類基本上都是靜態屬性,計算的可能性不大,所以即使為效能考慮,Person類為分散式部署的意義也不大。