今天說一說.NET 中的插件技術(shù),即 應(yīng)用程序熱升級。在很多情況下、我們希望用戶對應(yīng)用程序的升級是無感知的,并且盡可能不打斷用戶操作的。

雖然在Web 或者 WebAPI上,由于多點的存在可以逐個停用單點進行系統(tǒng)升級,而不影響整個服務(wù)。但是 客戶端卻不能這樣做,畢竟用戶一直在使用著。

那么有沒有一種方式,可以在用戶無感知的情況下(即、不停止進程的情況下)對客戶端進行升級呢?

答案是肯定的, 這就是我今天想說的插件技術(shù)、可以對應(yīng)用程序進行熱升級。當(dāng)然這種方式也同樣適用于 ASP.NET ,

不過當(dāng)前隨筆是以 WPF為例子的,并且原理是一樣的、代碼邏輯也是一樣的。

 

一、應(yīng)用程序域AppDomain

在介紹插件技術(shù)之前、我們需要先了解一些基礎(chǔ)性的知識,第一個就是應(yīng)用程序域AppDomain.

操作系統(tǒng)和運行時環(huán)境通常會在應(yīng)用程序間提供某種形式的隔離。 例如,Windows 使用進程來隔離應(yīng)用程序。 為確保在一個應(yīng)用程序中運行的代碼不會對其他不相關(guān)的應(yīng)用程序產(chǎn)生不良影響,這種隔離是必需的。這種隔離可以為應(yīng)用程序域提供安全性、可靠性, 并且為卸載程序集提供了可能。

 

在 .NET中應(yīng)用程序域AppDomain是CLR的運行單元,它可以加載應(yīng)用程序集Assembly、創(chuàng)建對象以及執(zhí)行程序。

在 CLR 里、AppDomain就是用來實現(xiàn)代碼隔離的,每一個AppDomain可以單獨創(chuàng)建、運行、卸載。

 

關(guān)于AppDomain中的未處理異常

如果默認(rèn)AppDomain監(jiān)聽了 UnhandledException 事件,任何線程的任何未處理異常都會引發(fā)該事件,無論線程是從哪個AppDomain中開始的。

如果一個線程開始于一個已經(jīng)監(jiān)聽了 UnhandledException事件的 app domain, 那么該事件將在這個app domain 中引發(fā)。

如果這個app domian 不是默認(rèn)的app domain, 并且 默認(rèn) app domain 中也監(jiān)聽了 UnhandledException 事件, 那么 該事件將會在兩個app domain 中引發(fā)。

 

CLR啟用時,會創(chuàng)建一個默認(rèn)的AppDomain,程序的入口點(Main方法)就是在這個默認(rèn)的AppDomain中執(zhí)行。

AppDomain是可以在運行時進行動態(tài)的創(chuàng)建和卸載的,正因如此,才為插件技術(shù)提供了基礎(chǔ)(注:應(yīng)用程序集和類型是不能卸載的,只能卸載整個AppDomain)。

 

AppDomain和其他概念之間的關(guān)系

1、AppDomain vs 進程Process

AppDomain被創(chuàng)建在Process中,一個Process內(nèi)可以有多個AppDomain。一個AppDomain只能屬于一個Process。

2、AppDomain vs 線程Thread

應(yīng)該說兩者之間沒有關(guān)系,AppDomain出現(xiàn)的目的是隔離,隔離對象,而 Thread 是 Process中的一個實體、是程序執(zhí)行流中的最小單元,保存有當(dāng)前指令指針 和 寄存器集合,為線程(上下文)切換提供可能。如果說有關(guān)系的話,可以牽強的認(rèn)為一個Thread可以使用多個AppDomain中的對象,一個AppDomain中可以使用多個Thread.

3、AppDomain vs 應(yīng)用程序集Assembly

Assembly是.Net程序的基本部署單元,它可以為CLR提供元數(shù)據(jù)等。

Assembly不能單獨執(zhí)行,它必須被加載到AppDomain中,然后由AppDomain創(chuàng)建程序集中的類型 及 對象。

一個Assembly可以被多個AppDomain加載,一個AppDomain可以加載多個Assembly。

每個AppDomain引用到某個類型的時候需要把相應(yīng)的assembly在各自的AppDomain中初始化。因此,每個AppDomain會單獨保持一個類的靜態(tài)變量。

4、AppDomain vs 對象object
任何對象只能屬于一個AppDomain,AppDomain用來隔離對象。 同一應(yīng)用程序域中的對象直接通信、不同應(yīng)用程序域中的對象的通信方式有兩種:一種是跨應(yīng)用程序域邊界傳輸對象副本(通過序列化對對象進行隱式值封送完成),一種是使用代理交換消息。

 

二、創(chuàng)建 和 卸載AppDomain

前文已經(jīng)說明了,我們可以在運行時動態(tài)的創(chuàng)建和卸載AppDomain, 有這樣的理論基礎(chǔ)在、我們就可以熱升級應(yīng)用程序了 。

那就讓我們來看一下如何創(chuàng)建和卸載AppDomain吧

創(chuàng)建:

                AppDomainSetup objSetup = new AppDomainSetup();
                objSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;                this.domain = AppDomain.CreateDomain("RemoteAppDomain", null, objSetup);

創(chuàng)建AppDomain的邏輯非常簡單:使用 AppDomain.CreateDomain 靜態(tài)方法、傳遞了一個任意字符串 和 AppDomainSetup 對象。

 

卸載:

              AppDomain.Unload(this.domain);

卸載就更簡單了一行代碼搞定:AppDomain.Unload 靜態(tài)方法,參數(shù)就一個 之前創(chuàng)建的AppDomain對象。

 

三、在新AppDomain中創(chuàng)建對象

上文已經(jīng)說了創(chuàng)建AppDomain了,但是創(chuàng)建的新AppDomain卻是不包含任何對象的,只是一個空殼子。那么如何在新的AppDomain中創(chuàng)建對象呢?

this.remoteIPlugin = this.domain.CreateInstance("PluginDemo.NewDomain", "PluginDemo.NewDomain.Plugin").Unwrap() as IPlugin;

 使用剛創(chuàng)建的AppDomain對象的實例化方法: this.domain.CreateInstance,傳遞了兩個字符串,分別為 assemblyName 和 typeName.

并且該方法的重載方法 和 相似功能的重載方法多達(dá)十幾個。

  

四、影像復(fù)制程序集

 創(chuàng)建、卸載AppDomain都有、創(chuàng)建新對象也可以了,但是如果想完成熱升級,還有一點小麻煩,那就是一個程序集被加載后會被鎖定,這時候是無法對其進行修改的。

所以就需要打開 影像復(fù)制程序集 功能,這樣在卸載AppDomain后,把需要升級的應(yīng)用程序集進行升級替換,然后再創(chuàng)建新的AppDomain即可了。

打開 影像復(fù)制程序集 功能,需要在創(chuàng)建新的AppDomain時做兩步簡單的設(shè)定即可:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

                AppDomainSetup objSetup = == .domain = AppDomain.CreateDomain(, , objSetup);

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

 

 五、簡單的Demo

 現(xiàn)有一接口IPlugin:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn) 接口 IPlugin

 

在另外的一個程序集中有其一個實現(xiàn)類 Plugin:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn) 實現(xiàn)類 Plugin

在另外的一個程序集中還有一個 

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn) 空類型 NonMarshalByRefObject

 

測試程序如下:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn) 測試程序

按測試程序代碼執(zhí)行,先Load AppDomain, 然后 Access Other Member, 此時會發(fā)現(xiàn)出現(xiàn)了異常,大致內(nèi)容如下:

創(chuàng)建AppDomain成功

GetInt():1020
GetString():iqingyu
GetNonMarshalByRefObject():程序集“PluginDemo.NewDomain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null”中的類型“PluginDemo.NewDomain.NonMarshalByRefObject”未標(biāo)記為可序列化。

 是由于 PluginDemo.NewDomain.NonMarshalByRefObject 這個類型未標(biāo)記可序列化 而引發(fā)的。 那么這種情況下和序列化又有什么關(guān)系呢?

請繼續(xù)往下看。 

 

 六、AppDomain間的對象通信

 前文說過了,AppDomain 是用來隔離對象的,AppDomain 之間的對象是不可以隨意通信的,這一點在 MSND的備注 中有一段描述:

應(yīng)用程序域是一個操作系統(tǒng)進程中一個或多個應(yīng)用程序所駐留的分區(qū)。 同一應(yīng)用程序域中的對象直接通信。 不同應(yīng)用程序域中的對象的通信方式有兩種:一種是跨應(yīng)用程序域邊界傳輸對象副本,一種是使用代理交換消息。

MarshalByRefObject 是通過使用代理交換消息來跨應(yīng)用程序域邊界進行通信的對象的基類。 不是從 MarshalByRefObject 繼承的對象根據(jù)值隱式封送。 當(dāng)遠(yuǎn)程應(yīng)用程序引用根據(jù)值封送的對象時,將跨應(yīng)用程序域邊界傳遞該對象的副本。

MarshalByRefObject 對象在本地應(yīng)用程序域的邊界內(nèi)可直接訪問。 遠(yuǎn)程應(yīng)用程序域中的應(yīng)用程序首次訪問 MarshalByRefObject 時,會向該遠(yuǎn)程應(yīng)用程序傳遞代理。 對該代理后面的調(diào)用將封送回駐留在本地應(yīng)用程序域中的對象。

當(dāng)跨應(yīng)用程序域邊界使用類型時,類型必須是從 MarshalByRefObject 繼承的,而且由于對象的成員在創(chuàng)建它們的應(yīng)用程序域之外無法使用,所以不得復(fù)制對象的狀態(tài)。

 也就是說AppDomain間的對象通信有兩種方式:一種是繼承 MarshalByRefObject ,擁有使用代理交換消息的能力,另外一種是利用序列化、傳遞對象副本。

第一種:表現(xiàn)形式上來說,傳遞的是對象引用。 第二種 傳遞的是對象副本,也就是說不是同一個對象。

 

也正因此,由于 PluginDemo.NewDomain.NonMarshalByRefObject 即不是 MarshalByRefObject 的子類,也不可以進行序列化,故 不可在兩個不同的AppDomain間通信。

而上面的異常,則是由序列化  PluginDemo.NewDomain.NonMarshalByRefObject 對象失敗導(dǎo)致的異常。

 

如果一個類型 【不是】 MarshalByRefObject的子類 并且 【沒有標(biāo)記】 SerializableAttribute, 
則該類型的對象不能被其他AppDomain中的對象所訪問, 當(dāng)然這種情況下的該類型對象中的成員也不可能被訪問到了 
反之,則可以被其他AppDomain中的對象所訪問

如果一個類型 【是】 MarshalByRefObject的子類, 則跨AppDomain所得到的是 【對象的引用】(為了好理解說成對象引用,實質(zhì)為代理)

如果一個類型 【標(biāo)記】 SerializableAttribute, 則跨AppDomain所得到的是 【對象的副本】,該副本是通過序列化進行值封送的 
此時傳遞到其他AppDomain 中的對象 和 當(dāng)前對象已經(jīng)不是同一個對象了(只傳遞了副本)

如果一個類型 【是】 MarshalByRefObject的子類 并且 【標(biāo)記了】 SerializableAttribute, 
則 MarshalByRefObject 的優(yōu)先級更高

 

另外:.net 基本類型 、string 類型、 List<T> 等類型,雖然沒有標(biāo)記 SerializableAttribute, 但是他們依然可以序列化。也就是說這些類型都可以在不同的AppDomain之間通信,只是傳遞的都是對象副本。

 

七、完整的Demo

完整的Demo筆者已上傳至Github,  https://github.com/iqingyu/BlogsDemo :

PluginDemo

PluginDemo.NewDomain

兩個項目為完整的Demo

http://www.cnblogs.com/08shiyan/p/6733651.html