1. 程式人生 > >基於AppDomain的"插件式"開發

基於AppDomain的"插件式"開發

file 添加 ice rar ssl lib ria exec getname

很多時候,我們都想使用(開發)USB式(熱插拔)的應用,例如,開發一個WinForm應用,並且這個WinForm應用能允許開發人員定制擴展插件,又例如,我們可能維護著一個WinService管理系統,這個WinService系統管理的形形色色各種各樣的服務,這些服務也是各個"插件式"的類庫,例如:

public interface IJob
    {
        void Run(DateTime time);
    }

    public class CollectUserInfo : IJob
    {

        public void
Run(DateTime time) { //doing some thing... } }

我們提供了一個IJob接口,所有"服務"都繼承該接口,然後做相關的配置,在服務啟動時,就可以根據配置,反射加載程序集,執行我們預期的任務.

更新程序集(dll/exe)

服務/插件程序(

發生該錯誤的原因很簡單,因為我們的程序中已經調用了該dll,那麽在CLR加載該dll到文件流中也給其加了鎖,所以,當我們要進行覆蓋,修改,刪除的時候自然就無法操作該文件了.那我們該怎麽做?為什麽Asp.net可以直接覆蓋?

AppDomain登場

我們知道,AppDomain是.Net平臺裏一個很重要的特性,在.Net以前,每個程序是"封裝"在不同的進程中的,這樣導致的結果就造就占用資源大,可復用性低等缺點.而AppDomain在同一個進程內劃分出多個"域",一個進程可以運行多個應用,提高了資源的復用性,數據通信等.詳見應用程序域

CLR在啟動的時候會創建系統域(System Domain),共享域(Shared Domain)和默認域(Default Domain),系統域與共享域對於用戶是不可見的,默認域也可以說是當前域,它承載了當前應用程序的各類信息(堆棧),所以,我們的一切操作都是在這個默認域上進行."插件式"開發很大程度上就是依靠AppDomain來進行.

"熱插拔"實現說明

當加載了一個程序集之後,該程序集就會被加入到指定AppDomain中,按照原來的想法,要實現"熱插拔",只要在需要使用該"插件"的時候,加載該"插件"的程序集(dll),使用結束後,卸載掉該程序集便可達到我們預期的效果.加載程序集很簡單,.C#提供一個Assembly類,方便又快捷.



var  _assembly = Assembly.LoadFrom(assemblyFile);

Assembly提供了數個加載方法詳見Assembly類.

然後,C#卻沒有提供卸載程序集的方法,唯一能卸載程序集的方法只有卸載該程序集所在的AppDomain,這樣,該AppDomain下的程序集都會被釋放.知道這一點,我們便可以利用AppDomain來達到我們預期的效果.

AppDomain實現"熱插拔"

首先,我們需要先實例化一個新AppDomain作為"插件"的宿主.在實例化一個Domain之前,先聲明該Domain的一些基本配置信息

          AppDomainSetup setup = new AppDomainSetup();
            setup.ApplicationName = "ApplicationLoader";    
            setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;  
            setup.PrivateBinPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "private");
            setup.CachePath = setup.ApplicationBase;
            setup.ShadowCopyFiles = "true"; //啟用影像復制程序集
            setup.ShadowCopyDirectories = setup.ApplicationBase;
            AppDomain.CurrentDomain.SetShadowCopyFiles();

setup.ShadowCopyFiles = "true";這句很重要,其作用就是啟用影像復制程序集,什麽是影像復制程序集,復制程序集是保證"熱插拔"

實現的主要工作.AppDomain加載程序集的時候,如果沒有ShadowCopyFiles,那就直接加載程序集,結果就是程序集被鎖定,相反,如果啟用了ShadowCopyFiles,則CLR會將準備加載的程序集拷貝一份至CachePath,再加載CachePath的這一份程序集,這樣原程序集也就不會被鎖定了. AppDomain.CurrentDomain.SetShadowCopyFiles();的作用就是當前AppDomain也啟用ShadowCopyFiles,在此,當前AppDomain也就是前面我們說過的那個默認域(Default Domain),為什麽當前域也要啟用ShadowCopyFiles呢?

主AppDomian在調用子AppDomain提供過來的類型,方法,屬性的時候,也會將該程序集添加到自身程序集引用當中去,所以,"插件"程序集就被主AppDomain鎖定,這也是為什麽創建了單獨的AppDomain程序集也不能刪除,替換(釋放)的根本原因

利用SOS,可以很清楚的看到這一點

0:018> !dumpdomain
--------------------------------------
System Domain:      5b912478
LowFrequencyHeap:   5b912784
HighFrequencyHeap:  5b9127d0
StubHeap:           5b91281c
Stage:              OPEN
Name:               None
--------------------------------------
Shared Domain:      5b912140
LowFrequencyHeap:   5b912784
HighFrequencyHeap:  5b9127d0
StubHeap:           5b91281c
Stage:              OPEN
Name:               None
Assembly:           00109de0 [C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll]
ClassLoader:        00110f68
  Module Name
58631000            C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll

--------------------------------------
Domain 1:           000f4598
LowFrequencyHeap:   000f4914
HighFrequencyHeap:  000f4960
StubHeap:           000f49ac
Stage:              OPEN
SecurityDescriptor: 000f5568
Name:               AppDomainTest.exe
Assembly:           00109de0 [C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll]
ClassLoader:        00110f68
SecurityDescriptor: 001097b0
  Module Name
58631000            C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll

Assembly:           0011d448 [E:\Test\AppDomainTest\AppDomainTest\bin\Debug\AppDomainTest.exe]
ClassLoader:        00117fd0
SecurityDescriptor: 0011d3c0
  Module Name
001c2e9c            E:\Test\AppDomainTest\AppDomainTest\bin\Debug\AppDomainTest.exe

Assembly:           00131370 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Windows.Forms\v4.0_4.0.0.0__b77a5c561934e089\System.Windows.Forms.dll]
ClassLoader:        0011fa00
SecurityDescriptor: 001299a0
  Module Name
579c1000            C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Windows.Forms\v4.0_4.0.0.0__b77a5c561934e089\System.Windows.Forms.dll

Assembly:           00131400 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Drawing\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Drawing.dll]
ClassLoader:        00131490
SecurityDescriptor: 0012e9c0
  Module Name
62661000            C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Drawing\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Drawing.dll

Assembly:           00131d20 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll]
ClassLoader:        00133d08
SecurityDescriptor: 0012f078
  Module Name
5aa81000            C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll

Assembly:           00131ed0 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Configuration\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Configuration.dll]
ClassLoader:        001415a8
SecurityDescriptor: 0012f430
  Module Name
5a981000            C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Configuration\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Configuration.dll

Assembly:           00132080 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Xml\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.dll]
ClassLoader:        00141620
SecurityDescriptor: 0012f5c8
  Module Name
546e1000            C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Xml\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.dll

Assembly:           00132ce0 [E:\Test\AppDomainTest\AppDomainTest\bin\Debug\CrossDomainController.dll]
ClassLoader:        001b3450
SecurityDescriptor: 06f94560
  Module Name
001c7428            E:\Test\AppDomainTest\AppDomainTest\bin\Debug\CrossDomainController.dll

Assembly:           00132350 [C:\Users\kong\AppData\Local\assembly\dl3\6ZYK3XE9.86Q\2AQ35O7C.VHE\1f704bbb\b7cca5cf_8c4fcc01\ShowHelloPlug.DLL]
ClassLoader:        001b32e8
SecurityDescriptor: 070a8620
  Module Name
001c7d78            C:\Users\kong\AppData\Local\assembly\dl3\6ZYK3XE9.86Q\2AQ35O7C.VHE\1f704bbb\b7cca5cf_8c4fcc01\ShowHelloPlug.DLL

--------------------------------------
Domain 2:           06fd0238
LowFrequencyHeap:   06fd05b4
HighFrequencyHeap:  06fd0600
StubHeap:           06fd064c
Stage:              OPEN
SecurityDescriptor: 06724510
Name:               ApplicationLoaderDomain
Assembly:           00109de0 [C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll]
ClassLoader:        00110f68
SecurityDescriptor: 06f93bd0
  Module Name
58631000            C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll

Assembly:           00132e90 [E:\Test\AppDomainTest\AppDomainTest\bin\Debug\ApplicationLoader\assembly\dl3\c91a2898\f6f7f865_9a4fcc01\CrossDomainController.DLL]
ClassLoader:        001b3540
SecurityDescriptor: 06f92be0
  Module Name
00a833c4            E:\Test\AppDomainTest\AppDomainTest\bin\Debug\ApplicationLoader\assembly\dl3\c91a2898\f6f7f865_9a4fcc01\CrossDomainController.DLL

Assembly:           001330d0 [E:\Test\AppDomainTest\AppDomainTest\bin\Debug\ApplicationLoader\assembly\dl3\32519346\b7cca5cf_8c4fcc01\ShowHelloPlug.DLL]
ClassLoader:        001b39f0
SecurityDescriptor: 06f92f98
  Module Name
00a83adc            E:\Test\AppDomainTest\AppDomainTest\bin\Debug\ApplicationLoader\assembly\dl3\32519346\b7cca5cf_8c4fcc01\ShowHelloPlug.DLL

除了新建的AppDomain(Domain2)中的Module引用了ShowHelloPlug.dll,默認域(Domian1)也有ShowHelloPlug.dll的

程序集引用.

應用程序域之間的通信

每個AppDomain都有自己的堆棧,內存塊,也就是說它們之間的數據並非共享了.若想共享數據,則涉及到應用程序域之間的通信.C#提供了MarshalByRefObject類進行跨域通信,那麽,我們必須提供自己的跨域訪問器.



public class RemoteLoader : MarshalByRefObject
    {
        private Assembly _assembly;

        public void LoadAssembly(string assemblyFile)
        {
            try
            {
               _assembly = Assembly.LoadFrom(assemblyFile);
                //return _assembly;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        public T GetInstance<T>(string typeName) where T : class
        {
            if (_assembly == null) return null;
            var type = _assembly.GetType(typeName);
            if (type == null) return null;
            return Activator.CreateInstance(type) as T;
        }

        public void ExecuteMothod(string typeName, string methodName)
        {
            if (_assembly == null) return;
            var type = _assembly.GetType(typeName);
            var obj = Activator.CreateInstance(type);
            Expression<Action> lambda = Expression.Lambda<Action>(Expression.Call(Expression.Constant(obj), type.GetMethod(methodName)), null);
            lambda.Compile()();
        }
    }

為了更好的操作這個跨域訪問器,接下來我構建了一個名為AssemblyDynamicLoader的類,它內部封裝了RemoteLoader類

的操作.

public class AssemblyDynamicLoader
    {
        private AppDomain appDomain;
        private RemoteLoader remoteLoader;
        public AssemblyDynamicLoader()
        {
            AppDomainSetup setup = new AppDomainSetup();
            setup.ApplicationName = "ApplicationLoader";    
            setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;  
            setup.PrivateBinPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "private");
            setup.CachePath = setup.ApplicationBase;
            setup.ShadowCopyFiles = "true"; 
            setup.ShadowCopyDirectories = setup.ApplicationBase;
            AppDomain.CurrentDomain.SetShadowCopyFiles();
            this.appDomain = AppDomain.CreateDomain("ApplicationLoaderDomain", null, setup);
            String name = Assembly.GetExecutingAssembly().GetName().FullName;
            this.remoteLoader = (RemoteLoader)this.appDomain.CreateInstanceAndUnwrap(name, typeof(RemoteLoader).FullName);
        }

        public void LoadAssembly(string assemblyFile)
        {
            remoteLoader.LoadAssembly(assemblyFile);
        }

        public T GetInstance<T>(string typeName) where T : class
        {
            if (remoteLoader == null) return null;
            return remoteLoader.GetInstance<T>(typeName);
        }

        public void ExecuteMothod(string typeName, string methodName)
        {
            remoteLoader.ExecuteMothod(typeName, methodName);
        }

        public void Unload()
        {
            try
            {
                if (appDomain == null) return;
                AppDomain.Unload(this.appDomain);
                this.appDomain = null;
            }
            catch (CannotUnloadAppDomainException ex)
            {
                throw ex;
            }
        }
    }

這樣我們每次都是通過AssemblyDynamicLoader類進行跨域的訪問.



     AppDomain.CurrentDomain.SetShadowCopyFiles();
            this.appDomain = AppDomain.CreateDomain("ApplicationLoaderDomain", null, setup);
            String name = Assembly.GetExecutingAssembly().GetName().FullName;
            this.remoteLoader = (RemoteLoader)this.appDomain.CreateInstanceAndUnwrap(name, typeof(RemoteLoader).FullName);

通過我們前面構造的一個AppDomainSetup,構建了一個我們所需的AppDomain,並且在這個appDomain中構建了

一個RemoteLoader類的實例(此時該實例已具備跨域訪問能力,也就是說我們在主域能獲取子域內部的數據信息).目前RemoteLoader只提供了少數的幾個方法.

跨域操作

下面,我們就模擬一次"插件式"的跨域操作.首先我們構造了一個窗體,其有以下元素.

技術分享

選擇程序集路徑之後,加載程序集,然後就觸發程序集指定類型(通過配置獲取)的特定操作.這裏我們定義了一個公共接口,它是所有"插件"操作的主要入口了.

 public interface IPlug
    {
        void Run();
    }

隨後定義了一個實現該接口的類.

  [Serializable]
    public class ShowHelloPlug : IPlug
    {
        public void Run()
        {
            MessageBox.Show("Hello World...");
        }
    }

這個"插件"的工作很簡單.僅僅彈出一個對話框,說聲"Hello World…",接下來將其編譯成一個dll.

技術分享

回到界面,選擇剛才編譯的Dll,然後直接加載.

技術分享

到這裏,我們的工作完成了一半了.呼呼.OK.我們的需求發生了變化,不再是彈出Hello World了.而時候彈出Hi,I‘m Kinsen,我們修改剛才的子類,並再編譯一次.再將Dll替換剛才的Dll,這次,Dll沒有沒鎖定(因為我們前面啟用了ShadowCopyFiles.).再加載一下程序集,你會發現結果並不是"Hi,I‘m Kinsen",而是"Hello World.."為什麽會這樣呢?這時候,借助SOS的力量(前面有SOS結果).

我們發現Domain1(Default Domain)和Domain2(新創建Domain)都引用了程序集ShowHelloPlug.DLL,但是兩個引用的Dll地址卻不相同,這是因為啟用了ShadowCopyFiles,它們加載的都是各自程序集的備份,我們根據Domain2的Assembly地址查看ShowHelloPlug的編譯代碼.



0:011> !dumpmt 00fc40ac     
00fc40ac is not a MethodTable
0:011> !dumpmd 00fc40ac     
Method Name:  Plug.ShowHelloPlug.Run()
Class:        046812b4
MethodTable:  00fc40bc
mdToken:      06000001
Module:       00fc3adc
IsJitted:     no
CodeAddr:     ffffffff
Transparency: Critical

從IsJitted為no可以看出,該程序集並沒有被調用,那調用的是誰?我們再次查看Domain1(Default Domain

)中的ShowHelloPlug.



0:011> !dumpmd 001f8240      
Method Name:  Plug.ShowHelloPlug.Run()
Class:        004446e4
MethodTable:  001f8250
mdToken:      06000001
Module:       001f7d78
IsJitted:     yes
CodeAddr:     00430de0
Transparency: Critical


已知每個AppDomain都有自己的堆棧信息,各自不互相影響,所以,當我們在主域中獲取到了子域中的數據,並非新建一個指向該實例的引用,而是在自己的堆棧上開辟出一塊空間"深度拷貝"該實例,那麽必然就達不到我們我需的結果.

子域內部調用

那麽為了達到我們預期的效果,我們必須在子域內部執行我們所需的操作(調用),所以在RemoteLoader類中增加了一個Execute方法

 public void ExecuteMothod(string typeName, string methodName)
        {
            if (_assembly == null) return;
            var type = _assembly.GetType(typeName);
            var obj = Activator.CreateInstance(type);
            Expression<Action> lambda = Expression.Lambda<Action>(Expression.Call(Expression.Constant(obj), type.GetMethod(methodName)), null);
            lambda.Compile()();
        }

此處我暫時只想到了利用反射調用,這樣的代價就是調用所需消耗的資源更多,效率低下.目前還沒有

想出較好的解決方案,有經驗的童鞋歡迎交流.

這樣外部的調用就變成以下



loader = new AssemblyDynamicLoader();
loader.LoadAssembly(txt_dllName.Text);
//var obj = loader.GetInstance<IPlug>("Plug.ShowHelloPlug");
//obj.Run();
loader.ExecuteMothod("Plug.ShowHelloPlug", "Run");

現在在將Dll替換,結果正常.

技術分享

尾聲

做"插件式"開發,除了利用AppDomain之外,也有童鞋給出了另一種解決方案,也就是在加載Dll的時候,先將Dll在內存中復制一份,這樣原來的Dll也就不會被鎖定了.詳見插件的“動態替換”.

以上實例本人皆做過實驗,但可能還存在一定不足或概念錯誤,若有不當之處,歡迎各位童鞋批評指點.

更多

通過應用程序域AppDomain加載和卸載程序集

什麽是的AppDomain

原文地址

http://www.cnblogs.com/kongyiyun/archive/2011/08/01/2123459.html

基於AppDomain的"插件式"開發