动态加载AppDomains

转自MSDN
http://www.microsoft.com/taiwan/msdn/library/2002/Aug-2002/article/csharp05162002.htm
作者:Eric Gunnerson
Microsoft Corporation

AppDomains

最近,我正在编写一些代码来处理 AppDomains 问题。AppDomains 提供了一种方法,可以将程序的一部分与另一部分分开。简单地说,它使您可以在两个独立的环境中运行。有时,这两个环境可能位于不同的计算机上,AppDomains 之间的通信通过远程进行;在其他情况下,它们可能处于同一进程中。



除提供隔离功能外,AppDomains 还提供了一个重要的附加功能。如果 AppDomain 被卸载,它加载的程序集也将被卸载。这样可以实现系统的动态更新。例如,服务器进程可以检测到已更新的程序集的到达时间,关闭旧程序集并开始使用新的程序集。



对 AppDomains 的处理在刚开始时显得有点怪。因为从当前的 AppDomain 中分离出了一个单独的 AppDomain,您不能在它们之间自由地传递对象。它们需要被封送。您需要告诉新的 AppDomain 创建一个新对象,并为其返回一个代理,然后通过该代理进行调用。



代理对象必须来自 MarshalByRefObject,因为这是代理的实现方式。我们将使用下面的代理对象来加载程序集并对其进行某些处理:



namespace AppDomainAssemblyLoader

{

   public class AssemblyProcessor : MarshalByRefObject

   {

      Assembly assembly;

      public AssemblyProcessor()

      {

      }

      public void Load(string assemblyName)

      {

         assembly = Assembly.Load(assemblyName);

      }

      public void Process()

      {

      }

   }

}



这个类只提供了使用 Assembly.Load() 加载程序集的方法,以及进行处理的方法。该类必须被编译到一个独立的程序集中,稍后我将解释其原因。



代码的真正内容位于创建新 AppDomain 的类中:



public class AssemblyLoader: IDisposable

{

   AppDomain appDomain;



   public AssemblyLoader()

   {

   }



   void Dispose(bool disposing)

   {

      if (appDomain != null)

      {

         AppDomain.Unload(appDomain);

         appDomain = null;

      }

   }



   ~AssemblyLoader()

   {

      Dispose(false);

   }



   public void Dispose()

   {

      Dispose(true);

   }



   public void ProcessAssembly(string filename)

   {

      FileInfo fileInfo = new FileInfo(filename);



      AppDomainSetup setup = new AppDomainSetup();

      setup.ApplicationBase = fileInfo.DirectoryName;

      setup.PrivateBinPath =

         AppDomain.CurrentDomain.BaseDirectory;

      setup.ApplicationName = "Loader";

      setup.ShadowCopyFiles = "true";



      try

      {

         appDomain = AppDomain.CreateDomain(

            "Loading Domain", null, setup);



         AssemblyProcessor processor = (AssemblyProcessor)

            appDomain.CreateInstanceFromAndUnwrap(

            "AppDomainAssemblyLoader.dll",

            "AppDomainAssemblyLoader.AssemblyProcessor");



         string name =

            fileInfo.Name.Replace(fileInfo.Extension, "");   

         Console.WriteLine(name);

         processor.Load(name);

         processor.Process();

      }

      finally

      {

         if (appDomain != null)

         {

            AppDomain.Unload(appDomain);

            appDomain = null;

         }

      }

   }

}



AppDomain 的安装是一个非常关键的部分。AssemblyProcessor 类使用 Assembly.Load() 定位程序集,它在 AppDomain 的应用程序库中寻找程序集。因此,我们需要将应用程序库设置为程序集所在的目录。这可以通过以下命令行来完成:



setup.ApplicationBase = fileInfo.DirectoryName;



注意:AppDomain 可以通过与 AssemblyResolve 事件相关联来实现自身的加载规则。

但该代码不能用于加载 AssemblyProcessor 类,因为它与当前的 AppDomain 位于同一目录。因此,我们可以将新 AppDomain 的专用二进制路径设置为当前应用程序域的目录。



setup.PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory;



只需再配置一项内容。最后,我们希望用户能够重写我们的程序集,不过,所有程序集在加载时都已被锁定。解决办法是在其他目录中存放一个程序集副本,这样,原始程序集就不会被锁定了。AppDomain 可以自动完成这项工作:



setup.ShadowCopyFiles = "true";



在定义了 AppDomain 的安装方式之后,我们就可以创建 AppDomain,然后在新的 AppDomain 中创建一个 AssemblyProcessor 实例:



AssemblyProcessor processor = (AssemblyProcessor)

      appDomain.CreateInstanceFromAndUnwrap(

         "AppDomainAssemblyLoader.dll",

         "AppDomainAssemblyLoader.AssemblyProcessor");



我们调用 CreateInstanceFromAndUnwrap(),传递 CreateInstanceFromAndUnwrap() 要使用的程序集的文件名,以及我们希望它创建的类型的完全限定的名称。



完成上述操作后,我们将在新创建的 AppDomain 中得到一个针对真正的 AssemblyProcessor 对象的 AssemblyProcessor 代理。我们可以将它用作本地对象:



string name = fileInfo.Name.Replace(fileInfo.Extension, "");   

Console.WriteLine(name);

processor.Load(name);

processor.Process();



由于 Assembly.Load() 需要程序集名称但不会处理文件名,所以必须除去扩展名。



处理结束后,AppDomain 将被卸载,这将清除所有内容。我还实现了一个析构函数和 IDisposable 接口(尽管这个简单示例并不需要它们)。

原文:

這個月,我參加 ASP.NET 討論會後,坐在棕櫚泉國際機場的候機室裡,準備搭機返回西雅圖。

本來我這個月的計劃 — 我還是計劃的 — 是要研究上個月 SuperGraph 應用程式的運算式剖析。但是,過去幾個星期我收到許多電子郵件,詢問我打算什麼時候完成 AppDomain 部分的組件載入和卸載,所以我決定先專心討論這個。

應用程式架構

討論程式碼之前,我想要先談談我的計劃。您可能還記得,SuperGraph 可讓您選擇函式。我希望能將增益功能組件放在特定目錄中,讓 SuperGraph 偵測、載入這些組件,並尋找組件中的函式。

這麼做本身並不需要一個單獨的 AppDomain Assembly.Load() 通常就很好用了。但不幸的是,只有 AppDomain 才能卸載,而組件並不能分別卸載。這表示,如果是撰寫伺服器,且又想讓使用者能在不必啟動或停止伺服器的情況下,就可更新其增益功能,則必須使用預設的 AppDomain。

最好的方法是,將所有增益功能組件下載到一個單獨的 AppDomain 中,加入或修改檔案後,再卸載該 AppDomain,建立新的 AppDomain,然後將目前的檔案載入其中,這樣就大功告成了。

為了更清楚得講解,我建立了 [圖 1] 的典型案例。

[圖 1] 典型 AppDomain 案例

圖中的 Loader 類別建立名為 Functions 的新 AppDomain。建立好 AppDomain 之後,Loader 會在新 AppDomain 內部建立 RemoteLoader 的執行個體。

要想載入組件,請在 RemoteLoader 上呼叫載入函式。它會開啟新組件,尋找組件中的所有函式,將它們封裝為 FunctionList 物件,然後將該物件傳回 Loader。然後,就可從 Graph 函式使用這個 FunctionList 裡的 Function 物件。

建立 AppDomain

第一項工作是建立 AppDomain。若要正確地建立 AppDomain,必須先傳遞一個 AppDomainSetup 物件給它。這方面的文件只有助於已瞭解這所有原理的讀者,但您的目的若是要瞭解運作方式,則可能沒多大的幫助。在 Google 裡搜尋這個主題時,上個月的專欄文章赫然排列在搜尋結果的前幾名,我想可能有些讀者可能會被誤導。

必須處理的基本問題是,如何在 Runtime 時載入組件。根據預設,Runtime 會在全域組件快取或目前的應用程式樹狀目錄中尋找。我們希望從完全不同的目錄載入我們的增益功能應用程式。

您查看 AppDomainSetup 的文件時,會發現可以將 ApplicationBase 屬性設定為該目錄來搜尋組件。可是,我們也需要參考原始程式目錄,因為 RemoteLoader 類別就存放在這個地方。

AppDomain 撰寫者瞭解這一點,因此提供另外一個可以搜尋組件的位置。我們會使用 ApplicationBase 來參考增益功能目錄,然後設定 PrivateBinPath 指向主要應用程式目錄。

以下是執行此作業之 Loader 類別的程式碼:

AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = functionDirectory;
setup.PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory;
setup.ApplicationName = "Graph";
appDomain = AppDomain.CreateDomain("Functions", null, setup);

remoteLoader = (RemoteLoader)  
    appDomain.CreateInstanceFromAndUnwrap("SuperGraph.exe", 
        "SuperGraphInterface.RemoteLoader");

建立 AppDomain 之後,使用 CreateInstanceFromAndUnwrap() 函式,在新的應用程式領域中建立 RemoteLoader 類別的執行個體。請注意,必須要有類別所在之組件的檔案名稱,以及完整的類別名稱。

執行這個呼叫時,會得到一個看起來就像 RemoteLoader 的執行個體。這實際上是一個小型 Proxy 類別,會將任何呼叫轉遞至其他 AppDomain 中的 RemoteLoader 執行個體。這個基礎結構和 .NET Remoting 使用的相同。

組件繫結記錄檔檢視器

您撰寫執行此作業的程式碼時,很可能發生錯誤。文件提供的偵錯應用程式的資訊不多,不過您若有人可以求教,他們會告訴您參考組件繫結記錄檔檢視器 (名為 fuslogvw.exe,因為載入子系統稱為 fusion) 的訊息。您執行檢視器時,可以要求檢視器記錄失敗,之後,若執行應用程式時,載入組件發生了問題,就可以重新整理檢視器,取得事件的詳細資料。

例如,發現 Assembly.Load() 的檔案名稱後面不需要 .dll,就很有幫助。您可以從記錄檔中得知此訊息,因為它會告訴您它曾嘗試載入 f.dll.dll

動態載入組件

即然我們已建立好應用程式領域,接著就該想出如何載入組件,並解壓縮其中的函式。這需要兩個不同區域的程式碼。第一個會尋找目錄中的檔案,然後載入每一個檔案:

void LoadUserAssemblies()
{
    availableFunctions = new FunctionList();
    LoadBuiltInFunctions();

    DirectoryInfo d = new DirectoryInfo(functionAssemblyDirectory);
    foreach (FileInfo file in d.GetFiles("*.dll"))
    {   
        string filename = file.Name.Replace(file.Extension, "");
        FunctionList functionList = loader.LoadAssembly(filename);

        availableFunctions.Merge(functionList);
    }
}

Graph 類別中的這個函式會尋找增益功能目錄中所有的 dll 檔案,移除檔案的副檔名,然後通知載入器載入檔案。傳回的函式清單會合併到目前的函式清單中。

第二段程式碼在 RemoteLoader 類別中,實際載入組件及尋找函式:

public FunctionList LoadAssembly(string filename)
{
    FunctionList functionList = new FunctionList();
    Assembly assembly = AppDomain.CurrentDomain.Load(filename);

    foreach (Type t in assembly.GetTypes())
    {
        functionList.AddAllFromType(t);
    }    
    return functionList;
}

這段程式碼只會在所傳入的檔案名稱 (其實是組件名稱) 上呼叫 Assembly.Load() ,然後將所有有用的函式載入 FunctionList 執行個體,傳回給呼叫者。

這時,應用程式可以啟動,載入增益功能組件,而使用者可以參考這些組件。

重新載入組件

下一個工作是要能視需要重新載入這些組件。我們希望這最終能夠自動執行,不過為了測試,我在表單加入了會重新載入組件的 [Reload] 按鈕。這個按鈕的處理常式只是呼叫 Graph.Reload(),它必須執行下列動作:

  1. 卸載 AppDomain。
  2. 建立新的 AppDomain。
  3. 在新的 AppDomain 裡重新載入組件。
  4. 連接圖形線至新建立的 AppDomain。

之所以需要步驟 4,是因為 GraphLine 物件包含來自舊 AppDomain 的 Function 物件。將該 AppDomain 卸載後,就不能再使用函式物件。

為了修正此一情形,HookupFunctions() 修改了 GraphLine 物件,使其從目前的應用程式領域指向正確的函式。

下列為其程式碼:

loader.Unload();
loader = new Loader(functionAssemblyDirectory);
LoadUserAssemblies();
HookupFunctions();
reloadCount++;

if (this.ReloadCountChanged != null)
    ReloadCountChanged(this, new ReloadEventArgs(reloadCount));

只要執行重新載入作業,最後兩行就會引發事件。這用於更新表單上的重新載入計數器。

偵測新組件

下一步是要能夠偵測在增益功能目錄中出現的新或修改過的組件。架構提供 FileSystemWatcher 類別來執行此工作。以下是我新增至 Graph 類別建構函式的程式碼:

watcher = new FileSystemWatcher(functionAssemblyDirectory, "*.dll");
watcher.EnableRaisingEvents = true;
watcher.Changed += new FileSystemEventHandler(FunctionFileChanged);
watcher.Created += new FileSystemEventHandler(FunctionFileChanged);
watcher.Deleted += new FileSystemEventHandler(FunctionFileChanged);

建立 FileSystemWatcher 類別後,我們告訴它應檢查的目錄和應追蹤的檔案。EnableRaisingEvents 屬性表示我們是否希望在它偵測到變更時傳送事件,最後 3 行將事件連接至我們類別中的函式。函式只是呼叫 Reload() 來重新載入組件。

這種方法有點沒效率。組件更新後,我們必須重新載入組件,才能載入新版本,但是在新增或刪除檔案時並無此必要。在此例中,為全部變更執行此作業的成本不算太高,而且可使程式碼較為簡單。

建置此程式碼後,我們執行應用程式,然後嘗試複製新的組件至新的增益功能目錄。可想而知,我們有個檔案變更事件,而且現在完成重新載入後,就有可用的新函式。

可是,我們嘗試更新現有組件時,又遇到了問題。Runtime 時鎖定了檔案,這表示我們不能將新組件複製到增益功能目錄中,所以發生錯誤。

AppDomain 的設計者知道這是個問題,因此提供很好的處理方法。ShadowCopyFiles 屬性設定為 True 時 (字串 true,而非布林常數 True。別問我為什麼...),Runtime 會複製組件至快取目錄,再開啟該組件。這會取消原始檔案的鎖定,讓我們得以更新使用中的組件。ASP.NET 使用了這種機能制。

為了啟用這項功能,我在 Loader 類別的建構函式新增下面這一行:

setup.ShadowCopyFiles = "true";

然後重建應用程式,結果還是發生相同錯誤。我查閱 ShadowCopyDirectories 屬性的文件,其中明確指出,如果未設定這個屬性,則所有 PrivateBinPath 所指定的目錄 (包括 ApplicationBase 指定的目錄) 都會遮蔽複製。記得我說過,文件在這一部分不是很周全嗎?

這個屬性的文件完全錯誤。我尚未確認確實的行為,不過可以告訴您,根據預設,ApplicationBase 目錄中的檔案不是遮蔽複製。明確指定目錄可以解決這個問題:

setup.ShadowCopyDirectories = functionDirectory;

要想出這一點至少花了我半小時。

現在我們可以更新現有的檔案,並且正確地載入。解決這個之後,我又遇到另一個小問題。我們從表單上的按鈕執行重新載入功能時,重新載入與繪圖總是發生在相同的執行緒,這表示我們在重新載入處理期間,根本沒有嘗試繪製線條。

即然我們已切換至檔案變更事件,就可能在卸載 AppDomain 之後,載入新 AppDomain 之前繪圖。若是如此,我們就會得到一個例外。

這是傳統的多執行緒程式設計問題,使用 C# lock 陳述式就能輕易處理。我在繪圖函式和重新載入函式中新增一個 lock 陳述式,這可確保兩者會同時發生。這解決了問題,而且新增組件的更新版本會讓程式自動切換至函式的新版本,真是一舉兩得。

不過,還是出現了怪異的行為。原來是,偵測檔案變更的 Win32® 函式傳送了很大的變更數目,所以執行一次檔案更新,竟然會傳送五個變更事件,導致組件重新載入五次。解決之道是製作一個聰明點的 FileSystemWatcher,將這些分組在一起,不過這一版尚未提供這個解決方法。

拖放

由於將檔案複製到目錄,不是那麼方便了,所以我決定在應用程式新增拖放功能。第一步是將表單的 AllowDrop 屬性設定為 True,開啟拖放支援。接著連接一個常式至 DragEnter 事件。拖放作業中游標移入物件時會呼叫這個事件,判斷目前的物件是否能夠拖放。

private void Form1_DragEnter(
    object sender, System.Windows.Forms.DragEventArgs e)
{
    object o = e.Data.GetData(DataFormats.FileDrop);
    if (o != null)
    {
        e.Effect = DragDropEffects.Copy;
    }
    string[] formats = e.Data.GetFormats();
}

我在這個處理常式中檢查是否有可用的 FileDrop 資料 (也就是檔案被拖曳至視窗中)。如果為真,我把效果設定為複製,這會適當地設定游標,並在使用者放開滑鼠按鍵後傳送 DragDrop 事件。函式的最後一行純綷是用於偵錯,檢查作業中有何可用的資訊。

下一件工作是撰寫 DragDrop 事件的處理常式:

private void Form1_DragDrop(
    object sender, System.Windows.Forms.DragEventArgs e)
{
    string[] filenames = (string[]) e.Data.GetData(DataFormats.FileDrop);
    graph.CopyFiles(filenames);
}

這個常式會取得與此作業關聯的資料 — 一批檔案名稱 — 傳送至圖形函式,函式再將檔案複製到增益功能目錄,促使檔案變更事件重新載入檔案。

狀態

現在您可以執行應用程式,拖放新組件到應用程式,應用程式會即時載入組件並繼續執行。不錯吧。


posted @ 2009-10-14 17:46  LutzMark  阅读(1011)  评论(0编辑  收藏  举报