前言
所謂熱升級,實際上就是在程序/服務不停止的前提下,通過增加、修改、刪除相關功能模塊,達到功能升級的目的。
為什么要熱升級
舉個例子,我們可能都有這樣一個經歷,正在操作一個軟件,可能是個重要的工作,這個時候軟件發現有新的功能更新,需要升級程序,彈出一個看似很人性化的提示:請重新啟動程序以完成升級!但是,問題是,升級的功能可能跟我們當前工作所用的功能完全沒有關系,卻要我們丟棄辛辛苦苦做了半天的工作,就為了一個不相關的功能重做!我們當然也可不理,繼續做我們的工作,直到完成后重啟完成升級。但這顯然不是我們理想的方式,如果軟件是以敏捷開發模式做出的,幾乎不可避免的要頻繁升級程序,那么可以想象這會讓用戶多么煩惱!
特別是對於服務來說,我們總是希望保持穩定,希望7*24小時永不停止。所以,我們希望能有這樣一種方式,能夠直接更新相應的模塊,在不停止進程的情況下,保持服務為最新版本。下面我介紹兩種實現方式。
應用程序域(AppDomain)
在正式介紹前,我們還要在這里重新溫習一下一個重要的概念,應用程序域。
所謂應用程序域,.Net引入的一個概念,指的是一種邊界,它標識了代碼的運行范圍,在其中產生的任何行為,包括異常都不會影響到其他應用程序域,起到安全隔離的效果。也可以看成是一個輕量級的進程。
一個進程可以包含多個應用程序域,各個域之間相互獨立。如下是一個.net進程的組成(圖片來自網絡)
進程啟動后,會首先建立兩個應用程序域,一個叫公共程序域(Domain-Neutral),其中加載的所有類型可以供其他所有應用程序域使用;另一個叫默認程序域,它加載了我們自己的應用程序,默認程序域中運行的代碼可能導致整個進程崩潰。為了使我們的進程能夠穩定運行,就可以新建一個應用程序域來加載一些我們認為可能會導致問題的程序集,而在新的應用程序域中運行的代碼不會對默認程序域造成影響,保證了進程的穩定。
使用AppDomain類提供的靜態方法,可進行應用程序域的創建與卸載
//創建應用程序域 public static AppDomain CreateDomain(string friendlyName); //卸載應用程序域 public static void Unload(AppDomain domain);
其中,創建應用程序域方法CreateDomain還有其他多個重載,為我們提供豐富的創建新應用程序域所用的配置。
卸載應用程序域時,CLR將清理該應用程序域使用的所有資源,包括加載的程序集,未釋放的非托管資源等。但公共程序域和默認程序域無法卸載。
通常,我們封裝的功能都是以程序集的形式存在的,而程序集只有在應用程序域卸載以后才能釋放。這就是為什么在程序運行的過程中無法直接進行修改程序文件的原因,程序運行后,相關的程序集文件被加載到了默認應用程序域中,而默認應用程序域又無法卸載,導致我們不得不關閉進程才能修改相應文件。
而我們在程序啟動的時候,把這個程序集加載一個我們新建的應用程序域中,需要更新文件的時候只要卸載這個域,在不關閉進程的情況下,不就可以對文件進行更新了么?這個也就是我們能夠進行“熱升級”的理論基礎。
方式一
在新的應用程序域中加載功能模塊,需要更新時,卸載應用程序域,再重新加載。
下面用一個簡單具體的解決方案說明一下。
解決方案如下所示:
其中,MainServer是主程序,Module1是程序中要使用的功能模塊,實現了CommonLib項目中ICalculater接口
public interface ICalculater { int Calc(int a, int b); }
public class Calculater : MarshalByRefObject, ICalculater { public int Calc(int a, int b) { int res = a + b; Console.WriteLine("Add {0} and {1}, result: {2} [run in {3}]", a, b, res, AppDomain.CurrentDomain.FriendlyName); return res; } }
當前功能是計算兩個整數之和,后面我將通過修改該計算的實現,達到功能升級的目的。
注意:如果要在新的應用程序域中調用,類型必須繼承MarshalByRefObject,請網上參考-按引用封送和按值封送-相關文章。
首先,我們來看如何創建新的程序域,並調用域內方法
static void Main(string[] args) { Console.WriteLine("current domain: {0}", AppDomain.CurrentDomain.FriendlyName + Environment.NewLine); Console.WriteLine("input two data to calc: "); Console.Write("a: "); int a = Convert.ToInt32(Console.ReadLine()); Console.Write("b: "); int b = Convert.ToInt32(Console.ReadLine()); AppDomain ad_Calc = AppDomain.CreateDomain("domin #calc"); ICalculater calc = (ICalculater)ad_Calc.CreateInstanceAndUnwrap("Module1", "Module1.Calculater"); calc.Calc(a, b); }
其實代碼很簡單,通過調用方法CreateDomain創建新的應用程序域並給域命名為domain #calc后,調用CreateInstanceAndUnwrap,輸入程序集名和要創建的實例類型名后,即可獲得一個該類型的一個引用代理。面向接口的方式使我們可以直接通過強制轉換調用相應的方法(如果不面向接口的話,就要使用反射的方式調用方法,相對來說麻煩一些,速度也會慢一些)。執行過程中,讓程序打印出當前所在域的名稱,可以直觀地看到當前代碼是在哪里執行的。如下是運行結果
結果中我們能很清晰的看到,沒有調用Calc方法時,當前運行所在域是MainServer.exe(也即是默認應用程序域的名稱),調用Calc方法時,實際上是運行在domain #calc域中的。
既然實現了在新域中運行,接下來我們看下怎么對功能進行升級。
繼續在Main函數中添加代碼,在域創建並加載Module1成功后,嘗試直接刪除Module1.dll文件
string cmd; while ((cmd = Console.ReadLine()).ToLower() != "quit") { switch (cmd.ToLower()) { case "del_calc": TryDelete("Module1.dll"); Console.WriteLine(); break; case "unload_calc": UnloadDomain(ad_Calc); Console.WriteLine(); break; case "reload_and_run_calc": AppDomain domainNew; if (ReloadDomain(out domainNew)) { ICalculater calcNew = (ICalculater)domainNew.CreateInstanceAndUnwrap("Module1", "Module1.Calculater"); calcNew.Calc(a, b); } Console.WriteLine(); break; } }
結果顯然是,無法刪除,Module1.dll文件句柄還在程序域中沒有釋放。
卸載程序域,再刪除試試呢
成功了!也就是卸載程序域后,該程序域加載的文件句柄也相應被釋放,這時候可以自由地操作文件了,當然也就可以把我們新版本的功能替換上去了。
這時候我們修改Calculater中的Calc方法,返回a,b兩參數之積,並生成新的dll文件
public class Calculater : MarshalByRefObject, ICalculater { public int Calc(int a, int b) { int res = a * b; Console.WriteLine("Multiply {0} and {1}, result: {2} [run in {3}]", a, b, res, AppDomain.CurrentDomain.FriendlyName); return res; } }
重新創建應用程序域,為了和之前創建的程序域區分,命名為domin reload #calc ,並加載Module1,執行Calc方法
比較前后兩次調用的結果,功能升級成功!
這里只是介紹了作為功能提供模塊或沒有界面的模塊的升級方法,而對於需要作為主界面的子窗體的模塊如何實現熱升級?我將在下一篇進行介紹。
本節完整的代碼請 點擊下載