.NET插件技术-应用程序热升级

今天说一说.NET 中的插件技术,即 应用程序热升级。在不少状况下、咱们但愿用户对应用程序的升级是无感知的,而且尽量不打断用户操做的。git

虽然在Web 或者 WebAPI上,因为多点的存在能够逐个停用单点进行系统升级,而不影响整个服务。可是 客户端却不能这样作,毕竟用户一直在使用着。github

那么有没有一种方式,能够在用户无感知的状况下(即、不中止进程的状况下)对客户端进行升级呢?安全

答案是确定的, 这就是我今天想说的插件技术、能够对应用程序进行热升级。固然这种方式也一样适用于 ASP.NET ,app

不过当前随笔是以 WPF为例子的,而且原理是同样的、代码逻辑也是同样的。dom

 

1、应用程序域AppDomain

在介绍插件技术以前、咱们须要先了解一些基础性的知识,第一个就是应用程序域AppDomain.ide

操做系统和运行时环境一般会在应用程序间提供某种形式的隔离。 例如,Windows 使用进程来隔离应用程序。 为确保在一个应用程序中运行的代码不会对其余不相关的应用程序产生不良影响,这种隔离是必需的。这种隔离能够为应用程序域提供安全性、可靠性, 而且为卸载程序集提供了可能。测试

 

在 .NET中应用程序域AppDomain是CLR的运行单元,它能够加载应用程序集Assembly、建立对象以及执行程序。ui

在 CLR 里、AppDomain就是用来实现代码隔离的,每个AppDomain能够单首创建、运行、卸载。this

 

关于AppDomain中的未处理异常spa

若是默认AppDomain监听了 UnhandledException 事件,任何线程的任何未处理异常都会引起该事件,不管线程是从哪一个AppDomain中开始的。

若是一个线程开始于一个已经监听了 UnhandledException事件的 app domain, 那么该事件将在这个app domain 中引起。

若是这个app domian 不是默认的app domain, 而且 默认 app domain 中也监听了 UnhandledException 事件, 那么 该事件将会在两个app domain 中引起。

 

CLR启用时,会建立一个默认的AppDomain,程序的入口点(Main方法)就是在这个默认的AppDomain中执行。

AppDomain是能够在运行时进行动态的建立和卸载的,正因如此,才为插件技术提供了基础(注:应用程序集和类型是不能卸载的,只能卸载整个AppDomain)。

 

AppDomain和其余概念之间的关系

一、AppDomain vs 进程Process

AppDomain被建立在Process中,一个Process内能够有多个AppDomain。一个AppDomain只能属于一个Process。

二、AppDomain vs 线程Thread

应该说二者之间没有关系,AppDomain出现的目的是隔离,隔离对象,而 Thread 是 Process中的一个实体、是程序执行流中的最小单元,保存有当前指令指针 和 寄存器集合,为线程(上下文)切换提供可能。若是说有关系的话,能够牵强的认为一个Thread可使用多个AppDomain中的对象,一个AppDomain中可使用多个Thread.

三、AppDomain vs 应用程序集Assembly

Assembly是.Net程序的基本部署单元,它能够为CLR提供元数据等。

Assembly不能单独执行,它必须被加载到AppDomain中,而后由AppDomain建立程序集中的类型 及 对象。

一个Assembly能够被多个AppDomain加载,一个AppDomain能够加载多个Assembly。

每一个AppDomain引用到某个类型的时候须要把相应的assembly在各自的AppDomain中初始化。所以,每一个AppDomain会单独保持一个类的静态变量。

四、AppDomain vs 对象object
任何对象只能属于一个AppDomain,AppDomain用来隔离对象。 同一应用程序域中的对象直接通讯、不一样应用程序域中的对象的通讯方式有两种:一种是跨应用程序域边界传输对象副本(经过序列化对对象进行隐式值封送完成),一种是使用代理交换消息。

 

2、建立 和 卸载AppDomain

前文已经说明了,咱们能够在运行时动态的建立和卸载AppDomain, 有这样的理论基础在、咱们就能够热升级应用程序了 。

那就让咱们来看一下如何建立和卸载AppDomain吧

建立:

                AppDomainSetup objSetup = new AppDomainSetup();
                objSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;

                this.domain = AppDomain.CreateDomain("RemoteAppDomain", null, objSetup);

建立AppDomain的逻辑很是简单:使用 AppDomain.CreateDomain 静态方法、传递了一个任意字符串 和 AppDomainSetup 对象。

 

卸载:

              AppDomain.Unload(this.domain);

卸载就更简单了一行代码搞定:AppDomain.Unload 静态方法,参数就一个 以前建立的AppDomain对象。

 

3、在新AppDomain中建立对象

上文已经说了建立AppDomain了,可是建立的新AppDomain倒是不包含任何对象的,只是一个空壳子。那么如何在新的AppDomain中建立对象呢?

this.remoteIPlugin = this.domain.CreateInstance("PluginDemo.NewDomain", "PluginDemo.NewDomain.Plugin").Unwrap() as IPlugin;

 使用刚建立的AppDomain对象的实例化方法: this.domain.CreateInstance,传递了两个字符串,分别为 assemblyName 和 typeName.

而且该方法的重载方法 和 类似功能的重载方法多达十几个。

  

4、影像复制程序集

 建立、卸载AppDomain都有、建立新对象也能够了,可是若是想完成热升级,还有一点小麻烦,那就是一个程序集被加载后会被锁定,这时候是没法对其进行修改的。

因此就须要打开 影像复制程序集 功能,这样在卸载AppDomain后,把须要升级的应用程序集进行升级替换,而后再建立新的AppDomain便可了。

打开 影像复制程序集 功能,须要在建立新的AppDomain时作两步简单的设定便可:

                AppDomainSetup objSetup = new AppDomainSetup();
                objSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;

          // 打开 影像复制程序集 功能 objSetup.ShadowCopyFiles
= "true"; // 虽然此方法已经被标记为过期方法, msdn备注也提倡不使用该方法, // 可是 以.net 4.0 + win10环境测试,还必须调用该方法 不然,即使卸载了应用程序域 dll 仍是未被解除锁定 AppDomain.CurrentDomain.SetShadowCopyFiles(); this.domain = AppDomain.CreateDomain("RemoteAppDomain", null, objSetup);

 

 5、简单的Demo

 现有一接口IPlugin:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Input;

namespace PluginDemo
{
    public interface IPlugin
    {
        int GetInt();

        string GetString();
        
        object GetNonMarshalByRefObject();

        Action GetAction();

        List<string> GetList();
    }
}
接口 IPlugin

 

在另外的一个程序集中有其一个实现类 Plugin:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PluginDemo;

namespace PluginDemo.NewDomain
{

    /// <summary>
    /// 支持跨应用程序域访问
    /// </summary>
    public class Plugin : MarshalByRefObject, IPlugin
    {
        // AppDomain被卸载后,静态成员的内存会被释放掉
        private static int length;

        /// <summary>
        /// int 做为基础数据类型, 是持续序列化的.
        /// <para>在与其余AppDomain通信时,传递的是对象副本(经过序列化进行的值封送)</para>
        /// </summary>
        /// <returns></returns>
        public int GetInt()
        {
            length += new Random().Next(10000);

            return length;
        }


        /// <summary>
        /// string 做为特殊的class, 也是持续序列化的.
        /// <para>在与其余AppDomain通信时,传递的是对象副本(经过序列化进行的值封送)</para>
        /// </summary>
        /// <returns></returns>
        public string GetString()
        {
            return "iqingyu";
        }



        /// <summary>
        /// 未继承 MarshalByRefObject 而且 不支持序列化 的 class, 是不能够跨AppDomain通讯的,也就是说其余AppDomain是获取不到其对象的
        /// </summary>
        /// <returns></returns>
        public object GetNonMarshalByRefObject()
        {
            return new NonMarshalByRefObject();
        }

        private NonMarshalByRefObjectAction obj = new NonMarshalByRefObjectAction();

        /// <summary>
        /// 委托,和 委托所指向的类型相关
        /// <para>也就是说,若是其指向的类型支持跨AppDomain通讯,那个其余AppDomain就能够获取都该委托, 反之,则不能获取到</para>
        /// </summary>
        /// <returns></returns>
        public Action GetAction()
        {
            obj.Add();
            obj.Add();
            //obj.Add();

            return obj.TestAction;
        }

        private List<string> list = new List<string>() { "A", "B" };

        /// <summary>
        /// List<T> 也是持续序列化的, 固然前提是T也必须支持跨AppDomain通讯
        /// <para>在与其余AppDomain通信时,传递的是对象副本(经过序列化进行的值封送)</para>
        /// </summary>
        /// <returns></returns>
        public List<string> GetList()
        {
            return this.list;
            // return new List<Action>() { this.GetAction() };
        }

    }


}
实现类 Plugin

在另外的一个程序集中还有一个 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace PluginDemo.NewDomain
{
    /// <summary>
    /// 未继承 MarshalByRefObject,  不能够跨AppDomain交换消息
    /// </summary>
    public class NonMarshalByRefObject
    {

    }
}
空类型 NonMarshalByRefObject

 

测试程序以下:

using System;
using System.Windows;
using System.Diagnostics;
using System.Runtime.Serialization.Formatters.Binary;

namespace PluginDemo
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        private AppDomain domain;
        private IPlugin remoteIPlugin;



        public MainWindow()
        {
            InitializeComponent();
        }

        private void loadBtn_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                unLoadBtn_Click(sender, e);

                this.txtBlock.Text = string.Empty;

                // 在新的AppDomain中加载 RemoteCamera 类型
                AppDomainSetup objSetup = new AppDomainSetup();
                objSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
                objSetup.ShadowCopyFiles = "true";

                // 虽然此方法已经被标记为过期方法, msdn备注也提倡不使用该方法,
                // 可是 以.net 4.0 + win10环境测试,还必须调用该方法 不然,即使卸载了应用程序域 dll 仍是未被解除锁定
                AppDomain.CurrentDomain.SetShadowCopyFiles();

                this.domain = AppDomain.CreateDomain("RemoteAppDomain", null, objSetup);
                this.remoteIPlugin = this.domain.CreateInstance("PluginDemo.NewDomain", "PluginDemo.NewDomain.Plugin").Unwrap() as IPlugin;

                this.txtBlock.AppendText("建立AppDomain成功\r\n\r\n");
            }
            catch (Exception ex)
            {
                this.txtBlock.AppendText(ex.Message);
                this.txtBlock.AppendText("\r\n\r\n");
            }
        }

        private void unLoadBtn_Click(object sender, RoutedEventArgs e)
        {
            if (this.remoteIPlugin != null)
            {
                this.remoteIPlugin = null;
            }

            if (this.domain != null)
            {
                AppDomain.Unload(this.domain);
                this.domain = null;
                this.txtBlock.AppendText("卸载AppDomain成功\r\n\r\n");
            }
        }



        private void invokeBtn_Click(object sender, RoutedEventArgs e)
        {
            if (this.remoteIPlugin == null)
                return;

            this.txtBlock.AppendText($"GetInt():{ this.remoteIPlugin.GetInt().ToString()}\r\n");
            this.txtBlock.AppendText($"GetString():{ this.remoteIPlugin.GetString().ToString()}\r\n");


            try
            {
                this.remoteIPlugin.GetNonMarshalByRefObject();
            }
            catch (Exception ex)
            {
                this.txtBlock.AppendText($"GetNonMarshalByRefObject():{ ex.Message}\r\n");
                if (Debugger.IsAttached)
                {
                    Debugger.Break();
                }
            }          
        }
    }
}
测试程序

按测试程序代码执行,先Load AppDomain, 而后 Access Other Member, 此时会发现出现了异常,大体内容以下:

建立AppDomain成功

GetInt():1020
GetString():iqingyu
GetNonMarshalByRefObject():程序集“PluginDemo.NewDomain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null”中的类型“PluginDemo.NewDomain.NonMarshalByRefObject”未标记为可序列化。

 是因为 PluginDemo.NewDomain.NonMarshalByRefObject 这个类型未标记可序列化 而引起的。 那么这种状况下和序列化又有什么关系呢?

请继续往下看。 

 

 6、AppDomain间的对象通讯

 前文说过了,AppDomain 是用来隔离对象的,AppDomain 之间的对象是不能够随意通讯的,这一点在 MSND的备注 中有一段描述:

应用程序域是一个操做系统进程中一个或多个应用程序所驻留的分区。 同一应用程序域中的对象直接通讯。 不一样应用程序域中的对象的通讯方式有两种:一种是跨应用程序域边界传输对象副本,一种是使用代理交换消息。

MarshalByRefObject 是经过使用代理交换消息来跨应用程序域边界进行通讯的对象的基类。 不是从 MarshalByRefObject 继承的对象根据值隐式封送。 当远程应用程序引用根据值封送的对象时,将跨应用程序域边界传递该对象的副本。

MarshalByRefObject 对象在本地应用程序域的边界内可直接访问。 远程应用程序域中的应用程序首次访问 MarshalByRefObject 时,会向该远程应用程序传递代理。 对该代理后面的调用将封送回驻留在本地应用程序域中的对象。

当跨应用程序域边界使用类型时,类型必须是从 MarshalByRefObject 继承的,并且因为对象的成员在建立它们的应用程序域以外没法使用,因此不得复制对象的状态。

 也就是说AppDomain间的对象通讯有两种方式:一种是继承 MarshalByRefObject ,拥有使用代理交换消息的能力,另一种是利用序列化、传递对象副本。

第一种:表现形式上来讲,传递的是对象引用。 第二种 传递的是对象副本,也就是说不是同一个对象。

 

也正所以,因为 PluginDemo.NewDomain.NonMarshalByRefObject 即不是 MarshalByRefObject 的子类,也不能够进行序列化,故 不可在两个不一样的AppDomain间通讯。

而上面的异常,则是由序列化  PluginDemo.NewDomain.NonMarshalByRefObject 对象失败致使的异常。

 

若是一个类型 【不是】 MarshalByRefObject的子类 而且 【没有标记】 SerializableAttribute,
则该类型的对象不能被其余AppDomain中的对象所访问, 固然这种状况下的该类型对象中的成员也不可能被访问到了
反之,则能够被其余AppDomain中的对象所访问

若是一个类型 【是】 MarshalByRefObject的子类, 则跨AppDomain所获得的是 【对象的引用】(为了好理解说成对象引用,实质为代理)

若是一个类型 【标记】 SerializableAttribute, 则跨AppDomain所获得的是 【对象的副本】,该副本是经过序列化进行值封送的
此时传递到其余AppDomain 中的对象 和 当前对象已经不是同一个对象了(只传递了副本)

若是一个类型 【是】 MarshalByRefObject的子类 而且 【标记了】 SerializableAttribute,
则 MarshalByRefObject 的优先级更高

 

另外:.net 基本类型 、string 类型、 List<T> 等类型,虽然没有标记 SerializableAttribute, 可是他们依然能够序列化。也就是说这些类型均可以在不一样的AppDomain之间通讯,只是传递的都是对象副本。

 

7、完整的Demo

完整的Demo笔者已上传至Github,  https://github.com/iqingyu/BlogsDemo :

PluginDemo

PluginDemo.NewDomain

两个项目为完整的Demo

相关文章
相关标签/搜索