Spiga

ABP成长系列2:模块化

2024-04-20 12:05:23

在讲ABP的模块实现前,我们先来认识一下ABP。

一、初识ABP

1. ABP vNext是什么

  • ASP.NET Core的开源WEB应用程序框架——就是再封装一层,扩展了一系列的封装,完成了很多通用的。
  • ABP是用于创建现代Web应用程序的完整架构和强大的基础设施! 遵循最佳实践和约定,为你提供SOLID开发经验
  • https://www.abp.io/
  • https://github.com/abpframework/abp

2. 架构特点

  • 应用程序模块化
  • 领域驱动设计DDD
  • 微服务架构
  • 支持仓储,基于O/RM实现数据库无关性和MongoDB集成
  • 可扩展及可替换

3. 基础设施

  • 领域驱动设计
  • 模块化设计
  • 多租户
  • Dependency Injection
  • 认证与授权
  • 事件总线
  • 数据访问
  • Auto API及动态代理

4. 其他组件

  • 横切面关注
  • 虚拟文件系统
  • 数据过滤
  • 本地化
  • 异常处理
  • 审计日志

二、开启ABP.vNext

1. 使用 ABP CLI 工具直接生成

超多内容,请自行查看。https://docs.abp.io/zh-Hans/abp/latest/CLI

也可以在 https://abp.io 直接生成

可以查看基础项目 BookStore,账号 admin,密码 1q2w3E*

自动化生成一次性生成了太多代码,不适合学习,接下来我们要研究一下手动集成

2. 手动集成控制台项目

  1. 手动创建一个.net core控制台项目,添加核心nuget包,Volo.Abp

  2. 创建一个HelloWorldService服务,使用瞬时生命周期

    [Dependency(ServiceLifetime.Transient)]
    public class HelloWorldService() 
    {
        public void Run()
        {
            Console.WriteLine("Hello World");
        }
    }
    
  3. ABP是模块化的,所以我们要创建一个模块类 AppModule

    public class AppModule : AbpModule
    {
        public override void OnApplicationInitialization(ApplicationInitializationContext context)
        {
            var service = context.ServiceProvider.GetRequiredService<HelloWorldService>();
            service.Run();
        }
    }
    
  4. 最后修改Program启动类

    // 根据启动模块,创建ABP应用对象
    var application = AbpApplicationFactory.Create<AppModule>();
    
    // 初始化应用对象
    application.Initialize();
    

3. 手动集成MVC项目

  1. 创建一个空的web项目,添加核心包,Volo.Abp.AspNetCore.Mvc

  2. 创建WebAppModule

    [DependsOn(typeof(AbpAspNetCoreMvcModule))]
    public class WebAppModule : AbpModule
    {
        public override void OnApplicationInitialization(ApplicationInitializationContext context)
        {
            var app = context.GetApplicationBuilder();
            var env = context.GetEnvironment();
    
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            // 添加路由中间件
            app.UseRouting();
    
            app.UseConfiguredEndpoints();
        }
    }
    

​ 与控制台项目不同的是,WebAppModule依赖AbpAspNetCoreMvcModule,需要在类上添加使用特性标注依赖关系。

  1. Program类

    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddApplication<WebAppModule>();
    
    var app = builder.Build();
    
    app.InitializeApplication();
    
    app.Run();
    

三、模块化

1. 定义

官方定义: ABP本身是一个包含许多nuget包的模块化框架。

它还提供了一个完整的基础架构来开发你自己的具有实体, 服务, 数据库集成, API, UI组件等等功能的应用程序模块。

不太好懂?顾名思义,把一个大项目,能拆解成N个小的独立模块(分治)---独立开发、独立部署、然后还能随意拼装---像乐高积木。

ABP vNext开发,实际上都是在做模块

吹一下模块化

  • 分治:分而治之,项目拆成低耦合的模块,独立开发,互不干扰
  • 复用:像积木一样去复用基础能力,包括了架构能力和应用能力

2. 理解模块化

  • 零件——class(最小)
  • 组件——component(较小),软件的最小部署单元dll,类库
  • 模块——module(更大),具有独立命名空间,可独立开发、部署和测试, 具备和其他模块组装的能力,比如用户管理模块、租户模块等,在Abp vNext当初,一个模块就是一个项目。
  • 微服务——microservice(最大),一个完成独立业务的功能体

3.模块化的意义

  • 方便分工:希望能像积木一样复用我们的基础能力,不管是架构能力还是 应用能力,避免重复造轮子。

  • 应用能力复用:业务功能模块

  • 架构能力复用:通用类库模块(是真的超牛!)

  • 模块跟dll的区别:dll属于部件,需要引用再调用才使用 模块是一个独立的个体,引入即可

4. 配置模块化

核心有三件事儿:

  • 模块依赖:DependOn
  • 配置DI:ConfigureService
  • 初始化:Intialization

ABP vNext 通过 [DependsOn] 特性来管理模块间的依赖关系。框架会使用拓扑排序算法树形结构后序遍历算法来分析这些依赖,确保所有模块按照正确的顺序加载和初始化——被依赖的模块总是先于依赖它的模块执行。

例如,若模块A [DependsOn] 了模块B和模块C,那么框架保证模块B和模块C的 ConfigureServices 和 OnApplicationInitialization 方法会先于模块A的相应方法执行

5. 模块化生命周期

  1. 初始化阶段 (ConfigureServices):这个阶段主要在 IServiceCollection 中进行服务的注册和配置。
    • PreConfigureServices:这是最早执行的方法,适用于需要在其他模块配置服务之前就进行一些操作的场景,例如重写某个模块的默认服务注册。
    • ConfigureServices:这是最核心的服务配置方法。通常会在这里注册依赖注入(DI)服务、配置选项等。
    • PostConfigureServices: 所有模块的 ConfigureServices 执行完毕后会调用此方法。适用于需要覆盖修改其他模块已注册服务的场景。
  2. 应用初始化阶段 (OnApplicationInitialization):此阶段发生在服务提供商 (IServiceProvider) 构建完成之后,应用程序正式处理请求之前。可以在这里获取到已注册的服务实例。
    • OnPreApplicationInitialization:在应用初始化管道之前执行代码。
    • OnApplicationInitialization:这是配置中间件管道的主要地方,例如添加认证、授权、MVC等。也可以在这里初始化一些需要启动的任务。
    • OnPostApplicationInitialization: 在所有模块的 OnApplicationInitialization 完成之后执行。
  3. 模块关闭阶段 (OnApplicationShutdown):当应用程序停止时(例如通过 IApplicationLifetime.ApplicationStopping 事件触发),会调用此方法。你应在此进行资源的清理和释放工作,例如关闭数据库连接、取消后台任务等。

除了上述同步方法,ABP vNext 还提供了对应的异步方法(如 ConfigureServicesAsync, OnApplicationInitializationAsync 等),允许在生命周期阶段执行异步操作

6. 插件化模块

  • Abp的模块支持插件化,能轻松独立部署成服务,能轻松调用独立服务。
  • 原本是一个单体进程,然后拆分独立部署,原项目里面可以像调用本地类一样去该分布式服务,是不是很cooooooool

插件式使用步骤:

  1. 添加模块
  2. 编译拷贝
  3. 指定加载文件夹
  4. 运行查看
  5. 支持UI扩展等

注意模块依赖的包,是需要宿主也依赖

https://docs.abp.io/zh-Hans/abp/latest/PlugIn-Modules

7. 三种模块使用方式

  • 插件式使用:不建议复杂模块使用插件试,容易失败
  • 解决方案引用式:架构师/项目负责人的用法,能看到全部项目。不过也只是临时,因为太大了
  • Nuget包:这种是最常用的,公司开发,需要一个私有的nuget服务器

8. 扩展:配置Nuget私服

  1. 下载BaGet:Git地址:https://github.com/loic-sharma/BaGet
  2. 解压运行:dotnet BaGet.dll --urls=http://localhost:8020/
  3. 项目打包:nupkg文件
  4. 访问:http://localhost:8020/upload
  5. 上传:VS中配置私服——程序包源

https://loic-sharma.github.io/BaGet/configuration/

https://docs.microsoft.com/zh-cn/nuget/hosting-packages/overview

四、模块化源码解读

1. 模块化源码:上帝视角

  1. 程序Build组装时,Startup的ConfigureService会加载好全部模块,各种IOC注册
  2. 程序模块各种依赖,几十上百个模块儿怎么个加载顺序?
    • 先找出全部模块,拓扑排序,按顺序加载,挨个儿Pre—Config—Post
    • 程序run启动时,Startup的Configure管理Pre-Initalize-Post
  3. 全局一个IOC容器,映射关系都在这里(命名空间区分好),所以单个进程,多进程工作不影响

2. 模块化任务分解

要完成模块化,我们先把任务分解一下:

  1. 需要一个IAbpModule的接口,所有继承整这个接口的实现才是一个模块化的。
  2. 应用启动时,在构建IServiceCollection期间,控制服务的注册、替换、装饰等依赖注入。
    • 接口:IPreConfigureServices、IPostConfigureServices
    • 特点:操作对象是IServiceCollection(服务描述符集合)。此时应用尚未启动(如中间件管道未构建、Host未运行)。
  3. 应用的的生命周期管理,依赖注入容器构建完成后,在应用启动或关闭时,执行与运行时相关的初始化或清理逻辑(如中间件配置、后台任务启动、资源释放)。
    • 接口:IOnPreApplicationInitialization、IOnApplicationInitialization、IOnPostApplicationInitialization和IOnApplicationShutdown
    • 特点:操作对象是已构建的IServiceProvider(可解析服务实例),此时应用已具备运行时环境(如HTTP请求管道、Host服务)。
  4. 依赖关系的描述,即DependsOn特性和拓扑排序
  5. 插件的支持
接口 阶段 执行顺序 典型用途
IPreConfigureServices DI配置前 依赖顺序 覆盖默认服务注册、预配置选项
IPostConfigureServices DI配置后 依赖逆序 装饰服务、验证服务集合
IOnPreApplicationInitialization 应用启动前(Host启动时) 依赖顺序 初始化中间件、预热缓存
IOnApplicationInitialization 应用启动中 依赖顺序 主初始化逻辑(如种子数据)
IOnPostApplicationInitialization 应用启动后 依赖逆序 启动后台服务、触发事件
IOnApplicationShutdown 应用关闭时 依赖逆序 释放资源、保存状态

3. 模块基础设施关系图

  • AbpModule:模块的默认实现,它实现了
    • IAbpModule
    • IOnPreApplicationInitialization,IOnApplicationInitialization,IOnPostApplicationInitialization,IOnApplicationShutdown
    • IPreConfigureServices,IPostConfigureServices
  • ModuleLoader:模块加载器,完成获取全部的module,并且按依赖顺序加载模块
  • ModuleManager:模块管理者,用来初始化模块和卸载模块。通过内部对象_lifecycleContributors来完成

4. 核心1:AbpApplicationBase类

AbpApplicationBase类是应用的入口,它包括下面功能

  1. 注册核心ABP服务—生命周期管理
  2. 加载模块
  3. IOC注册
  4. LoadModules—加载全部module
internal AbpApplicationBase(
    [NotNull] Type startupModuleType,
    [NotNull] IServiceCollection services,
    [CanBeNull] Action<AbpApplicationCreationOptions> optionsAction)
{
    Check.NotNull(startupModuleType, nameof(startupModuleType));
    Check.NotNull(services, nameof(services));

    StartupModuleType = startupModuleType;//起始module--BookStoreWebModule
    Services = services;//全局的IOC容器

    services.TryAddObjectAccessor<IServiceProvider>();

    var options = new AbpApplicationCreationOptions(services);
    optionsAction?.Invoke(options);

    services.AddSingleton<IAbpApplication>(this);
    services.AddSingleton<IModuleContainer>(this);

    services.AddCoreServices();//基本服务注册
    services.AddCoreAbpServices(this, options);//一堆IOC+Initialization*3+Shutdown

    Modules = LoadModules(services, options);//查找模块--排序

    if (!options.SkipConfigureServices)
    {
        ConfigureServices();//配置服务
    }
}

4. 核心2:AbpApplicationFactory

我们留意AbpApplicationBase构造方法的第二个参数 IServiceCollection,这个对象的来源有两处

  • 外部提供:我们知道ASP.NET项目默认是提供的DI支持的,项目启动后就有一个IServiceCollection对象的,在ASP.NET项目中直接传入这个对象即可。
  • 内部创建:而如控制台项目,默认就没有DI支持了,这时就需要Abp内部提供一个new一个IServiceCollection的对象

Abp提供了一个工厂方法用来创建启动模块,我们来看一下关系图

public static class AbpApplicationFactory
{
    public static IAbpApplicationWithInternalServiceProvider Create<TStartupModule>(
        [CanBeNull] Action<AbpApplicationCreationOptions> optionsAction = null)
        where TStartupModule : IAbpModule
    {
        return Create(typeof(TStartupModule), optionsAction);
    }

    public static IAbpApplicationWithInternalServiceProvider Create(
        [NotNull] Type startupModuleType,
        [CanBeNull] Action<AbpApplicationCreationOptions> optionsAction = null)
    {
        return new AbpApplicationWithInternalServiceProvider(startupModuleType, optionsAction);
    }

    public static IAbpApplicationWithExternalServiceProvider Create<TStartupModule>(
        [NotNull] IServiceCollection services,
        [CanBeNull] Action<AbpApplicationCreationOptions> optionsAction = null)
        where TStartupModule : IAbpModule
    {
        return Create(typeof(TStartupModule), services, optionsAction);
    }

    public static IAbpApplicationWithExternalServiceProvider Create(
        [NotNull] Type startupModuleType,
        [NotNull] IServiceCollection services,
        [CanBeNull] Action<AbpApplicationCreationOptions> optionsAction = null)
    {
        return new AbpApplicationWithExternalServiceProvider(startupModuleType, services, optionsAction);
    }
}

internal class AbpApplicationWithInternalServiceProvider : AbpApplicationBase, IAbpApplicationWithInternalServiceProvider
{
    public IServiceScope ServiceScope { get; private set; }

    public AbpApplicationWithInternalServiceProvider(
        [NotNull] Type startupModuleType,
        [CanBeNull] Action<AbpApplicationCreationOptions> optionsAction
        ) : this(
        startupModuleType,
        new ServiceCollection(),	//内部直接new一个ServiceCollection
        optionsAction)
    {

    }
}

关于Abp的DI部分我们在后面分析。

5. 核心3:ModuleLoader类

LoadModules:模块加载器,完成2件事儿:

  • 找到全部模块,总结好依赖信息——从Startup模块开始,AbpModuleHelper去查找——递归查找,依赖,放入合集,然后再找插件的全部依赖模块,然后把依赖也整理清楚
public IAbpModuleDescriptor[] LoadModules(
    IServiceCollection services,
    Type startupModuleType,
    PlugInSourceList plugInSources)
{
    Check.NotNull(services, nameof(services));
    Check.NotNull(startupModuleType, nameof(startupModuleType));
    Check.NotNull(plugInSources, nameof(plugInSources));

    //获取全部的module,搞个集合
    var modules = GetDescriptors(services, startupModuleType, plugInSources);
    //按照依赖排序---拓扑排序
    modules = SortByDependency(modules, startupModuleType);

    return modules.ToArray();
}
  • 拓扑排序
protected virtual List<IAbpModuleDescriptor> SortByDependency(List<IAbpModuleDescriptor> modules, Type startupModuleType)
{
    var sortedModules = modules.SortByDependencies(m => m.Dependencies);
    sortedModules.MoveItem(m => m.Type == startupModuleType, modules.Count - 1);
    return sortedModules;
}

拓扑排序的源码这里不贴了,放一张图理解一下拓扑排序的顺序。

6. 核心4:ModuleManager类

  • 接口IModuleLifecycleContributor,定义了Initialize和Shutdown方法约束
  • 抽象类ModuleLifecycleContributorBase,提供默认的空的Initialize和Shutdown实现
  • DefaultModuleLifecycleContributor,遍历加载的模块,执行注册的方法
    • services.AddCoreAbpServices(this, options); 完成Option注册 AbpModuleLifecycleOptions
    • Startup的Configure==>InitializeApplication——最终是 AbpApplicationBase的InitializeModules——找到ModuleManager==> InitializeModules和ShutdownModules
    • 顺序是OnPreApp1234==>OnApp1234==>OnPostApp1234==>Shutdown
public class OnApplicationInitializationModuleLifecycleContributor : ModuleLifecycleContributorBase
{
    public override void Initialize(ApplicationInitializationContext context, IAbpModule module)
    {
        (module as IOnApplicationInitialization)?.OnApplicationInitialization(context);
    }
}

public class OnApplicationShutdownModuleLifecycleContributor : ModuleLifecycleContributorBase
{
    public override void Shutdown(ApplicationShutdownContext context, IAbpModule module)
    {
        (module as IOnApplicationShutdown)?.OnApplicationShutdown(context);
    }
}

public class OnPreApplicationInitializationModuleLifecycleContributor : ModuleLifecycleContributorBase
{
    public override void Initialize(ApplicationInitializationContext context, IAbpModule module)
    {
        (module as IOnPreApplicationInitialization)?.OnPreApplicationInitialization(context);
    }
}

public class OnPostApplicationInitializationModuleLifecycleContributor : ModuleLifecycleContributorBase
{
    public override void Initialize(ApplicationInitializationContext context, IAbpModule module)
    {
        (module as IOnPostApplicationInitialization)?.OnPostApplicationInitialization(context);
    }
}
  • IModuleLifecycleContributor对象最终组装在ModuleManager对象中,由AbpApplicationBase对象调用
public virtual void Shutdown()
{
    using (var scope = ServiceProvider.CreateScope())
    {
        scope.ServiceProvider
            .GetRequiredService<IModuleManager>()
            .ShutdownModules(new ApplicationShutdownContext(scope.ServiceProvider));
    }
}

protected virtual void InitializeModules()
{
    using (var scope = ServiceProvider.CreateScope())
    {
        WriteInitLogs(scope.ServiceProvider);
        scope.ServiceProvider
            .GetRequiredService<IModuleManager>()
            .InitializeModules(new ApplicationInitializationContext(scope.ServiceProvider));
    }
}

7. 核心5:ConfigureService方法

ConfigureService方法在AbpApplicationBase,遍历全部模块儿(已排序),挨个儿执行。都是放入同一 个IOC容器,所以可以在一个进程随意使用任意模块的东西

  • PreConfigureServices
  • ConfigureServices
  • PostConfigureServices
//TODO: We can extract a new class for this
public virtual void ConfigureServices()
{
    CheckMultipleConfigureServices();
        
    var context = new ServiceConfigurationContext(Services);
    Services.AddSingleton(context);
    //全部模块使用同个IOC容器
    foreach (var module in Modules)
    {
        if (module.Instance is AbpModule abpModule)
        {
            abpModule.ServiceConfigurationContext = context;
        }
    }

    //PreConfigureServices--先把所有的模块按顺序去执行PreConfigureServices
    foreach (var module in Modules.Where(m => m.Instance is IPreConfigureServices))
    {
        try
        {
            ((IPreConfigureServices)module.Instance).PreConfigureServices(context);
        }
        catch (Exception ex)
        {
            throw new AbpInitializationException($"An error occurred during {nameof(IPreConfigureServices.PreConfigureServices)} phase of the module {module.Type.AssemblyQualifiedName}. See the inner exception for details.", ex);
        }
    }

    var assemblies = new HashSet<Assembly>();

    //ConfigureServices--先把所有的模块按顺序去执行ConfigureServices
    foreach (var module in Modules)
    {
        if (module.Instance is AbpModule abpModule)
        {
            if (!abpModule.SkipAutoServiceRegistration)
            {
                var assembly = module.Type.Assembly;
                if (!assemblies.Contains(assembly))
                {
                    Services.AddAssembly(assembly);
                    assemblies.Add(assembly);
                }
            }
        }

        try
        {
            module.Instance.ConfigureServices(context);
        }
        catch (Exception ex)
        {
            throw new AbpInitializationException($"An error occurred during {nameof(IAbpModule.ConfigureServices)} phase of the module {module.Type.AssemblyQualifiedName}. See the inner exception for details.", ex);
        }
    }

    //PostConfigureServices---先把所有的模块按顺序去执行PostConfigureServices
    foreach (var module in Modules.Where(m => m.Instance is IPostConfigureServices))
    {
        try
        {
            ((IPostConfigureServices)module.Instance).PostConfigureServices(context);
        }
        catch (Exception ex)
        {
            throw new AbpInitializationException($"An error occurred during {nameof(IPostConfigureServices.PostConfigureServices)} phase of the module {module.Type.AssemblyQualifiedName}. See the inner exception for details.", ex);
        }
    }

    foreach (var module in Modules)
    {
        if (module.Instance is AbpModule abpModule)
        {
            abpModule.ServiceConfigurationContext = null;
        }
    }

    _configuredServices = true;
}

注意跟俄罗斯套娃的区别

  • ConfigureServices的流程1234-1234-1234
  • 套娃的流程1234-AAA-4321

8. 核心6:插件的支持

在ModuleLoader类的LoadModules方法的第三个参数实现插件的支持

public IAbpModuleDescriptor[] LoadModules(
    IServiceCollection services,
    Type startupModuleType,
    PlugInSourceList plugInSources)
{
    Check.NotNull(services, nameof(services));
    Check.NotNull(startupModuleType, nameof(startupModuleType));
    Check.NotNull(plugInSources, nameof(plugInSources));

    //获取全部的module,搞个集合
    var modules = GetDescriptors(services, startupModuleType, plugInSources);
    //按照依赖排序---拓扑排序
    modules = SortByDependency(modules, startupModuleType);

    return modules.ToArray();
}

private List<IAbpModuleDescriptor> GetDescriptors(
    IServiceCollection services,
    Type startupModuleType,
    PlugInSourceList plugInSources)
{
    var modules = new List<AbpModuleDescriptor>();

    FillModules(modules, services, startupModuleType, plugInSources);//把起始模块+插件模块关联全部的模块儿都找到--放入modules
    SetDependencies(modules);//设置依赖关系--把全部的module都设置一遍

    return modules.Cast<IAbpModuleDescriptor>().ToList();
}

/// <summary>
/// 把起始模块+插件模块关联全部的模块儿都找到--放入modules
/// </summary>
/// <param name="modules"></param>
/// <param name="services"></param>
/// <param name="startupModuleType"></param>
/// <param name="plugInSources"></param>
protected virtual void FillModules(
    List<AbpModuleDescriptor> modules,
    IServiceCollection services,
    Type startupModuleType,
    PlugInSourceList plugInSources)
{
    var logger = services.GetInitLogger<AbpApplicationBase>();

    //All modules starting from the startup module
    foreach (var moduleType in AbpModuleHelper.FindAllModuleTypes(startupModuleType, logger))
    {
        modules.Add(CreateModuleDescriptor(services, moduleType));
    }

    //Plugin modules--反射扫描插件,找出全部的Module类--然后在遍历查找全部依赖
    foreach (var moduleType in plugInSources.GetAllModules(logger))
    {
        if (modules.Any(m => m.Type == moduleType))
        {
            continue;
        }

        modules.Add(CreateModuleDescriptor(services, moduleType, isLoadedAsPlugIn: true));
    }
}

Abp支持3中方式加载插件,源码看Volo.Abp.Modularity.PlugIns命名空间下的代码

  • 文件名
  • 目录
  • 内存中类型

9. 总结

ASP.NET Core启动==>ConfigureService去完成模块的加载:

  1. 从startup的模块找到全部依赖+从plugin加载的模块依赖
  2. 设置依赖:跟据依赖关系完成拓扑排序
  3. 遍历已排序模块,分别执行configureservice然后再initialize
  4. 这全部过程,都依赖于一个IOC容器实例,保证进程的全局统一

然后呢,多个模块就组合起来了