【.net 深呼吸】程序集的热更新

当一个程序集被加载使用的时候,出于数据的完整性和安全性考虑,程序集文件(在99.9998%的状况下是.dll文件)会被锁定,若是此时你想更新程序集(其实是替换dll文件),是不能够操做的,这时你得把应用程序退出,替换文件后再启动程序。缓存

多数状况下这样作是可行的,只是有时候,好比ASP.NET或一些须要一直运行的服务进程,重启程序来更新好像不太好。安全

要是想对程序集进行热更新,即在程序运行的同时替换文件,有一个你们很熟悉的方案——影像复制,若是你不熟悉.net,你确定没据说过的。固然了,这个叫法也挺难听的,没办法,只好这样翻译,原词是 Shadow Copy ,Shadow是影子,阴影,影像的意思,那也只好这么翻译了。不过,你不用担忧它很抽象很高端,其实,只要用心学,没什么东西是攻不克的。函数

我用一句话来归纳一下影子复制(也能够叫拷贝,但我不喜欢拷贝这个词,很黄很暴力的感受)——应用程序域在加载程序集时,会把程序集文件复制到另外一个地方,再进行加载。这样一来,当程序集文件被使用时,它锁定的是复制后的文件,即原始文件咱们能够放心地去替换了,等到合适的时间,把应用程序从新启动一下,再次运行时,就会自动把最新的程序集复制到缓存的目录下,而后执行最新版本的代码。最好把这些代码的调用放到一个新的应用程序域中执行,由于这样的好处是不用从新启动应用程序,而只要把某个应用程序域卸载掉再从新建立一个新的,就会自动加载最新的程序集了。并且,一般你都应该这么作的,建立一个应用程序域,在里面执行代码,执行完了就把应用程序域卸掉,能够节约资源。测试

应用程序在运行的时候,默认会建立一个应用程序域的,说白了,一个进程中至少会有一个应用程序域,若是你把某段代码放到一个新的应用程序域中执行,而且你但愿执行完后,能够把结果传回给主应用程序域,那就用老周之前写过的方法,记得老周前面写过的,想按引用传递对象,就从MarshalByRefObject类派生,想让对象按值传递,就让它支持序列化。spa

 

在建立新的应用程序域时,能够同时传递一个SetupInfo对象,这个对象有一个 ShadowCopyFiles 属性,虽然它定义的类型是 string,但你千万不要理解错,不要把一个文件的路径赋给它。老周之前就见到一位朋友理解错了,它误觉得这个属性是用来设置复制程序集文件的缓存路径,结果代码写了总是不行。唉,这就是不看MSDN的下场。.net

不要乱来,设置复制程序集的缓存目录是 CachePath 属性,不是 ShadowCopyFiles 属性。ShadowCopyFiles 属性只能用两个字符串的值,若是要启用影像复制,就设置为 true,若是想禁用,就设置 false 或者干脆保持默认的null值。也就说,它是一个用字符串表示的 bool 值。命令行

 

下面,咱们用一个例子来表演一下,很精彩的。翻译

首先,弄一个类库项目,而后在里面写一个全宇宙最简单的类。3d

namespace TestLib
{
    public class Demo
    {
        public string Call()
        {
            return "Ver - 3";
        }
    }
}

 

而主启动项目是一个控制台应用,这里,老周但愿设置新应用程序域的 PrivateBinPath ,这个属性能够设置一堆目录,能够是相对路径,其实应该是用相对路径的,由于这个目录不能乱设的,它必须是应用程序目录的子目录。若是是多个目录,能够用英文的分号(;)来分隔。code

ApplicationBase路径指定的是应用程序,即.exe启动的目录,无论你建立多个新的应用程序域,这个目录都必须指定为当前exe的启动目录。不然你试试看,不能运行的,由于应用程序域之间是隔离的,因此在新建立的应用程序域中也必须加载当前exe所在的程序集,这个程序集是必须的,由于它是主入口点。

而 PrivateBinPath 属性所指定的路径必须为应用程序目录的子目录,好比,咱们的项目在Debug模式下,一般是把exe生成到 bin \ Debug目录下的,因此,你能够在Debug目录下建立一个子目录,我这里创了一个,叫ExtDlls,随后我会把要用到的dll文件放在这个目录中,并设置 PrivateBinPath = "ExtDlls" ,这样一来,就算项目不引用这个类库项目,在运行阶段它都会自动到这个 ExtDlls 目录下去找,找到了就加载,要是找不到就会“呵呵”。

我这个类库项目名叫 TestLib,为了让它生成后可以自动把最新的版本复制到 ExtDlls 目录中,能够打开类库项目的项目属性窗口,切换到【生成事件】页,并在“后期生成命令行”中输入如下命令:

copy "$(TargetPath)" "$(SolutionDir)MyApp\bin\Debug\ExtDlls\"

这么一搞,每次我从新生成类库项目后,就会自动把dll文件复制过去。

 

好,下面的重点放在主项目上,在代码中,能够建立一个新的应用程序域,而后调用类库中的代码。

                AppDomainSetup setup = new AppDomainSetup();
                setup.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
                setup.ApplicationName = "ExtFuncs";
                setup.PrivateBinPath = "ExtDlls";
                setup.ShadowCopyFiles = "true";
                AppDomain newDom = AppDomain.CreateDomain("hello", null, setup);

                newDom.DoCallBack(() =>
                {
                    Type t = Type.GetType("TestLib.Demo, TestLib");
                    // 获取公共无参构造函数
                    ConstructorInfo costr = t.GetConstructor(new Type[] { });
                    // 调用构造函数,建立类型实例
                    object instance = costr.Invoke(new object[] { });
                    // 找到要调用的方法
                    MethodInfo m = t.GetMethod("Call", BindingFlags.Public | BindingFlags.Instance);
                    // 调用方法,获得返回值
                    object retval = m.Invoke(instance, new object[] { });
                    Console.WriteLine($"调用输出:{retval}");
                    Console.WriteLine("\n===================================");

                    // 输出引用程序集的路径
                    var refAsses = AppDomain.CurrentDomain.GetAssemblies();
                    foreach (var ass in refAsses)
                    {
                        Console.WriteLine("名称:"+ ass.GetName().Name);
                        Console.WriteLine("路径:" + ass.Location);
                        Console.WriteLine();
                    }
                });
                AppDomain.Unload(newDom); //卸载应用程序域

 

实验代表,ApplicationName 属性的值能够随便写,但 ApplicationBase 属性必须是当前应用程序所在目录。

这里我用的是反射的方法来调用的,DoCallBack 方法容许在另外一个应用程序域中执行代码,代码内容经过一个委托来关联。

 

在反射调用完测试类库后,我还用这段代码来输出新的应用程序域所引用的全部程序集的路径。

                    var refAsses = AppDomain.CurrentDomain.GetAssemblies();
                    foreach (var ass in refAsses)
                    {
                        Console.WriteLine("名称:"+ ass.GetName().Name);
                        Console.WriteLine("路径:" + ass.Location);
                        Console.WriteLine();
                    }

因为这段代码是在新的应用程序域中执行的,因此 CurrentDomain 属性所指的是新建立的应用程序域,而不是进程运行时建立的默认域。

之因此要在反射以后输出路径是由于,应用程序域是动态加载程序集,即当你用到类库中的类型时才会加载,若是不访问类库中的任何东西,是不会加载这个程序集的。

我为啥要输出路径呢,就是让大伙可以清楚地看到,TestLib 类库已经被复制到另外一个目录中执行了。请看:

从这个图你就看到,默认的缓存程序集的路径是在你的用户配置目录下的 AppData \ Local \ assembly 下面。

 

可能你以为这个默认的缓存路径很差,能不能自定义啊?能,前面老周提了一下 CachePath 属性,对,你给这个属性分配一个路径,缓存的程序集就会放到这个自定义路径中了。好比,我在Debug目录下新建一个 TempAss 目录,用来存放临时复制的程序集。

setup.CachePath = CACHE_PATH;

 

而后你再看它的路径。

看,是否是变了?

 

如今,咱们来验证一下,是否是能够热更新。

先运行exe,输出Ver - 1 ,如图。

 

好,保持exe运行着,不要关,而后修改一下类库项目的代码。

    public class Demo
    {
        public string Call()
        {
            return "Ver - 2";
        }
    }

把 1 改成 2。

 

从新生成一下类库项目,它会自动复制到 ExtDlls 目录。

如今在控制台窗口按除 Esc 之外的任意键,就会从新建一个应用程序域,并加载执行类库代码,由于我弄了个循环,只有遇到Esc键才会退出。

这时候,你看到,输出的内容变了。

 

不用退出应用程序,就能实现程序集文件的替换了,这对于服务应用特别好使。

 

为了写代码有智能提示,若是我不想用反射呢,而是直接在VS中引用类库项目呢,试试,引用以后,把所TestLib属性中的“复制本地”改成false,由于 ExtDlls 目录下已经有文件了,没必要再复制了,在新的应用程序域中执行时,会自动搜索。

 

而后把DoCallBack 方法中的代码改一下:

                newDom.DoCallBack(() =>
                {
                    TestLib.Demo dm = new TestLib.Demo();
                    Console.WriteLine($"输出:{dm.Call()}");
                });

如今代码就变得简单多了,是吧,才两行就完事了。

 

那能不能运行呢,固然能了。看。

 

怎么样,牛逼烘烘吧。

好了,老周的芹菜炒鱼蛋饭作好了,肚子饿了,开饭了。

示例源代码下载

相关文章
相关标签/搜索