java中如何創建一個不會被反射破壞的單例

前言

單例相信各位都不陌生,從學習java開始就會經常接觸到這個概念,首先先來回憶一下什麼是單例:

單例模式是設計模式中最簡單的形式之一。這一模式的目的是使得類的一個對象成爲系統中的唯一實例。要實現這一點,可以從客戶端對其進行實例化開始。因此需要用一種只允許生成對象類的唯一實例的機制,“阻止”所有想要生成對象的訪問。使用工廠方法來限制實例化過程。這個方法應該是靜態方法(類方法),因爲讓類的實例去生成另一個唯一實例毫無意義。


————來自百度百科。

簡單來說,就是讓你的對象全局只有一個,而且還是你想要別人獲取到的那一個。

以下代碼測試使用環境:
java版本:1.8
編譯器:idea、eclipse


預熱一下

那麼怎麼做一個單例呢?先拋開反射這種情況不說,簡單的來一個小小的單例demo:

	/** 一個簡單的單例demo */
	public class Singleton {

	/** 唯一的單例 */
    private static volatile Singleton singleton = null;
 
 	/** 構造私有化 */
    private Singleton(){}
 
 	/** 獲取實例的工廠方法 */
    public static Singleton getSingleton(){
        synchronized (Singleton.class){
            if(singleton == null){
                singleton = new Singleton();
            }
        }
        return singleton;
    }    

從上面的代碼我們可以看到,

  • 構造是私有的,外界無法直接通過new來創建一個新的對象實例
  • 類內存在一個私有的、靜態的、被volatile修飾的字段,作爲唯一單例對象。
  • 存在一個工廠方法,當唯一單例對象不存在則創建一個並保存,如果有則直接返回。同時還使用synchronized關鍵字來保證線程的安全。

上面這個就是一個很簡單的、有線程安全的懶漢式單例。(如果不清楚什麼是懶漢、餓漢式的話可以去百度下補補功課哦)
但是問題就來了:在這個單例中,構造和字段都是私有的沒錯,但是通過反射可以輕鬆的去破壞他們。那麼,我要怎麼才能做到絕對的安全呢?

步入正題

首先我們先來思考一下,單例,需要全局唯一,且不可以被篡改。

  1. 怎樣才能夠全局唯一?
    如果侷限在一個類中的話,那麼使用static靜態字段來保存一個單例對象可以做到唯一,就像上面的那個示例一樣。*

  2. 怎樣才能夠不被篡改?
    *有兩種方法可以防止篡改。
    第一,使用final關鍵字對字段進行修飾。Java中的final字段的作用就是使一個字段作爲常量無法修改。

    第二,使用異常。當一個人想要創建一個新的對象的時候,你直接拋出一個異常,讓他嚐嚐苦頭。*

綜上所述,根據上面的線索來制定一個計劃吧。
當我的類一初始化,便使類中出現一個常量作爲唯一的單例實例,然後在構造方法中拋出異常,直接打斷想要創建新實例的人的想法。
哦~是個不錯的計劃呢。

好了,思想工作做好了,怎樣才能利用上面的幾點來實現一個安全的單例呢?

/**@ 一個反射安全的單例實例Forte */
public class Singleton {
	
	/** 一個常量,作爲單例的唯一實例Scarlet */
	private static final Singleton singleton;
	/** 隨便再來一個字段 */
	private final String name;

	/** 靜態代碼塊,會在類加載的時候執行,並且在這個類的一生中只會執行一次,利用這個特性來初始化靜態常量 */
	static {
		singleton = new Singleton("這是一個名稱");
	}
	
	/** 私有構造,就算是利用反射調用也只會迎接拋出的一個異常罷了 */	
	private Singleton(String name) {
		if(singleton != null) {
			throw new RuntimeException("這可是個單例啊!");
		}
		this.name = name;
	}
	
	/** 提供一個工廠方法來獲取單例的實例對象 */
	public static Singleton getInstance() {
		return singleton;
	}
	
	/** 獲取name */
	public String getName() {
		return name;
	}
}

如上所示,就形成了一個不會被反射所破壞的單例。
首先,單例對象通過final字段保護,不會被反射暴力破壞,其次構造方法內部也會直接拋出異常,阻止創建實例。


更深一步,出個難題

比點到爲止要再向前邁出一小步

什麼?假如說你想要在代碼運行期間修改單例的實例?
以上面的舉例代碼爲準,假如說你想要在代碼運行期間,出於某種原因去修改name這個靜態常量字符串,要怎麼辦纔好?
首先,想要修改他的值,那麼就不能使用final關鍵字來修飾了。那麼,想要能夠修改name這個字段,只有去掉singleton字段的final關鍵字。在你想要修改的時候先將其賦值爲null,然後再重新new一個新的對象。
但是說到這裏,問題出現了。當我去掉final字段的一瞬間,豈不是偏離了這篇帖子的主題?沒有了final的保護,反射豈不是可以趁虛而入?
彆着急,接下來我將會展示從反射手下保護字段和方法的另一個手段:

/**@ 一個反射安全的單例實例Forte */
public class Singleton {
	
	/** 一個常量,作爲單例的唯一實例Scarlet,不再使用final修飾 */
	private static Singleton singleton;
	/** 隨便再來一個字段 */
	private final String name;

	/** 靜態代碼塊,會在類加載的時候執行,並且在這個類的一生中只會執行一次,利用這個特性來初始化靜態常量 */
	static {
		//反射保護,保護Singleton.class的'name'字段
		Reflection.registerFieldsToFilter(Singleton.class, "name");
		singleton = new Singleton("這是一個名稱");
	}
	
	/** 私有構造,就算是利用反射調用也只會迎接拋出的一個異常罷了 */	
	private Singleton(String name) {
		if(singleton != null) {
			throw new RuntimeException("這可是個單例啊!");
		}
		this.name = name;
	}
	
	/** 提供一個工廠方法來獲取單例的實例對象 */
	public static Singleton getInstance() {
		return singleton;
	}

	/** 對單例的重新賦值, 通過synchronized保證線程安全 */
	public static synchronized Singleton reset(String name){
		//賦值爲null
		singleton = null;
		//重新賦值
		singleton = new Singleton(name);
		//獲取新值
		return getInstance();
	}
	
	/** 獲取name */
	public String getName() {
		return name;
	}
}

聰明的你已經發現了,去掉final後,我在靜態代碼塊裏增加了一段新的代碼:

	Reflection.registerFieldsToFilter(Singleton.class, "name");

在這裏我想對不熟悉Reflection這個類的人賣個關子,我會在我後續的帖子中來聊聊這個類。但是在現在,我只告訴你這段代碼的最終結果:
他會將Singleton.class類中的name字段註冊到反射過濾器,註冊完成後在你使用反射獲取類中字段、方法的時候便不會獲取到你註冊過後的字段或方法。於是便相當於從反射手中保護了這些字段和方法。

有興趣的朋友可以試試通過反射去獲取System類中的classLoader這個字段


更難的難題

首先先說一下,這個標題下的難題大部分我已經無力解決了…如果有誰能夠解決下面將會提到的問題中我沒有給出解決方案的問題,歡迎評論或者偷偷告訴我~

問題如下:

  • 如果我想要用懶漢式該怎麼辦呢?

    如果想要用懶漢式的話,只能通過使用Reflection反射保護來實現了,final是無法後期賦值的。

  • 利用枚舉不也可以直接實現單例嗎?

    普通層面 上來講的話,是可以的。但是從 我就是硬要槓你! 的角度來講的話,通過反射去破壞枚舉類型的單例的難易度,遠遠小於去繞過Reflection對反射字段、方法的過濾和修改final字段的值的難度(或者說繁瑣度)。

  • 第一個單例中,假如我使用Unsafe類和對class的深度反射拆解方法步驟去破壞final字段的值,該怎麼辦呢?

    這個問題我暫時是沒有解決方案的。既然你都祭出了通過底層原理強行修改final字段值的方法,我是沒有什麼保護措施的。
    但是通過我個人的測試經驗來講,final字段的值在被修改後(我通過debug模式確認了字段的值已經被修改),在程序的執行過程中依舊是你這個final字段修改前的值。原因或許是因爲字段內存地址的分配原則導致,也有可能是其他原因導致,這我不得而知,但是我知道的是當你通過底層代碼修改了final字段的之後,有很大可能在程序執行過程中已經是使用的他原本的值。

  • 第二個單例中,假如我在Reflection註冊字段之前去利用反射獲取字段或者通過對class的深度反射拆解方法步驟來繞過Reflection對字段獲取的過濾,該怎麼辦呢?

    這個我也沒轍啦。這樣通過繞過字段過濾或者在註冊前就先提前拿到字段對象的行爲我是暫時沒有想到辦法避免的,除非你把你的代碼寫進java源碼中,做到jvm一啓動,你的反射保護就被註冊這樣。


結束啦

辛辛苦苦寫完了這篇帖子,如果你喜歡的話請點贊收藏投硬幣 評個論,收個藏,關個注之類的~
以上帖子來源於個人經驗,如要轉載請註明來源出處。我一般喜歡用ForeScarlet這個名字o( ̄▽ ̄)ブ

如果你有什麼更好的方法、更加便捷的操作,可以從評論或者私信指教我;
如果你發現了我有什麼地方說的不對,請指正我;
如果你只是看我不爽想罵我,請別罵的太難聽。
感謝您百忙中閱讀我的文章~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章