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

今天说一说.NET 中的插件技术,即 应用程序热升级。在很多情况下、我们希望用户对应用程序的升级是无感知的,并且尽可能不打断用户操作的。

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

那么有没有一种方式,可以在用户无感知的情况下(即、不停止进程的情况下)对客户端进行升级呢?

答案是肯定的, 这就是我今天想说的插件技术、可以对应用程序进行热升级。当然这种方式也同样适用于 ASP.NET ,

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

 

一、应用程序域AppDomain

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

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

 

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

在 CLR 里、AppDomain就是用来实现代码隔离的,每一个AppDomain可以单独创建、运行、卸载。

 

关于AppDomain中的未处理异常

如果默认AppDomain监听了 UnhandledException 事件,任何线程的任何未处理异常都会引发该事件,无论线程是从哪个AppDomain中开始的。

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

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

 

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

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

 

AppDomain和其他概念之间的关系

1、AppDomain vs 进程Process

AppDomain被创建在Process中,一个Process内可以有多个AppDomain。一个AppDomain只能属于一个Process。

2、AppDomain vs 线程Thread

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

3、AppDomain vs 应用程序集Assembly

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

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

一个Assembly可以被多个AppDomain加载,一个AppDomain可以加载多个Assembly。

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

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

 

二、创建 和 卸载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对象。

 

三、在新AppDomain中创建对象

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

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

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

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

  

四、影像复制程序集

 创建、卸载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);

 

 五、简单的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 这个类型未标记可序列化 而引发的。 那么这种情况下和序列化又有什么关系呢?

请继续往下看。 

 

 六、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之间通信,只是传递的都是对象副本。

 

七、完整的Demo

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

PluginDemo

PluginDemo.NewDomain

两个项目为完整的Demo

 

 

posted @ 2017-04-19 17:51 把爱延续 阅读(...) 评论(...) 编辑 收藏