Java的反射破壞單例的私有建構函式保護,最典型的就是Spring的Bean注入,我們可以通過改造私有建構函式來防止。
在Singleton中,我們只對外提供工廠方法(獲取單例),而私有化建構函式,來防止外面多餘的建立。
對於一般的外部呼叫來說,私有建構函式已經很安全了。
public class Singleton {
private Singleton(){}
private static volatile Singleton instance = null;
public static SingletongetInstance() throws Exception {
if (instance ==null) { //為了簡潔度,暫不考慮執行緒安全
instance =new Singleton();
}
returninstance;
}
}
Class singletonClass = Class.forName("com.jscai.spring.demo.Singleton");
Constructor[] cts =singletonClass.getConstructors();
System.out.println(cts.length)
一般的外部呼叫,編譯器會校驗,直接提示編譯錯誤。而正常的反射也是找不到私有建構函式的,所以上面的輸出為0.
但是一些特權使用者可以通過反射來訪問私有建構函式,並且例項化:
Constructor[] cts = singletonClass.getDeclaredConstructors();
System.out.println(cts.length);
cts[0].setAccessible(true);
Singletonsingleton = (Singleton) cts[0].newInstance();
上述程式碼首先通過反射的getDeclaredConstructors()來獲取所有建構函式(public,protected,default * (package) access, andprivate constructors),當然這個函式會校驗呼叫者的許可權。
此時預設還是不能呼叫私有建構函式的,還需要把訪問許可權開啟setAccessible(true),就可以訪問私有建構函式了,這樣破壞了單例的私有建構函式保護。
如果要防禦這樣的反射侵入,可以修改建構函式,加上第二次例項化的檢查。(上面的getInstance()經過多執行緒(DoubleCheck)處理後,就不會出現執行緒衝突來觸發這個異常)
private static int cntInstance = 0;
private Singleton()throws Exception {
if (++cntInstance > 1 ) {
throw new Exception("Can'tcreate another instance.");
}
}
另外,在Spring的Bean注入中,即使你私有化建構函式,預設他還是會去呼叫你的私有建構函式去例項化。 【通過BeanFactory來裝配Bean,和上面的邏輯如出一轍】
所以,如果我們想保證例項的單一性,就要在定義<bean>時加上factory-method=""的屬性,並且在私有建構函式中新增防禦機制。單例的getInstance()可能會新增一些邏輯,而Spring的預設呼叫建構函式去建立,就不能保證這份邏輯的準確性,所以會帶來隱患。
我們可以通過scope="prototype"來測試單例是否被多次建立:
<beanid="test"class="com.jscai.spring.demo.Singleton"scope="prototype"></bean>
BeanFactory bf = new ClassPathXmlApplicationContext("demoAppTestContext.xml");
Singleton test1 = (Singleton) bf.getBean("singleton");
Singleton test2 = (Singleton) bf.getBean("singleton");
發現防禦機制生效,丟擲"Can't create another instance."的異常,證明Spring能正常呼叫私有的建構函式來建立Bean,並且建立了多次。
這時候我們要使用factory-method來指定工廠方法,才能達到我們想要的效果
<beanid="test"class="com.jscai.spring.demo.Singleton"scope="prototype"factory-method="getInstance"></bean>