前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使Spring.NET的IOC容器支持动态加载的程序集

使Spring.NET的IOC容器支持动态加载的程序集

作者头像
明年我18
发布2019-09-18 11:39:33
6630
发布2019-09-18 11:39:33
举报
文章被收录于专栏:明年我18明年我18

当我们发布系统时,有时候希望不用关掉应用程序就能完成发布,但Spring.NET的ApplicationContext是从AppDomain.CurrentDomain中加载的程序集中创建对象的,并不支持从动态加载的程序集中创建对象,如果直接把更新后的程序集复制到bin目录,会无法替换dll或导致应用程序重启。最近我正好有这个需求,就研究了一下Spring的相关代码,需要解决的问题如下:

1.首先要解决如何动态加载程序集

2.其次要找到某种方式告诉Spring在创建对象的时候用我们自己加载进来的程序集

如何动态加载程序集

动态加载程序集,网上最多的说法是另外创建一个AppDomain,然后在新创建的AppDomain里加载程序集。但这种方式不太不适合我,因为要想让一个对象能够穿过AppDomain边界,必须要继承MarshalByRefObject类,否则无法被其他AppDomain使用,而且从一个AppDomain调用另外一个AppDomain里的程序集时,需要多一层通信,这样太复杂了,而且出了问题也不好调试。

我的主程序,利用Spring的IOC容器取出对象,然后调用对象的方法,这种应用场景,最适合的还是只存在一个AppDomain,所以得考虑另外的方法来动态加载程序集。

动态加载程序集的目的,就是为了可以在不关闭应用程序的情况下替换dll,如果我直接Assembly.LoadFile肯定是不行的,因为我一旦Load了这个File,这个文件就被使用了,没法替换。

所以首先要把程序集复制到临时目录,然后用Assembly.LoadFile去加载临时目录中的程序集,这样就可以在运行期替换程序集了。当然,我们还需要一个FileSystemWatcher来监控程序集的目录,当目录中的程序集发生变化时,再把新的程序集复制到新的临时目录,然后再加载新临时目录中的程序集文件。

当然,已经加载的程序集在AppDomain.CurrentDomain没有被销毁前,是不能卸载的,所以经过几次程序集的更新后,我们的AppDomain.CurrentDomain中就会存在几个不同版本的程序集。为了区分出哪个程序集才是最新的,我们还需要一个全局的dictionary来存放最新的程序集,这个dictionary以程序集的名字作为key,以最新的加载后的程序集作为value。每次加载完临时目录的程序集之后,要更新这个dictionary。

相关代码我在这里就不贴了,大家可以查看附件里的源代码。需要注意的是由于FileSystemWatcher在文件被修改时会多次触发Changed事件,所以为了避免多次加载同一个程序集,我稍微处理了一下,加了一个Timer,当定时器事件触发的时候才去加载程序集,而不是文件一被修改了就去加载。

如何让Spring用我们的程序集创建对象

Spring在创建对象的时候,是利用反射,根据type的字符串形式来加载System.Type类型。它的ResolveType的核心逻辑如下(位于Spring.Core.TypeResolution.TypeResolver中):

代码语言:javascript
复制
public virtual Type Resolve(string typeName){    if (StringUtils.IsNullOrEmpty(typeName))    {        throw BuildTypeLoadException(typeName);    }    TypeAssemblyHolder typeInfo = new TypeAssemblyHolder(typeName);    Type type = null;    try    {        type = typeInfo.IsAssemblyQualified ? LoadTypeDirectlyFromAssembly(typeInfo) : LoadTypeByIteratingOverAllLoadedAssemblies(typeInfo);    }    catch (Exception exception)    {        if (exception is TypeLoadException)        {            throw;        }        throw BuildTypeLoadException(typeName, exception);    }    if (type == null)    {        throw BuildTypeLoadException(typeName);    }    return type;}

从上面的代码可以看到,他resolve type的逻辑是:

  1. 如果是“TypeName,AssemblyName”的格式,则调用LoadTypeDirectlyFromAssembly来加载程序集。即先用Assembly.LoadWithPartialName来得到程序集,然后用Assembly.GetType方法来得到类型。
  2. 如果是“TypeName”的格式(即不写AssemblyName),则调用LoadTypeByIteratingOverAllLoadedAssemblies来加载程序集。也就是便利AppDomain.CurrentDomain中所有已加载的程序集,对每个程序集都调用一次Assembly.GetType方法,看看哪个程序集可以成功的返回类型。这可就不保险了,因为我们上面解释了,AppDomain.CurrentDomain中可能包含多个版本的程序集。

在实际使用时,我们大多都会使用第一种格式的类型字符串,即“TypeName,AssemblyName”的形式,所以它会走到第1中情况中。我们知道,当Assembly.LoadWithPartialName失败时,会触发AppDomain.AssemblyResolve事件(当然不仅仅是LoadWithPartialName失败时会触发,还要很多情况下会触发这个事件),那我们是否可以通过AppDomain.AssemblyResolve事件来返回我们需要的程序集呢?通过试验,这种方式也是不可以的,因为一旦Assembly.LoadWithPartialName利用AppDomain.AssemblyResolve事件得到了程序集之后,第二次再调用这个方法时,就不会再去触发AppDomain.AssemblyResolve事件了,因为.net framework认为这个程序集它是可以识别的,不需要再触发这个事件了。但我们的程序集在运行期是有可能不断变化的。

那么,是否可以通过继承TypeResolver,并重写Resolve方法来达到我们的目的?这应该是一个比较好的解决方案,但我并没有找到在哪里可以注入我们自己的TypeResolver的实现。大家可以去看一下Spring.Core.TypeResolution.TypeResolutionUtils类的实现,或许你们可以找到如何注入自己的TypeResolver的方法。

通过查看Spring的相关代码,最后我的解决方案如下:创建一个类ObjectFactory,并继承自Spring.Objects.Factory.Support.DefaultListableObjectFactory,然后重写GetMergedObjectDefinition方法,在这个方法里去Resolve出正确的type。

代码语言:javascript
复制
public class ObjectFactory : DefaultListableObjectFactory{    public ObjectFactory(bool caseSensitive, IObjectFactory parentFactory)        : base(caseSensitive, parentFactory)    {    }     protected override RootObjectDefinition GetMergedObjectDefinition(string name, IObjectDefinition definition)    {        var rootDefinition = base.GetMergedObjectDefinition(name, definition);        if (rootDefinition != null)        {            Type type;            if (rootDefinition.HasObjectType)            {                type = AssemblyUtils.ResolveType(rootDefinition.ObjectType.AssemblyQualifiedName);            }            else            {                type = AssemblyUtils.ResolveType(rootDefinition.ObjectTypeName);            }             if (type != null)            {                rootDefinition.ObjectType = type;            }        }        return rootDefinition;    }        }

AssemblyUtils.ResolveType就是我用来resolve出正确的type的方法,它的实现如下:

代码语言:javascript
复制
public Type ResolveType(string typeString){    if (string.IsNullOrEmpty(typeString)) return null;     var typeName = typeString;    string assemblyName = null;    var i = typeString.IndexOf(',');    if (i == 0) return null;    if (i > 0)    {        typeName = typeString.Substring(0, i);        if (i < typeString.Length - 1)        {            assemblyName = typeString.Substring(i + 1);        }    }    Assembly assembly;    if (string.IsNullOrEmpty(assemblyName))    {        assembly = Assembly.GetExecutingAssembly();    }    else    {        assembly = ResolveAssembly(assemblyName);    }    if (assembly == null) return null;     var type = assembly.GetType(typeName, false, true);    return type;} public Assembly ResolveAssembly(string assemblyString){    var targetName = new AssemblyName(assemblyString);    foreach (var assembly in loadedAssemblies.Keys)    {        var assemblyName = new AssemblyName(assembly);        if (assemblyName.FullName.Equals(targetName.FullName, StringComparison.OrdinalIgnoreCase)            || assemblyName.Name.Equals(targetName.Name, StringComparison.OrdinalIgnoreCase))        {            return loadedAssemblies[assembly];        }    }     var assemblies = AppDomain.CurrentDomain.GetAssemblies();    foreach (var assembly in assemblies)    {        var assemblyName = assembly.GetName();        if (assemblyName.FullName.Equals(targetName.FullName, StringComparison.OrdinalIgnoreCase)            || assemblyName.Name.Equals(targetName.Name, StringComparison.OrdinalIgnoreCase))        {            return assembly;        }    }    return null;}

其中,loadedAssemblies是一个dictionary,里面存的就是最新加载进来的程序集。

有了ObjectFactory之后,剩下的问题就是如何用这个ObjectFactory。创建一个类XmlApplicationContext,让它继承自Spring.Context.Support.XmlApplicationContext,然后重写它的CreateObjectFactory方法,在这里就可以返回我们自己的ObjectFactory了:

代码语言:javascript
复制
public class XmlApplicationContext : Spring.Context.Support.XmlApplicationContext{    protected override DefaultListableObjectFactory CreateObjectFactory()    {        return new ObjectFactory(CaseSensitive, GetInternalParentObjectFactory());    }}

到此为止,就可以在程序里利用我们这个XmlApplicationContext来GetObject了,例如:

代码语言:javascript
复制
XmlApplicationContext ctx = new XmlApplicationContext("d:\objects.xml");var o = ctx.GetObject("Test");

但这样就完了吗?还没。因为我们在实际应用中一般都会把IApplicationContext交给ContextHandler去处理,然后把ContextHandler配置在app.config里。所以我们还需要创建一个ContextHandler:】

代码语言:javascript
复制
public class ContextHandler : Spring.Context.Support.ContextHandler{    protected override Type DefaultApplicationContextType    {        get { return typeof(XmlApplicationContext); }    }}

在这个ContextHandler里,我们重写了DefaultApplicationContextType属性,并且返回了我们自己的XmlApplicationContext。

最后,在app.config里配置我们这个ContextHandler:

代码语言:javascript
复制
<configSections>  <sectionGroup name="spring">    <section name="context" type="Spring.DynamicLoading.ContextHandler, Spring.DynamicLoading" />        </sectionGroup></configSections> <spring>      <context>    <resource uri="Configs\\objects.xml"/>        </context></spring>

这样我们的程序还是像以前那样使用:

代码语言:javascript
复制
IApplicationContext ctx = ContextRegistry.GetContext();var calculator = ctx.GetObject("Calculator") as ICalculator;

不过配置object的时候,必须得把object配置成singleton=false的,要不然我们的ObjectFactory不会起作用。但这样想想也对,程序集更新后,如果想让对象也重新创建,那对象从本身来讲就不应该是singleton的了。如果您有更高的要求,比如需要对象一直singleton到下次更新,我想也是有办法解决的,因为Spring.net的扩展性非常好,在这里我就不多做研究了。

附件的说明

在这里可以下载源代码,里面有4个项目,其中Spring.DynamicLoading是用于使Sring支持动态加载程序集的类库,另外三个是测试用的。这三个测试项目的关系如下:

  1. ClassLibrary.Interface:定义了一个ICalculator接口,用于计算两个int数字的结果
  2. ClassLibrary1:引用ClassLibrary.Interface,实现ICalculator接口。
  3. Spring.DynamicLoading.WinFormTest:引用ClassLibrary.Interface,但注意并不直接引用ClassLibrary1,而是在objects.xml里配置了ICalculator的实现类,然后在程序里利用IApplicationContext取出ICalculator的实现。

ICalculator接口定义如下:

代码语言:javascript
复制
namespace ClassLibrary.Interface{    public interface ICalculator    {        int Calculate(int a, int b);    }}

ClassLibrary1中的实现如下:

代码语言:javascript
复制
using ClassLibrary.Interface; namespace ClassLibrary1{    public class Calculator : ICalculator    {        public int Calculate(int a, int b)        {            return a + b;            //return a * b;        }    }}

WinFormTest中的objects.xml如下:

代码语言:javascript
复制
<?xml version="1.0" encoding="utf-8" ?><objects xmlns="http://www.springframework.net"         xmlns:db="http://www.springframework.net/database">   <object id="Calculator" type="ClassLibrary1.Calculator,ClassLibrary1" singleton="false">  </object> </objects>

WinFormTest中调用ICalculator的逻辑如下:

代码语言:javascript
复制
private void btnCalculate_Click(object sender, EventArgs e){    int a = int.Parse(txtA.Text);    int b = int.Parse(txtB.Text);        IApplicationContext ctx = ContextRegistry.GetContext();    var calculator = ctx.GetObject("Calculator") as ICalculator;    if(calculator != null)    {        txtResult.Text = calculator.Calculate(a, b).ToString();    }}

如果想看到效果,先运行起WinFormTest,如下图:

image
image

dll目录选择ClassLibrary1项目的输出目录,然后点击“加载程序集”按钮:

image
image

如果弹出加载成功的提示,就说明程序已经把ClassLibrary1输出目录里的dll加载进来了,并且监控了这个目录,以后这个目录里的文件如果有修改,就会自动加载它们。

点击计算按钮,看看结果是不是5?这时候执行了a+b的逻辑:

image
image

这时候不要关闭Form1,直接修改一下ClassLibrary1的Calculator类,把a+b改成a*b,然后编译ClassLibrary1,再点击Form1的计算按钮,看看结果是不是变成了6?

image
image
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2010-10-03 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 如何动态加载程序集
  • 如何让Spring用我们的程序集创建对象
  • 附件的说明
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档