天天看點

《Java安全編碼标準》一3.1 DCL00-J防止類的循環初始化

在java語言規範(java language specification, jls)第 12.4節“對類和接口的初始化”中提到[jls 2005]:

對類進行的初始化包括執行該類的static靜态初始化方法和初始化該類中的靜态資料成員(類變量)。

換句話說,一個靜态資料成員的出現會觸發類的初始化。然而,一個靜态資料成員可能會依賴于其他類的初始化,這樣有可能形成一個初始化循環。在jls的8.3.2.1節“類變量的初始化”中也提到[jls 2005]:

在運作态中,使用編譯期的常量來初始化的final的static變量,是最先初始化的。

這個jls的描述會讓人誤解。誤解之處在于,對于某些對象執行個體而言,變量就算是static final的,它們的初始化也可能會安排在後期進行。聲明一個字段為static final并不能夠保證它在被讀之前已經完全初始化。

一般來說,程式,特别是對安全敏感的程式,必須消除所有的類初始化循環。

不符合規則的代碼示例如下,它存在一個涉及多個類的類初始化循環。

cycle類聲明了一個private static final類變量,這個變量會在建立cycle對象的時候進行初始化。靜态的初始化方法可以保證隻調用一次,而這次調用是在第一次使用靜态類變量或者在第一次調用構造函數之前發生。

程式員希望通過在存款賬戶中減去處理費用來計算賬戶餘額。然而,對類變量c的初始化在deposit變量被初始化之前發生,因為在編碼上,它在deposit域的初始化之前出現。是以,當對變量c進行靜态初始化時,cycle類的構造函數會讀取deposit的數值,這個deposit數值會是0而并非一個随機值。結果是,賬戶餘額計算下來的值是-10。

jls允許實作忽略這種可能出現的循環的初始化[bloch 2005a]。

這個方案改變了類cycle的初始化次序,是以對資料成員的初始化是不會造成任何依賴循環的。特别是對c的初始化,因為在編碼上處于對deposit初始化之後發生,因而它會在deposit被完全初始化之後進行初始化。

當涉及許多字段的時候,并不容易發現這樣的初始化循環。是以,確定控制流不産生這樣的循環就非常重要。

盡管這個方案可以防止初始化循環,但它也依賴于聲明次序,因而也是脆弱的。程式的維護者可能不知道這種聲明的次序必須被保持來保證程式的正确性。因而,這種依賴必須在代碼的文檔中清楚地說明。

這個不符合規則的代碼示例聲明了兩個類,這兩個類都有靜态變量,這些變量的值是互相依賴的。當把這兩個類放在一起的時候,這個循環是很顯然的;而當把它們分開的時候,卻很容易忽略這點。

因為對這些類的初始化次序是可變的,是以導緻的結果是,會計算出不同的a.a?和b.b的值。當先初始化a類時,a.a的值是2,而b.b的值是1。當先初始化b類時,這兩個值就要反過來了。

與規則一緻的方案打破了這個類間循環,這是通過消除其中一個依賴來完成的。

當打破這個循環時,初始化的值是不變的,總是a.a=2且b.b=3,并且它與初始化次序無關。

初始化循環會導緻不可預測的結果。

《Java安全編碼标準》一3.1 DCL00-J防止類的循環初始化
《Java安全編碼标準》一3.1 DCL00-J防止類的循環初始化
《Java安全編碼标準》一3.1 DCL00-J防止類的循環初始化
《Java安全編碼标準》一3.1 DCL00-J防止類的循環初始化