Silky微服务框架之模块

时间:2022-10-28 10:11:23

模块的定义

Silky是一个包括多个nuget包构成的模块化的框架,每个模块将程序划分为一个个小的结构,在这个结构中有着自己的逻辑代码和自己的作用域,不会影响到其他的结构。

模块类

一般地,一个模块的定义是通过在该程序集内创建一个派生自 SilkyModule的类,如下所示:


public class RpcModule : SilkyModule
{

}

SilkyModule是一个抽象的类,它定义了模块的基础方法,体现了模块在框架中的作用;

SilkyModule模块定义的核心代码如下所示:

public abstract class SilkyModule : Autofac.Module, ISilkyModule, IDisposable
{

   protected SilkyModule()
   {
       Name = GetType().Name.RemovePostFix(StringComparison.OrdinalIgnoreCase, "Module");
   }

   protected override void Load([NotNull] ContainerBuilder builder)
   {
       base.Load(builder);
       RegisterServices(builder);
   }

    public virtual void ConfigureServices(IServiceCollection services, IConfiguration configuration)
    {
    }
 
    protected virtual void RegisterServices([NotNull] ContainerBuilder builder)
    {
    }

    public virtual Task Initialize([NotNull] ApplicationContext applicationContext)
    {
       return Task.CompletedTask;
    }

    public virtual Task Shutdown([NotNull] ApplicationContext applicationContext)
    {
        return Task.CompletedTask;
    }

    public virtual string Name { get; }

   // 其他代码略...
}

通过对SilkyModule模块代码定义的分析我们可以得知,一个Silky模块有如下几个作用:

  1. ConfigureServices()方法中,通过IServiceCollection实现服务注册;

  2. RegisterServices()方法中,通过ContainerBuilder实现服务注册;

  3. 在应用程序启动时,通过Initialize()方法实现模块的初始化方法;

  4. 在应用程序停止时,执行Shutdown()方法,可以实现模块资源的释放;

关于上述第1、2 点的作用, 我们已经在服务引擎一章中做了详细的解析;关于第3、4点的作用,应用程序是如何在启动时调用Initialize()方法或是在停止时执行Shutdown()方法呢?

构建服务引擎一章中,我们提到,在构建服务引擎时,我们有一项很重要的工作就是注册了InitSilkyHostedService后台任务

后台任务InitSilkyHostedService的源码如下所示:

    public class InitSilkyHostedService : IHostedService
    {
        private readonly IModuleManager _moduleManager;
        private readonly IHostApplicationLifetime _hostApplicationLifetime;

        public InitSilkyHostedService(IServiceProvider serviceProvider,
            IModuleManager moduleManager,
            IHostApplicationLifetime hostApplicationLifetime)
        {
            if (EngineContext.Current is SilkyEngine)
            {
                EngineContext.Current.ServiceProvider = serviceProvider;
            }

            _moduleManager = moduleManager;
            _hostApplicationLifetime = hostApplicationLifetime;
        }

        public async Task StartAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine(@"                                              
   _____  _  _  _           
  / ____|(_)| || |          
 | (___   _ | || | __ _   _ 
  \___ \ | || || |/ /| | | |
  ____) || || ||   < | |_| |
 |_____/ |_||_||_|\_\ \__, |
                       __/ |
                      |___/
            ");
            var version = Assembly.GetExecutingAssembly().GetName().Version;
            var ver = $"{version.Major}.{version.Minor}.{version.Build}";
            Console.WriteLine($" :: Silky ::        {ver}");
            _hostApplicationLifetime.ApplicationStarted.Register(async () =>
            {
                await _moduleManager.InitializeModules();
            });
        }

        public async Task StopAsync(CancellationToken cancellationToken)
        {
            _hostApplicationLifetime.ApplicationStopped.Register(async () =>
            {
                await _moduleManager.ShutdownModules();
            });
        }
    }
  1. 在后台任务StartAsync(),在打印Silky的banner后,在应用启动时注册一个回调方法,通过模块管理器IModuleManager执行初始化模块方法;

  2. 在后台任务StopAsync(),在应用停止后注册一个回调方法,通过模块管理器IModuleManager执行关闭模块方法,一般用于各个模块的资源释放;

下面,我们查看模块管理器ModuleManager是如何初始化模块的:

        public async Task InitializeModules()
        {
            foreach (var module in _moduleContainer.Modules)
            {
                try
                {
                    Logger.LogInformation("Initialize the module {0}", module.Name);
                    await module.Instance.Initialize(new ApplicationContext(_serviceProvider, _moduleContainer));
                }
                catch (Exception e)
                {
                    Logger.LogError($"Initializing the {module.Name} module is an error, reason: {e.Message}");
                    throw;
                }
            }
        }

模块容器_moduleContainer的属性_moduleContainer.Modules是通过模块加载器ModuleLoader加载并通过依赖关系进行排序得到的所有模块的实例,我们看到通过foreach对所有的模块实例进行遍历,并依次执行各个模块的Initialize()方法。

同样的,在应用程序停止时,会调用InitSilkyHostedService任务的StopAsync(),该方法通过调用模块管理器的ShutdownModules()方法,实现对各个模块资源的释放;

    public async Task ShutdownModules()
    {
        foreach (var module in _moduleContainer.Modules)
        {
            await module.Instance.Shutdown(new ApplicationContext(_serviceProvider, _moduleContainer));
        }
    }

模块的类型

在Silky框架中,我将模块的类型划分为如下几种类型:

  1. 模块的定义SilkyModule: SilkyModule是一个抽象的模块,用于定义模块的概念;其他业务模块必须要派生自该类;

  2. 业务模块: 直接派生自SilkyModule类的非抽象类,Silky框架中,几乎所有的包在通过定义业务模块后从而实现模块化编程的,很多核心的包都是业务模块,如:SilkyModuleConsulModuleDotNettyModule等等模块都属于业务模块;

  3. Http类型的业务模块:该类型的业务模块派生自HttpSilkyModule,相比一般的业务模块,该类型的模块增加了Configure(IApplicationBuilder application)方法,该类型的模块一般用于通过web主机构建的微服务应用或是网关中,可以在Configure()方法中通过IApplicationBuilder引用http中间件,在silky框架中,诸如: CorsModuleIdentityModuleMiniProfilerModule等均是该类型的模块; 需要特别注意的是,需要http业务模块配置的中间件起效果的话,不要忘记需要在Startup类中的Configure进行如下配置:


public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 
{
   app.ConfigureSilkyRequestPipeline();
}
  1. 启动模块:该类型的模块派生自StartUpModule的非抽象类;在模块加载过程中,通过指定启动模块,从而得知模块的依赖关系,模块加载器会通过模块的依赖对模块进行排序,从而影响应用在启动时各个模块的执行的先后顺序;Silky模块预定义了多个启动模块,可以用于不同silky主机的构成:
    A) DefaultGeneralHostModule 用于构建普通的业务主机,一般用于托管只提供RPC服务的微服务应用;
    B) WebSocketHostModule 用于构建提供WebSocket服务能力的业务主机;
    C) DefaultWebHostModule 用于构建能够提供Http服务的业务主机,对外可以提供http服务,也可以用于内部rpc通信;
    D) DefaultGatewayHostModule 用于构建网关微服务,一般为微服务集群暴露对外部的http访问端口,通过路由机制,将http请求转发到具体某个服务条目,对内通过RPC进行通信;

除此之外,开发者也可以自己的需求,为自己定义需要的启动模块,在构建微服务主机时,指定相应的启动模块。

模块的加载

Silky所有的模块是在什么时候以及如何进行加载和排序的呢?

在之前的构建服务引擎的一章中,我们知道在AddSilkyServices<T>()方法中,我们通过泛型T来指定应用程序的启用模块StartUpModule类型。并构建了模块加载器对象ModuleLoader,并且将模块加载器对象作为服务引擎的LoadModules()方法参数:

public static IEngine AddSilkyServices<T>(this IServiceCollection services, IConfiguration configuration,
            IHostEnvironment hostEnvironment) where T : StartUpModule
{
    var moduleLoader = new ModuleLoader();
    engine.LoadModules<T>(services, moduleLoader);
}

在服务引擎SilkyEngine实现类中,除了实现IEngine接口之外,还需要实现了IModuleContainer接口,IModuleContainer只定义了一个只读属性Modules,要求通过该属性获取所有的模块;在服务引擎中,我们通过模块加载器对象moduleLoader.LoadModules()方法实现对模块的加载与解析,并对属性Modules进行赋值;

internal sealed class SilkyEngine : IEngine, IModuleContainer
{
  // 其他代码略...

  
   public void LoadModules<T>(IServiceCollection services, IModuleLoader moduleLoader)
   where T : StartUpModule
   {
      Modules = moduleLoader.LoadModules(services, typeof(T));
   }
  
   // 实现IModuleContainer定义的属性
   public IReadOnlyList<ISilkyModuleDescriptor> Modules { get; private set; }
}

模块加载器ModuleLoader要求传递两个参数,一个是IServiceCollection的对象services,一个是启动模块StartupModule的的类型typeof(T);下面我们来描述模块加载的过程:

  1. 通过SilkyModuleHelper.FindAllModuleTypes(startupModuleType) 查找到启动模块StartupModule类型依赖的所有模块类型;

  2. 通过反射创建模块的实例,并通过IServiceCollection注册单例的模块实例,并创建模块描述符SilkyModuleDescriptor;

  3. 根据模块的依赖关系对模块进行排序;

模块的依赖关系是通过特性DependsOnAttribute指定的,通过DependsOnAttribute在对模块的类进行标注,就可以解析到各个模块的依赖关系,从而实现通过模块的依赖关系进行排序;

提示
熟悉APB框架的小伙伴应该可以看出来,Silky模块的设计主要是借鉴了APB框架的模块设计,在一些细节方面做了调整。

Silky的核心模块

通过上面的介绍, 我们知道一个模块类的最重要的工作主要由两点: 1. 实现服务的注册; 2. 在应用启动时或是停止时执行指定的方法完成初始化任务或是释放资源的任务;

如何判断是否是silky的核心模块呢? 核心模块最重要的一个作用就是在应用启动时,通过Initialize()方法执行该模块的初始化资源的任务;

通过查看源码,我们发现大部分silky模块在应用启动时并没有重写Initialize()方法,也就是说,大部分silky模块在应用启动过程时主要是完成各个模块的服务类的注册并不需要做什么工作。

Silky微服务框架之模块

如上图所示,我们看到silky框架定义的模块,由如上几个模块是在应用启动是完成了主机启动时的关键性作业;

我们再根据模块的依赖关系,可以看到主机在应用启动时,通过模块初始化任务的一个执行顺序如下所示:

RpcModule --> DotNettyTcpModule | TransactionModule | WebSocketModule | [RpcMonitorModule] 

--> GeneralHostModule(启动模块[StartUpModule])[DefaultGeneralHostModule|WebSocketHostModule|DefaultWebSocketHostModule] 

通过上述的依赖关系,我们可以知道:

  1. Rpc模块在应用启动时是最早被执行;

  2. 然后依次执行: DotNettyTcpModule | TransactionModule | WebSocketModule | [RpcMonitorModule] 等模块;

  3. 最后执行应用启动模块指定的初始化方法;

在上述的过程中,Silky主机在启动时需要完成如下的工作:

  1. 实现Rpc消息监听的订阅;

  2. 解析应用服务与服务条目;

  3. 启动Rpc消息监听;

  4. 解析服务主机和注册该主机实例对应的端点;

  5. 向服务注册中心更新或是获取服务元数据(应用服务描述信息和服务条目描述信息);

  6. 向服务注册中心注册该服务当前实例的端点以及从服务注册中心获取该服务对应的所有实例;

  7. 通过心跳的方式从服务注册中心获取最新的服务元数据以及服务实例信息;

在下面的篇章中,我们将着重介绍上述的过程是如何实现的。

开源地址

在线文档

相关文章