从字节数组加载时找不到 AppDomain 程序集

Posted

技术标签:

【中文标题】从字节数组加载时找不到 AppDomain 程序集【英文标题】:AppDomain Assembly not found when loaded from byte array 【发布时间】:2018-05-02 05:58:19 【问题描述】:

请耐心等待,我花了 30 多个小时试图完成这项工作 - 但没有成功。

在我的程序开始时,我在 bytearray 中加载了一个程序集 (dll),然后将其删除。

_myBytes = File.ReadAllBytes(@"D:\Projects\AppDomainTest\plugin.dll");

稍后在程序中我创建一个新的 Appdomain,加载字节数组并枚举类型。

var domain = AppDomain.CreateDomain("plugintest", null, null, null, false);

domain.Load(_myBytes);

foreach (var ass in domain.GetAssemblies())

    Console.WriteLine($"ass.FullName: ass.FullName");
    Console.WriteLine(string.Join(Environment.NewLine, ass.GetTypes().ToList()));

类型被正确列出:

ass.FullName: plugin, Version=1.0.0.0, Culture=neutral,PublicKeyToken=null

...

插件测试

...

现在我想在新的 AppDomain 中创建该类型的实例

domain.CreateInstance("plugin", "Plugins.Test");

这个调用结果是System.IO.FileNotFoundException,我不知道为什么。

当我查看 .NET Assemblies -> Appdomain: plugintest 下的 ProcessExplorer 时,我看到程序集已正确加载到新的 appdomain 中。

我怀疑发生异常是因为再次在磁盘上搜索程序集。但是程序为什么要重新加载呢?

如何使用从字节数组加载的程序集在新的 appdomain 中创建实例?

【问题讨论】:

您是否尝试过挂钩 AppDomain 的 AssemblyResolve 事件 msdn.microsoft.com/fr-fr/library/… 并“手动”返回它所要求的内容? @SimonMourier 是的,我已经尝试过这里描述的方法:***.com/a/19702548/9279154。仍然抛出 FileNotFound 异常。 您是否对每个对 AssemblyResolve 的调用都回答非空程序集?有时,您需要同时挂钩起始 appdomain(创建新 AppDomain 的那个)和新 AppDomain @SimonMourier 是的,当我尝试这种方法时,我在两个 Appdomains 中都返回了 Assembly.Load(_myBytes)。 【参考方案1】:

这里的主要问题是认为您可以在主应用程序域中执行代码时实例化插件。

您需要做的是创建一个代理类型,该类型在已加载的程序集中定义,但在 new 应用程序域中实例化。您不能跨应用程序域边界传递类型,除非在两个应用程序域中都加载了类型的程序集。 例如,如果您想像上面那样枚举类型并打印到控制台,您应该从在新应用程序域中执行的代码中执行此操作,而不是从当前应用程序域中执行的代码中执行此操作强>。

所以,让我们创建我们的插件代理,它将存在于您的主程序集中,并将负责执行所有与插件相关的代码:

// Mark as MarshalByRefObject allows method calls to be proxied across app-domain boundaries
public class PluginRunner : MarshalByRefObject

    // make sure that we're loading the assembly into the correct app domain.
    public void LoadAssembly(byte[] byteArr)
    
        Assembly.Load(byteArr);
    

    // be careful here, only types from currently loaded assemblies can be passed as parameters / return value.
    // also, all parameters / return values from this object must be marked [Serializable]
    public string CreateAndExecutePluginResult(string assemblyQualifiedTypeName)
    
        var domain = AppDomain.CurrentDomain;

        // we use this overload of GetType which allows us to pass in a custom AssemblyResolve function
        // this allows us to get a Type reference without searching the disk for an assembly.
        var pluginType = Type.GetType(
            assemblyQualifiedTypeName,
            (name) => domain.GetAssemblies().Where(a => a.FullName == name.FullName).FirstOrDefault(),
            null,
            true);

        dynamic plugin = Activator.CreateInstance(pluginType);

        // do whatever you want here with the instantiated plugin
        string result = plugin.RunTest();

        // remember, you can only return types which are already loaded in the primary app domain and can be serialized.
        return result;
    

上面cmet中的几个关键点我在这里重申一下:

您必须从MarshalByRefObject 继承,这意味着可以使用远程处理跨应用程序域边界代理对该对象的调用。 向或从代理类传递数据时,数据必须标记为[Serializable],并且必须是当前加载程序集中的类型。如果你需要你的插件返回一些特定的对象给你,比如PluginResultModel,那么你应该在一个由两个程序集/应用程序域加载的共享程序集中定义这个类。 必须在当前状态下将程序集限定类型名称传递给 CreateAndExecutePluginResult,但可以通过自己迭代程序集和类型并删除对 Type.GetType 的调用来删除此要求。

接下来,您需要创建域并运行代理:

static void Main(string[] args)

    var bytes = File.ReadAllBytes(@"...filepath...");
    var domain = AppDomain.CreateDomain("plugintest", null, null, null, false);
    var proxy = (PluginRunner)domain.CreateInstanceAndUnwrap(typeof(PluginRunner).Assembly.FullName, typeof(PluginRunner).FullName);
    proxy.LoadAssembly(bytes);
    proxy.CreateAndExecutePluginResult("TestPlugin.Class1, TestPlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");

再说一遍,因为它超级重要,我很久没明白这个:当你在这个代理类上执行一个方法时,比如proxy.LoadAssembly,这实际上是被序列化成一个字符串并且被传递到要执行的新应用程序域。这不是一个普通的函数调用,您需要非常小心传递给/从这些方法传递的内容。

【讨论】:

有趣的帖子,虽然我会稍微改写第一段(参考我的回答)。【参考方案2】:

此调用导致 System.IO.FileNotFoundException,我不知道为什么。我怀疑会发生异常,因为在磁盘上再次搜索程序集。但是程序为什么要重新加载呢?

这里的关键是了解加载器上下文,MSDN 上有an excellent article:

将加载程序上下文视为应用程序域中包含程序集的逻辑存储桶。根据程序集的加载方式,它们属于三个加载器上下文之一。

加载上下文 Lo​​adFrom 上下文 没有上下文

byte[] 加载会将程序集置于“两者”上下文中。

对于两个上下文,这个上下文中的程序集不能绑定,除非应用程序订阅了 AssemblyResolve 事件。通常应该避免这种情况。

在下面的代码中,我们使用 AssemblyResolve 事件在 Load 上下文中加载程序集,使我们能够绑定到它。

如何使用从字节数组加载的程序集在新的 appdomain 中创建实例?

请注意,这只是一个概念证明,探索加载程序上下文的具体细节。建议的方法是使用@caesay 所述的代理,并由 Suzanne Cook 在this article 中进一步评论。

这是一个不保留对实例的引用的实现(类似于即发即弃)。

首先,我们的插件:

Test.cs

namespace Plugins

    public class Test
    
        public Test()
        
            Console.WriteLine($"Hello from AppDomain.CurrentDomain.FriendlyName.");
        
    

接下来,在一个新的ConsoleApp 中,我们的插件加载器:

PluginLoader.cs

[Serializable]
class PluginLoader

    private readonly byte[] _myBytes;
    private readonly AppDomain _newDomain;

    public PluginLoader(byte[] rawAssembly)
    
        _myBytes = rawAssembly;
        _newDomain = AppDomain.CreateDomain("New Domain");
        _newDomain.AssemblyResolve += new ResolveEventHandler(MyResolver);
    

    public void Test()
    
        _newDomain.CreateInstance("plugin", "Plugins.Test");
    

    private Assembly MyResolver(object sender, ResolveEventArgs args)
    
        AppDomain domain = (AppDomain)sender;
        Assembly asm = domain.Load(_myBytes);
        return asm;
    

Program.cs

class Program

    static void Main(string[] args)
    
        byte[] rawAssembly = File.ReadAllBytes(@"D:\Projects\AppDomainTest\plugin.dll");
        PluginLoader plugin = new PluginLoader(rawAssembly);

        // Output: 
        // Hello from New Domain
        plugin.Test();

        // Output: 
        // Assembly: mscorlib
        // Assembly: ConsoleApp
        foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
        
            Console.WriteLine($"Assembly: asm.GetName().Name");
        

        Console.ReadKey();
    

输出显示 CreateInstance("plugin", "Plugins.Test") 已成功从默认应用程序域调用,尽管它不知道插件程序集。

【讨论】:

有趣的答案,感谢您纠正我的错误。需要注意的是,上面的所有代码都在主应用程序域中运行,这只能按照描述的方式工作,因为您实际上已经通过调用CreateInstance 创建了一个ObjectHandle。您实际上不能在实例化类型上运行任何方法,因为它不是从MarshalByRefObject 继承的。 (也就是说,如果你尝试解开它,你会得到一个错误,说你的插件不可序列化) @caesay 没错,除此之外,Assembly.Load() 确实应该从相关的AppDomain 中调用(与代理一样)。【参考方案3】:

您是否尝试过提供程序集的全名,在您的情况下

domain.CreateInstance("plugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "Plugins.Test");

【讨论】:

刚试过。仍然抛出 FileNotFoundException。 这应该是一条评论

以上是关于从字节数组加载时找不到 AppDomain 程序集的主要内容,如果未能解决你的问题,请参考以下文章

尝试通过反射加载应用程序并获取错误“无法加载文件或程序集......系统找不到指定的文件。”

从 App.Config 将程序集加载到 AppDomain

在不知道类型的情况下将程序集从文件加载到自定义 AppDomain

如何在新的 AppDomain 中运行从加载到引用的程序集的方法

如何将带有 dll 的应用程序从内存加载到 AppDomain 中并执行它?

AppDomain 加载错误