【.net 深呼吸】程序集的熱更新

當一個程序集被加載使用的時候,出於數據的完整性和安全性考慮,程序集文件(在99.9998%的狀況下是.dll文件)會被鎖定,若是此時你想更新程序集(其實是替換dll文件),是不能夠操做的,這時你得把應用程序退出,替換文件後再啓動程序。緩存

多數狀況下這樣作是可行的,只是有時候,好比ASP.NET或一些須要一直運行的服務進程,重啓程序來更新好像不太好。安全

要是想對程序集進行熱更新,即在程序運行的同時替換文件,有一個你們很熟悉的方案——影像複製,若是你不熟悉.net,你確定沒據說過的。固然了,這個叫法也挺難聽的,沒辦法,只好這樣翻譯,原詞是 Shadow Copy ,Shadow是影子,陰影,影像的意思,那也只好這麼翻譯了。不過,你不用擔憂它很抽象很高端,其實,只要用心學,沒什麼東西是攻不克的。函數

我用一句話來歸納一下影子複製(也能夠叫拷貝,但我不喜歡拷貝這個詞,很黃很暴力的感受)——應用程序域在加載程序集時,會把程序集文件複製到另外一個地方,再進行加載。這樣一來,當程序集文件被使用時,它鎖定的是複製後的文件,即原始文件咱們能夠放心地去替換了,等到合適的時間,把應用程序從新啓動一下,再次運行時,就會自動把最新的程序集複製到緩存的目錄下,而後執行最新版本的代碼。最好把這些代碼的調用放到一個新的應用程序域中執行,由於這樣的好處是不用從新啓動應用程序,而只要把某個應用程序域卸載掉再從新建立一個新的,就會自動加載最新的程序集了。並且,一般你都應該這麼作的,建立一個應用程序域,在裏面執行代碼,執行完了就把應用程序域卸掉,能夠節約資源。測試

應用程序在運行的時候,默認會建立一個應用程序域的,說白了,一個進程中至少會有一個應用程序域,若是你把某段代碼放到一個新的應用程序域中執行,而且你但願執行完後,能夠把結果傳回給主應用程序域,那就用老周之前寫過的方法,記得老周前面寫過的,想按引用傳遞對象,就從MarshalByRefObject類派生,想讓對象按值傳遞,就讓它支持序列化。spa

 

在建立新的應用程序域時,能夠同時傳遞一個SetupInfo對象,這個對象有一個 ShadowCopyFiles 屬性,雖然它定義的類型是 string,但你千萬不要理解錯,不要把一個文件的路徑賦給它。老周之前就見到一位朋友理解錯了,它誤覺得這個屬性是用來設置複製程序集文件的緩存路徑,結果代碼寫了總是不行。唉,這就是不看MSDN的下場。.net

不要亂來,設置複製程序集的緩存目錄是 CachePath 屬性,不是 ShadowCopyFiles 屬性。ShadowCopyFiles 屬性只能用兩個字符串的值,若是要啓用影像複製,就設置爲 true,若是想禁用,就設置 false 或者乾脆保持默認的null值。也就說,它是一個用字符串表示的 bool 值。命令行

 

下面,咱們用一個例子來表演一下,很精彩的。翻譯

首先,弄一個類庫項目,而後在裏面寫一個全宇宙最簡單的類。3d

namespace TestLib
{
    public class Demo
    {
        public string Call()
        {
            return "Ver - 3";
        }
    }
}

 

而主啓動項目是一個控制檯應用,這裏,老周但願設置新應用程序域的 PrivateBinPath ,這個屬性能夠設置一堆目錄,能夠是相對路徑,其實應該是用相對路徑的,由於這個目錄不能亂設的,它必須是應用程序目錄的子目錄。若是是多個目錄,能夠用英文的分號(;)來分隔。code

ApplicationBase路徑指定的是應用程序,即.exe啓動的目錄,無論你建立多個新的應用程序域,這個目錄都必須指定爲當前exe的啓動目錄。不然你試試看,不能運行的,由於應用程序域之間是隔離的,因此在新建立的應用程序域中也必須加載當前exe所在的程序集,這個程序集是必須的,由於它是主入口點。

而 PrivateBinPath 屬性所指定的路徑必須爲應用程序目錄的子目錄,好比,咱們的項目在Debug模式下,一般是把exe生成到 bin \ Debug目錄下的,因此,你能夠在Debug目錄下建立一個子目錄,我這裏創了一個,叫ExtDlls,隨後我會把要用到的dll文件放在這個目錄中,並設置 PrivateBinPath = "ExtDlls" ,這樣一來,就算項目不引用這個類庫項目,在運行階段它都會自動到這個 ExtDlls 目錄下去找,找到了就加載,要是找不到就會「呵呵」。

我這個類庫項目名叫 TestLib,爲了讓它生成後可以自動把最新的版本複製到 ExtDlls 目錄中,能夠打開類庫項目的項目屬性窗口,切換到【生成事件】頁,並在「後期生成命令行」中輸入如下命令:

copy "$(TargetPath)" "$(SolutionDir)MyApp\bin\Debug\ExtDlls\"

這麼一搞,每次我從新生成類庫項目後,就會自動把dll文件複製過去。

 

好,下面的重點放在主項目上,在代碼中,能夠建立一個新的應用程序域,而後調用類庫中的代碼。

                AppDomainSetup setup = new AppDomainSetup();
                setup.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
                setup.ApplicationName = "ExtFuncs";
                setup.PrivateBinPath = "ExtDlls";
                setup.ShadowCopyFiles = "true";
                AppDomain newDom = AppDomain.CreateDomain("hello", null, setup);

                newDom.DoCallBack(() =>
                {
                    Type t = Type.GetType("TestLib.Demo, TestLib");
                    // 獲取公共無參構造函數
                    ConstructorInfo costr = t.GetConstructor(new Type[] { });
                    // 調用構造函數,建立類型實例
                    object instance = costr.Invoke(new object[] { });
                    // 找到要調用的方法
                    MethodInfo m = t.GetMethod("Call", BindingFlags.Public | BindingFlags.Instance);
                    // 調用方法,獲得返回值
                    object retval = m.Invoke(instance, new object[] { });
                    Console.WriteLine($"調用輸出:{retval}");
                    Console.WriteLine("\n===================================");

                    // 輸出引用程序集的路徑
                    var refAsses = AppDomain.CurrentDomain.GetAssemblies();
                    foreach (var ass in refAsses)
                    {
                        Console.WriteLine("名稱:"+ ass.GetName().Name);
                        Console.WriteLine("路徑:" + ass.Location);
                        Console.WriteLine();
                    }
                });
                AppDomain.Unload(newDom); //卸載應用程序域

 

實驗代表,ApplicationName 屬性的值能夠隨便寫,但 ApplicationBase 屬性必須是當前應用程序所在目錄。

這裏我用的是反射的方法來調用的,DoCallBack 方法容許在另外一個應用程序域中執行代碼,代碼內容經過一個委託來關聯。

 

在反射調用完測試類庫後,我還用這段代碼來輸出新的應用程序域所引用的全部程序集的路徑。

                    var refAsses = AppDomain.CurrentDomain.GetAssemblies();
                    foreach (var ass in refAsses)
                    {
                        Console.WriteLine("名稱:"+ ass.GetName().Name);
                        Console.WriteLine("路徑:" + ass.Location);
                        Console.WriteLine();
                    }

因爲這段代碼是在新的應用程序域中執行的,因此 CurrentDomain 屬性所指的是新建立的應用程序域,而不是進程運行時建立的默認域。

之因此要在反射以後輸出路徑是由於,應用程序域是動態加載程序集,即當你用到類庫中的類型時纔會加載,若是不訪問類庫中的任何東西,是不會加載這個程序集的。

我爲啥要輸出路徑呢,就是讓大夥可以清楚地看到,TestLib 類庫已經被複制到另外一個目錄中執行了。請看:

從這個圖你就看到,默認的緩存程序集的路徑是在你的用戶配置目錄下的 AppData \ Local \ assembly 下面。

 

可能你以爲這個默認的緩存路徑很差,能不能自定義啊?能,前面老周提了一下 CachePath 屬性,對,你給這個屬性分配一個路徑,緩存的程序集就會放到這個自定義路徑中了。好比,我在Debug目錄下新建一個 TempAss 目錄,用來存放臨時複製的程序集。

setup.CachePath = CACHE_PATH;

 

而後你再看它的路徑。

看,是否是變了?

 

如今,咱們來驗證一下,是否是能夠熱更新。

先運行exe,輸出Ver - 1 ,如圖。

 

好,保持exe運行着,不要關,而後修改一下類庫項目的代碼。

    public class Demo
    {
        public string Call()
        {
            return "Ver - 2";
        }
    }

把 1 改成 2。

 

從新生成一下類庫項目,它會自動複製到 ExtDlls 目錄。

如今在控制檯窗口按除 Esc 之外的任意鍵,就會從新建一個應用程序域,並加載執行類庫代碼,由於我弄了個循環,只有遇到Esc鍵纔會退出。

這時候,你看到,輸出的內容變了。

 

不用退出應用程序,就能實現程序集文件的替換了,這對於服務應用特別好使。

 

爲了寫代碼有智能提示,若是我不想用反射呢,而是直接在VS中引用類庫項目呢,試試,引用以後,把所TestLib屬性中的「複製本地」改成false,由於 ExtDlls 目錄下已經有文件了,沒必要再複製了,在新的應用程序域中執行時,會自動搜索。

 

而後把DoCallBack 方法中的代碼改一下:

                newDom.DoCallBack(() =>
                {
                    TestLib.Demo dm = new TestLib.Demo();
                    Console.WriteLine($"輸出:{dm.Call()}");
                });

如今代碼就變得簡單多了,是吧,才兩行就完事了。

 

那能不能運行呢,固然能了。看。

 

怎麼樣,牛逼烘烘吧。

好了,老周的芹菜炒魚蛋飯作好了,肚子餓了,開飯了。

示例源代碼下載

相關文章
相關標籤/搜索