.NET插件技术-应用程序热升级
今天说一说.NET 中的插件技术,即 应用程序热升级。在很多情况下、我们希望用户对应用程序的升级是无感知的,并且尽可能不打断用户操作的。
虽然在Web 或者 WebAPI上,由于多点的存在可以逐个停用单点进行系统升级,而不影响整个服务。但是 客户端却不能这样做,毕竟用户一直在使用着。
那么有没有一种方式,可以在用户无感知的情况下(即、不停止进程的情况下)对客户端进行升级呢?
答案是肯定的, 这就是我今天想说的插件技术、可以对应用程序进行热升级。当然这种方式也同样适用于 ASP.NET ,
不过当前随笔是以 WPF为例子的,并且原理是一样的、代码逻辑也是一样的。
一、应用程序域AppDomain
在介绍插件技术之前、我们需要先了解一些基础性的知识,第一个就是应用程序域AppDomain.
操作系统和运行时环境通常会在应用程序间提供某种形式的隔离。 例如,Windows 使用进程来隔离应用程序。 为确保在一个应用程序中运行的代码不会对其他不相关的应用程序产生不良影响,这种隔离是必需的。这种隔离可以为应用程序域提供安全性、可靠性, 并且为卸载程序集提供了可能。
在 .NET中应用程序域AppDomain是CLR的运行单元,它可以加载应用程序集Assembly、创建对象以及执行程序。
在 CLR 里、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(); 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 :
两个项目为完整的Demo
.NET插件技术-应用程序热升级的更多相关文章
- 使用Qt编写模块化插件式应用程序
动态链接库技术使软件工程师们兽血沸腾,它使得应用系统(程序)可以以二进制模块的形式灵活地组建起来.比起源码级别的模块化,二进制级别的模块划分使得各模块更加独立,各模块可以分别编译和链接,模块的升级不会 ...
- 【腾讯Bugly干货分享】Android 插件技术实战总结
本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/1p5Y0f5XdVXN2EZYT0AM_A 前言 安 ...
- 使用 SailingEase WinForm 框架构建复合式应用程序(插件式应用程序)
对于一些较小的项目,具备一定经验的开发人员应该能够设计和构建出便于进行维护和扩展的应用程序.但是,随着功能模块数量(以及开发维护这些部件的人员)的不断增加,对项目实施控制的难度开始呈指数级增长. Sa ...
- rpm包安装的nginx热升级
文章目录一.本地环境基本介绍二.yum升级命令说明三.升级好nginx后如何不中断业务切换3.1.nginx相关的信号说明3.2.在线热升级nginx可执行文件程序一.本地环境基本介绍本次测试环境,是 ...
- Nginx热升级流程,看这篇就够了
在之前做过 Nginx 热升级的演示,他能保证nginx在不停止服务的情况下更换他的 binary 文件,这个功能非常有用,但我们在执行 Nginx 的 binary 文件升级过程中,还是会遇到很多问 ...
- nginx 安装第三方模块(lua)并热升级
需求: nginx上将特定请求拒绝,并返回特定值. 解决办法: 使用lua脚本,实现效果. 操作步骤: 安装Luajit环境 重新编译nginx(目标机器上nginx -V 配置一致,并新增两个模块n ...
- Beego开启热升级
1.打开配置 beego.BConfig.Listen.Graceful = true 2.写入pid 程序入口main()函数里写入pid func writePid() { fileName := ...
- 螣龙安科:威胁研究——与MAZE勒索软件事件相关的策略,技术和程序
至少从2019年5月开始,恶意行为者就一直在积极部署MAZE勒索软件.勒索软件最初是通过垃圾邮件和漏洞利用工具包分发的,后来又转移到妥协后进行部署.根据我们在地下论坛中对涉嫌用户的观察以及整个Mand ...
- PHP插件技术-插件钩子(hooks)分析
最近准备做一个开源的个人博客系统,因为在构想中要添加插件功能,所以就研究了一下插件功能的实现方法. 插件的功能按照本人自己的理解就是对已有的程序进行功能方面的添加以及改进,插件要与程序所提供的接口进行 ...
随机推荐
- RunTime.getRuntime().exec()运行脚本命令介绍和阻塞
java在企业级项目开发中,无论是强制性的功能需要,还是为了简便java的实现,需要调用服务器命令脚本来执行.在java中,RunTime.getRuntime().exec()就实现了这个功能. ...
- 双系统win7和ubuntu14.04进入了grub rescue>
可以跳过的废话:最近在学习caffe,需要在linux下安装cuda,sudo apt-get install cuda后,出现了由于根目录/空间不足而失败的情况. 于是想把win7下80G的一个盘格 ...
- Spark_总结五
Spark_总结五 1.Storm 和 SparkStreaming区别 Storm 纯实时的流式处理,来一条数据就立即进行处理 SparkStreaming ...
- 消消乐、candy crush类三消游戏程序逻辑分析
最近在开发一款类似消消乐的三消游戏,在碰到实现斜方向下落的时候卡住了很长时间.好几天没有思路,原本的思路是一次性预判多个宝石的一连串运动路径,运用缓动运动队列来实现宝石运动路径,例如 下落->滑 ...
- 轻松理解JavaScript闭包
摘要 闭包机制是JavaScript的重点和难点,本文希望能帮助大家轻松的学习闭包 一.什么是闭包? 闭包就是可以访问另一个函数作用域中变量的函数. 下面列举出常见的闭包实现方式,以例子讲解闭包概念 ...
- .NET中可空值类型实现原理
为了让.Net中的值类型可以赋值为null,微软特地添加了Nullable<T>类型,也可简写为T?.但是Nullable<T>自身是结构体,也是值类型,那么它是如何实现将nu ...
- NIO(三、Channel)
目录 NIO(一.概述) NIO(二.Buffer) NIO(三.Channel) Channel 上文说了描述了Buffer的实现机制,那么这个章节就主要描述数据是如何进入缓冲区的,并且又是如何从缓 ...
- Java语言定义的线程状态分析
说到线程,一定要谈到线程状态,不同的状态说明线程正处于不同的工作机制下,不同的工作机制下某些动作可能对线程产生不同的影响. Java语言定义了6中状态,而同一时刻,线程有且仅有其中的一种状态.要获取J ...
- WebService客户端添加SOAPHeader信息
通过JAXBContext创建Marshaller对头信息进行解析为dom,获取WSBindingProvider,使用Headers.creat()创建soap的Header元素: 另外就是:将us ...
- 多边形剪裁img
<!DOCTYPE html><html><head> <meta charset="utf-8"/> <title>& ...