热门标签 | HotTags
当前位置:  开发笔记 > 后端 > 正文

浅析.NetCore中Json配置的自动更新

这篇文章主要介绍了浅析.NetCore中Json配置的自动更新,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

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();

在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) (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 configureSource)
 {
  return builder.Add(configureSource);
 }

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

 public static IConfigurationBuilder Add(this IConfigurationBuilder builder,Action 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 Sources { get; } = new List();

  public IConfigurationBuilder Add(IConfigurationSource source)
  {
   Sources.Add(source);
   return this;
  }

  public IConfigurationRoot Build()
  {
   var providers = new List();
   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 OnChange(Func changeTokenProducer, Action changeTokenConsumer)
  {
   return new ChangeTokenRegistration(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 changeTokenProducer 方法知道,这里的:rat:,其实是 IChangeToken .

IChangeToken

 public interface IChangeToken
 {
  bool HasChanged { get; }

  bool ActiveChangeCallbacks { get; }

  IDisposable RegisterChangeCallback(Action 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 OnChange(Func changeTokenProducer, Action changeTokenConsumer)
  {
   return new ChangeTokenRegistration(changeTokenProducer, callback => callback(), changeTokenConsumer);
  }
 }
 public class ChangeTokenRegistration
 {
  private readonly Func _changeTokenProducer;
  private readonly Action _changeTokenConsumer;
  private readonly TAction _state;

  public ChangeTokenRegistration(Func changeTokenProducer, Action 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();
  this._timerFactory = (Func) (() => 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

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


推荐阅读
  • Java验证码——kaptcha的使用配置及样式
    本文介绍了如何使用kaptcha库来实现Java验证码的配置和样式设置,包括pom.xml的依赖配置和web.xml中servlet的配置。 ... [详细]
  • YOLOv7基于自己的数据集从零构建模型完整训练、推理计算超详细教程
    本文介绍了关于人工智能、神经网络和深度学习的知识点,并提供了YOLOv7基于自己的数据集从零构建模型完整训练、推理计算的详细教程。文章还提到了郑州最低生活保障的话题。对于从事目标检测任务的人来说,YOLO是一个熟悉的模型。文章还提到了yolov4和yolov6的相关内容,以及选择模型的优化思路。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 本文介绍了在SpringBoot中集成thymeleaf前端模版的配置步骤,包括在application.properties配置文件中添加thymeleaf的配置信息,引入thymeleaf的jar包,以及创建PageController并添加index方法。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
  • 本文介绍了Android 7的学习笔记总结,包括最新的移动架构视频、大厂安卓面试真题和项目实战源码讲义。同时还分享了开源的完整内容,并提醒读者在使用FileProvider适配时要注意不同模块的AndroidManfiest.xml中配置的xml文件名必须不同,否则会出现问题。 ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • r2dbc配置多数据源
    R2dbc配置多数据源问题根据官网配置r2dbc连接mysql多数据源所遇到的问题pom配置可以参考官网,不过我这样配置会报错我并没有这样配置将以下内容添加到pom.xml文件d ... [详细]
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • flowable工作流 流程变量_信也科技工作流平台的技术实践
    1背景随着公司业务发展及内部业务流程诉求的增长,目前信息化系统不能够很好满足期望,主要体现如下:目前OA流程引擎无法满足企业特定业务流程需求,且移动端体 ... [详细]
author-avatar
伟经理_469
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有