pre

很早在看 jesse 的 asp.net core快速入门 的课程的时候就了解到了在asp .net core中,如果添加的json配置被更改了,是支持自动重载配置的,作为一名有着严重”造轮子”情节的程序员,最近在折腾一个博客系统,也想造出一个这样能自动更新以mysql为数据源的configuresource,于是点开了addjsonfile这个拓展函数的源码,发现别有洞天,蛮有意思,本篇文章就简单地聊一聊json config的reloadonchange是如何实现的,在学习reloadonchange的过程中,我们会把configuration也顺带撩一把:grin:,希望对小伙伴们有所帮助.

public static iwebhostbuilder createwebhostbuilder(string[] args) =>
  webhost.createdefaultbuilder(args)
  .configureappconfiguration(option =>
   {
   option.addjsonfile("appsettings.json",optional:true,reloadonchange:true);
   })
  .usestartup<startup>();

在asp .net core中如果配置了json数据源,把reloadonchange属性设置为true即可实现当文件变更时自动更新配置,这篇博客我们首先从它的源码简单看一下,看完你可能还是会有点懵的,别慌,我会对这些代码进行精简,做个简单的小例子,希望能对你有所帮助.

一窥源码

addjson

首先,我们当然是从这个我们耳熟能详的扩展函数开始,它经历的演变过程如下.

 public static iconfigurationbuilder addjsonfile(this iconfigurationbuilder builder,string path,bool optional,bool reloadonchange)
 {
 return builder.addjsonfile((ifileprovider) null, path, optional, reloadonchange);
 }

传递一个null的fileprovider给另外一个重载addjson函数.

敲黑板,null的fileprovider很重要,后面要考:smile:.

 public static iconfigurationbuilder addjsonfile(this iconfigurationbuilder builder,ifileprovider provider,string path,bool optional,bool reloadonchange)
 {
  return builder.addjsonfile((action<jsonconfigurationsource>) (s =>
  {
  s.fileprovider = provider;
  s.path = path;
  s.optional = optional;
  s.reloadonchange = reloadonchange;
  s.resolvefileprovider();
  }));
 }

把传入的参数演变成一个action委托给 jsonconfigurationsource 的属性赋值.

 public static iconfigurationbuilder addjsonfile(this iconfigurationbuilder builder, action<jsonconfigurationsource> configuresource)
 {
  return builder.add<jsonconfigurationsource>(configuresource);
 }

最终调用的builder.add (action)方法.

 public static iconfigurationbuilder add<tsource>(this iconfigurationbuilder builder,action<tsource> configuresource)where tsource : iconfigurationsource, new()
 {
  tsource source = new tsource();
  if (configuresource != null)
  configuresource(source);
  return builder.add((iconfigurationsource) source);
 }

在add方法里,创建了一个source实例,也就是jsonconfigurationsource实例,然后把这个实例传为刚刚的委托,这样一来,我们在最外面传入的 "appsettings.json",optional:true,reloadonchange:true 参数就作用到这个示例上了.

最终,这个实例添加到builder中.那么builder又是什么?它能干什么?

configurationbuild

前面提及的builder默认情况下是 configurationbuilder ,我对它的进行了简化,关键代码如下.

public class configurationbuilder : iconfigurationbuilder
 {
  public ilist<iconfigurationsource> sources { get; } = new list<iconfigurationsource>();

  public iconfigurationbuilder add(iconfigurationsource source)
  {
   sources.add(source);
   return this;
  }

  public iconfigurationroot build()
  {
   var providers = new list<iconfigurationprovider>();
   foreach (var source in sources)
   {
    var provider = source.build(this);
    providers.add(provider);
   }
   return new configurationroot(providers);
  }
 }

可以看到,这个builder中有个集合类型的sources,这个sources可以保存任何实现了 iconfigurationsource 的source,前面聊到的 jsonconfigurationsource 就是实现了这个接口,常用的还有 memoryconfigurationsource , xmlconfiguresource , commandlineconfigurationsource 等.

另外,它有一个很重要的build方法,这个build方法在 webhostbuilder 方法执行 build 的时候也被调用,不要问我 webhostbuilder.builder 方法什么执行的:joy:.

public static void main(string[] args)
  {
   createwebhostbuilder(args).build().run();
  }

在configurebuilder的方法里面就调用了每个source的builder方法,我们刚刚传入的是一个 jsonconfigurationsource ,所以我们有必要看看jsonsource的builder做了什么.

这里是不是被这些builder绕哭了? 别慌,下一篇文章中我会讲解如何自定义一个configuresoure,会把congigure系列类uml类图整理一下,应该会清晰很多.

jsonconfigurationsource

public class jsonconfigurationsource : fileconfigurationsource
 {
  public override iconfigurationprovider build(iconfigurationbuilder builder)
  {
   ensuredefaults(builder);
   return new jsonconfigurationprovider(this);
  }
 }

这就是 jsonconfigurationsource 的所有代码,未精简,它只实现了一个build方法,在build内,ensuredefaults被调用,可别小看它,之前那个空的fileprovider在这里被赋值了.

 public void ensuredefaults(iconfigurationbuilder builder)
  {
   fileprovider = fileprovider ?? builder.getfileprovider();
  }
  public static ifileprovider getfileprovider(this iconfigurationbuilder builder)
  {
   return new physicalfileprovider(appcontext.basedirectory ?? string.empty);
  }

可以看到这个fileprovider默认情况下就是 physicalfileprovider ,为什么对这个 fileprovider 如此宠幸让我花如此大的伏笔要强调它呢?往下看.

jsonconfigurationprovider && fileconfigurationprovider

在jsonconfigurationsource的build方法内,返回的是一个jsonconfigurationprovider实例,所以直觉告诉我,在它的构造函数内必有猫腻:confused:.

 public class jsonconfigurationprovider : fileconfigurationprovider
 {
  
  public jsonconfigurationprovider(jsonconfigurationsource source) : base(source) { }

  
  public override void load(stream stream)
  {
   try {
    data = jsonconfigurationfileparser.parse(stream);
   } catch (jsonreaderexception e)
   {
    throw new formatexception(resources.error_jsonparseerror, e);
   }
  }
 }

看不出什么的代码,事出反常必有妖~~

看看base的构造函数.

 public fileconfigurationprovider(fileconfigurationsource source)
  {
   source = source;

   if (source.reloadonchange && source.fileprovider != null)
   {
    _changetokenregistration = changetoken.onchange(
     () => source.fileprovider.watch(source.path),
     () => {
      thread.sleep(source.reloaddelay);
      load(reload: true);
     });
   }
  }

真是个天才,问题就在这个构造函数里,它构造函数调用了一个 changetoken.onchange 方法,这是实现reloadonchange的关键,如果你点到这里还没有关掉,恭喜,好戏开始了.

reloadonchange

talk is cheap. show me the code (屁话少说,放 过来).

 public static class changetoken
 {
  public static changetokenregistration<action> onchange(func<ichangetoken> changetokenproducer, action changetokenconsumer)
  {
   return new changetokenregistration<action>(changetokenproducer, callback => callback(), changetokenconsumer);
  }
 }

onchange方法里,先不管什么func,action,就看看这两个参数的名称,producer,consumer,生产者,消费者,不知道看到这个关键词想到的是什么,反正我想到的是小学时学习食物链时的:snake:与:rat:.

那么我们来看看这里的:snake:是什么,:rat:又是什么,还得回到 fileconfigurationprovider 的构造函数.

可以看到生产者:rat:是:

() => source.fileprovider.watch(source.path)

消费者:snake:是:

() => {
 thread.sleep(source.reloaddelay);
 load(reload: true);
}

我们想一下,一旦有一条:rat:跑出来,就立马被:snake:吃了,

那我们这里也一样,一旦有fileprovider.watch返回了什么东西,就会发生load()事件来重新加载数据.

:snake:与:rat:好理解,可是代码就没那么好理解了,我们通过 onchange 的第一个参数 func<ichangetoken> changetokenproducer 方法知道,这里的:rat:,其实是 ichangetoken .

ichangetoken

 public interface ichangetoken
 {
  bool haschanged { get; }

  bool activechangecallbacks { get; }

  idisposable registerchangecallback(action<object> callback, object state);
 }

ichangetoken的重点在于里面有个registerchangecallback方法,:snake:吃:rat:的这件事,就发生在这回调方法里面.

我们来做个:snake:吃:rat:的实验.

实验1

 static void main()
  {
   //定义一个c:\users\liuzh\mybox\testspace目录的fileprovider
   var phyfileprovider = new physicalfileprovider("c:\\users\\liuzh\\mybox\\testspace");

   //让这个provider开始监听这个目录下的所有文件
   var changetoken = phyfileprovider.watch("*.*");

   //注册吃这件事到回调函数
   changetoken.registerchangecallback(_=> { console.writeline("老鼠被蛇吃"); }, new object());

   //添加一个文件到目录
   addfiletopath();

   console.readkey();

  }

  static void addfiletopath()
  {
   console.writeline("老鼠出洞了");
   file.create("c:\\users\\liuzh\\mybox\\testspace\\老鼠出洞了.txt").dispose();
  }

这是运行结果

可以看到,一旦在监听的目录下创建文件,立即触发了执行回调函数,但是如果我们继续手动地更改(复制)监听目录中的文件,回调函数就不再执行了.

这是因为changetoken监听到文件变更并触发回调函数后,这个changetoken的使命也就完成了,要想保持一直监听,那么我们就在在回调函数中重新获取token,并给新的token的回调函数注册通用的事件,这样就能保持一直监听下去了.

这也就是changetoken.onchange所作的事情,我们看一下源码.

 public static class changetoken
 {
  public static changetokenregistration<action> onchange(func<ichangetoken> changetokenproducer, action changetokenconsumer)
  {
   return new changetokenregistration<action>(changetokenproducer, callback => callback(), changetokenconsumer);
  }
 }
 public class changetokenregistration<taction>
 {
  private readonly func<ichangetoken> _changetokenproducer;
  private readonly action<taction> _changetokenconsumer;
  private readonly taction _state;

  public changetokenregistration(func<ichangetoken> changetokenproducer, action<taction> changetokenconsumer, taction state)
  {
   _changetokenproducer = changetokenproducer;
   _changetokenconsumer = changetokenconsumer;
   _state = state;

   var token = changetokenproducer();

   registerchangetokencallback(token);
  }

  private void registerchangetokencallback(ichangetoken token)
  {
   token.registerchangecallback(_ => onchangetokenfired(), this);
  }

  private void onchangetokenfired()
  {
   var token = _changetokenproducer();

   try
   {
    _changetokenconsumer(_state);
   }
   finally
   {
    // we always want to ensure the callback is registered
    registerchangetokencallback(token);
   }
  }
 }

简单来说,就是给token注册了一个 onchangetokenfired 的回调函数,仔细看看 onchangetokenfired 里做了什么,总体来说三步.

1.获取一个新的token.
2.调用消费者进行消费.
3.给新获取的token再次注册一个onchangetokenfired的回调函数.

如此周而复始~~

实验2

既然知道了onchange的工作方式,那么我们把实验1的代码修改一下.

  static void main()
  {
   var phyfileprovider = new physicalfileprovider("c:\\users\\liuzh\\mybox\\testspace");
   changetoken.onchange(() => phyfileprovider.watch("*.*"),
    () => { console.writeline("老鼠被蛇吃"); });
   console.readkey();
  }

执行效果看一下

可以看到,只要被监控的目录发生了文件变化,不管是新建文件,还是修改了文件内的内容,都会触发回调函数,其实jsonconfig中,这个回调函数就是load(),它负责重新加载数据,可也就是为什么asp .net core中如果把reloadonchang设置为true后,json的配置一旦更新,配置就会自动重载.

physicalfileswatcher

那么,为什么文件一旦变化,就会触发changetoken的回调函数呢? 其实 physicalfileprovider 中调用了 physicalfileswatcher 对文件系统进行监视,观察physicalfileswatcher的构造函数,可以看到 physicalfileswatcher 需要传入 filesystemwatcher , filesystemwatchersystem.io 下的底层io类,在构造函数中给这个watcher的created,changed,renamed,deleted注册eventhandler事件,最终,在这些eventhandler中会调用changtoken的回调函数,所以文件系统一旦发生变更就会触发回调函数.

 public physicalfileswatcher(string root,filesystemwatcher filesystemwatcher,bool pollforchanges,exclusionfilters filters)
 {
  this._root = root;
  this._filewatcher = filesystemwatcher;
  this._filewatcher.includesubdirectories = true;
  this._filewatcher.created += new filesystemeventhandler(this.onchanged);
  this._filewatcher.changed += new filesystemeventhandler(this.onchanged);
  this._filewatcher.renamed += new renamedeventhandler(this.onrenamed);
  this._filewatcher.deleted += new filesystemeventhandler(this.onchanged);
  this._filewatcher.error += new erroreventhandler(this.onerror);
  this.pollforchanges = pollforchanges;
  this._filters = filters;
  this.pollingchangetokens = new concurrentdictionary<ipollingchangetoken, ipollingchangetoken>();
  this._timerfactory = (func<timer>) (() => noncapturingtimer.create(new timercallback(physicalfileswatcher.raisechangeevents), (object) this.pollingchangetokens, timespan.zero, physicalfileswatcher.defaultpollinginterval));
 }

如果你和我一样,对源码感兴趣,可以从官方的 aspnet/extensions 中下载源码研究: https://github.com/aspnet/extensions

在下一篇文章中,我会讲解如何自定义一个以mysql为数据源的configuresoure,并实现自动更新功能,同时还会整理configure相关类的uml类图,有兴趣的可以关注我以便第一时间收到下篇文章.

本文章涉及的代码地址: https://github.com/liuzhenyulive/miniconfiguration

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持www.887551.com。