首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 動態載入組件至獨立 AppDomain 實現熱插拔與卸載

     編輯:關於C#

在作業系統層面,進程提供了應用程式的隔離邊界,每個進程擁有獨立的虛擬記憶體空間,彼此無法直接存取。然而進程切換開銷大、無法共享記憶體,限制了高效通訊。.NET 引入 AppDomain(應用程式域) 作為輕量級隔離單元,所有 Managed Code 皆運行於某個 AppDomain 內,單一進程可容納多個相互隔離的 AppDomain。

核心難題:預設 AppDomain 無法卸載組件

當作業排程器需定時執行外部 .NET 組件(如 DLL)時,若直接在預設 AppDomain 載入,組件即被鎖定,無法替換或刪除,除非關閉宿主服務——這在生產環境顯然不可接受。因為預設 AppDomain 隨進程生命週期始終存在,無法單獨卸載。

解決方案:建立臨時 AppDomain 執行並卸載

核心思路:動態建立新 AppDomain → 載入組件執行 → 任務完成後卸載該 AppDomain,釋放檔案鎖定。關鍵在於跨域呼叫需透過 MarshalByRefObject 代理物件。

1. 組件動態載入器 AssemblyDynamicLoader

負責建立獨立 AppDomain、設定 Shadow Copy 避免鎖定原始檔案,並產生遠端執行代理 RemoteLoader。

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
using Ark.Log;

public class AssemblyDynamicLoader
{
    private static readonly ILog log = LogManager.GetLogger(typeof(AssemblyDynamicLoader));
    private AppDomain appDomain;
    private RemoteLoader remoteLoader;

    public AssemblyDynamicLoader()
    {
        var setup = new AppDomainSetup
        {
            ApplicationName = "ApplicationLoader",
            ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
            PrivateBinPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "private"),
            CachePath = AppDomain.CurrentDomain.BaseDirectory,
            ShadowCopyFiles = "true",
            ShadowCopyDirectories = AppDomain.CurrentDomain.BaseDirectory
        };

        this.appDomain = AppDomain.CreateDomain("ApplicationLoaderDomain", null, setup);
        string assemblyName = Assembly.GetExecutingAssembly().FullName;
        this.remoteLoader = (RemoteLoader)this.appDomain.CreateInstanceAndUnwrap(assemblyName, typeof(RemoteLoader).FullName);
    }

    public string InvokeMethod(string fullName, string className, string argsInput, string programName)
    {
        this.remoteLoader.InvokeMethod(fullName, className, argsInput, programName);
        return this.remoteLoader.Output;
    }

    public void Unload()
    {
        try
        {
            AppDomain.Unload(this.appDomain);
            this.appDomain = null;
        }
        catch (CannotUnloadAppDomainException ex)
        {
            log.Error("卸載組件失敗!", ex);
        }
    }
}

2. 跨域代理執行器 RemoteLoader

繼承 MarshalByRefObject,使實例可跨 AppDomain 邊界以 Proxy 方式傳遞。在目標域內載入組件、反射呼叫指定方法(支援靜態/實例、有無參數、有無回傳值)。

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;

public class RemoteLoader : MarshalByRefObject
{
    private Assembly assembly;
    private string output = string.Empty;

    public string Output => this.output;

    public void InvokeMethod(string fullName, string className, string argsInput, string programName)
    {
        this.assembly = null;
        this.output = string.Empty;

        try
        {
            this.assembly = Assembly.LoadFrom(fullName);
            Type pgmType = this.assembly?.GetType(className, true, true) ?? Type.GetType(className, true, true);
            object[] args = RunJob.GetArgs(argsInput);

            var bindingFlags = BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic
                             | BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.InvokeMethod | BindingFlags.Static;
            var culture = new CultureInfo("es-ES", false);

            try
            {
                MethodInfo method = RunJob.GetItsMethodInfo(pgmType, bindingFlags, programName);
                if (method == null) { this.output = "EMethod does not exist!"; return; }

                bool isStatic = method.IsStatic;
                int paramCount = method.GetParameters().Length;
                bool hasReturn = method.ReturnType != typeof(void);

                if (isStatic)
                {
                    if (paramCount == 0)
                    {
                        object result = pgmType.InvokeMember(programName, bindingFlags, null, null, null, culture);
                        this.output = hasReturn ? (string)result : "STo call a method without return value successful.";
                    }
                    else
                    {
                        object result = pgmType.InvokeMember(programName, bindingFlags, null, null, args, culture);
                        this.output = hasReturn ? (string)result : "STo call a method without return value successful.";
                    }
                }
                else
                {
                    object instance = Activator.CreateInstance(pgmType);
                    if (paramCount == 0)
                    {
                        object result = pgmType.InvokeMember(programName, bindingFlags, null, instance, null, culture);
                        this.output = hasReturn ? (string)result : "STo call a method without return value successful.";
                    }
                    else
                    {
                        object result = pgmType.InvokeMember(programName, bindingFlags, null, instance, args, culture);
                        this.output = hasReturn ? (string)result : "STo call a method without return value successful.";
                    }
                }
            }
            catch
            {
                this.output = (string)pgmType.InvokeMember(programName, bindingFlags, null, null, null, culture);
            }
        }
        catch (Exception e)
        {
            this.output = "E" + e.Message;
        }
    }
}

3. 使用範例

var loader = new AssemblyDynamicLoader();
string result = loader.InvokeMethod("MyPlugin.dll", "MyNamespace.MyClass", "arg1,arg2", "Execute");
loader.Unload(); // 釋放檔案鎖定,可隨時替換 DLL

關鍵優勢:
✅ 組件隔離,不污染主域
✅ 支援 Shadow Copy,原始檔案可隨時覆寫
✅ 執行完畢即時卸載,實現真正的熱插拔

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved