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. 手动集成控制台项目
手动创建一个.net core控制台项目,添加核心nuget包,Volo.Abp
创建一个HelloWorldService服务,使用瞬时生命周期
[Dependency(ServiceLifetime.Transient)] public class HelloWorldService() { public void Run() { Console.WriteLine("Hello World"); } }
ABP是模块化的,所以我们要创建一个模块类 AppModule
public class AppModule : AbpModule { public override void OnApplicationInitialization(ApplicationInitializationContext context) { var service = context.ServiceProvider.GetRequiredService<HelloWorldService>(); service.Run(); } }
最后修改Program启动类
// 根据启动模块,创建ABP应用对象 var application = AbpApplicationFactory.Create<AppModule>(); // 初始化应用对象 application.Initialize();
3. 手动集成MVC项目
创建一个空的web项目,添加核心包,Volo.Abp.AspNetCore.Mvc
创建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,需要在类上添加使用特性标注依赖关系。
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. 模块化生命周期
- 初始化阶段 (ConfigureServices):这个阶段主要在 IServiceCollection 中进行服务的注册和配置。
- PreConfigureServices:这是最早执行的方法,适用于需要在其他模块配置服务之前就进行一些操作的场景,例如重写某个模块的默认服务注册。
- ConfigureServices:这是最核心的服务配置方法。通常会在这里注册依赖注入(DI)服务、配置选项等。
- PostConfigureServices: 所有模块的 ConfigureServices 执行完毕后会调用此方法。适用于需要覆盖或修改其他模块已注册服务的场景。
- 应用初始化阶段 (OnApplicationInitialization):此阶段发生在服务提供商 (IServiceProvider) 构建完成之后,应用程序正式处理请求之前。可以在这里获取到已注册的服务实例。
- OnPreApplicationInitialization:在应用初始化管道之前执行代码。
- OnApplicationInitialization:这是配置中间件管道的主要地方,例如添加认证、授权、MVC等。也可以在这里初始化一些需要启动的任务。
- OnPostApplicationInitialization: 在所有模块的 OnApplicationInitialization 完成之后执行。
- 模块关闭阶段 (OnApplicationShutdown):当应用程序停止时(例如通过 IApplicationLifetime.ApplicationStopping 事件触发),会调用此方法。你应在此进行资源的清理和释放工作,例如关闭数据库连接、取消后台任务等。
除了上述同步方法,ABP vNext 还提供了对应的异步方法(如 ConfigureServicesAsync, OnApplicationInitializationAsync 等),允许在生命周期阶段执行异步操作
6. 插件化模块
- Abp的模块支持插件化,能轻松独立部署成服务,能轻松调用独立服务。
- 原本是一个单体进程,然后拆分独立部署,原项目里面可以像调用本地类一样去该分布式服务,是不是很cooooooool
插件式使用步骤:
- 添加模块
- 编译拷贝
- 指定加载文件夹
- 运行查看
- 支持UI扩展等
注意模块依赖的包,是需要宿主也依赖
https://docs.abp.io/zh-Hans/abp/latest/PlugIn-Modules
7. 三种模块使用方式
- 插件式使用:不建议复杂模块使用插件试,容易失败
- 解决方案引用式:架构师/项目负责人的用法,能看到全部项目。不过也只是临时,因为太大了
- Nuget包:这种是最常用的,公司开发,需要一个私有的nuget服务器
8. 扩展:配置Nuget私服
- 下载BaGet:Git地址:https://github.com/loic-sharma/BaGet
- 解压运行:dotnet BaGet.dll --urls=http://localhost:8020/
- 项目打包:nupkg文件
- 访问:http://localhost:8020/upload
- 上传:VS中配置私服——程序包源
https://loic-sharma.github.io/BaGet/configuration/
https://docs.microsoft.com/zh-cn/nuget/hosting-packages/overview
四、模块化源码解读
1. 模块化源码:上帝视角
- 程序Build组装时,Startup的ConfigureService会加载好全部模块,各种IOC注册
- 程序模块各种依赖,几十上百个模块儿怎么个加载顺序?
- 先找出全部模块,拓扑排序,按顺序加载,挨个儿Pre—Config—Post
- 程序run启动时,Startup的Configure管理Pre-Initalize-Post
- 全局一个IOC容器,映射关系都在这里(命名空间区分好),所以单个进程,多进程工作不影响
2. 模块化任务分解
要完成模块化,我们先把任务分解一下:
- 需要一个IAbpModule的接口,所有继承整这个接口的实现才是一个模块化的。
- 应用启动时,在构建IServiceCollection期间,控制服务的注册、替换、装饰等依赖注入。
- 接口:IPreConfigureServices、IPostConfigureServices
- 特点:操作对象是IServiceCollection(服务描述符集合)。此时应用尚未启动(如中间件管道未构建、Host未运行)。
- 应用的的生命周期管理,依赖注入容器构建完成后,在应用启动或关闭时,执行与运行时相关的初始化或清理逻辑(如中间件配置、后台任务启动、资源释放)。
- 接口:IOnPreApplicationInitialization、IOnApplicationInitialization、IOnPostApplicationInitialization和IOnApplicationShutdown
- 特点:操作对象是已构建的IServiceProvider(可解析服务实例),此时应用已具备运行时环境(如HTTP请求管道、Host服务)。
- 依赖关系的描述,即DependsOn特性和拓扑排序
- 插件的支持
接口 | 阶段 | 执行顺序 | 典型用途 |
---|---|---|---|
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类是应用的入口,它包括下面功能
- 注册核心ABP服务—生命周期管理
- 加载模块
- IOC注册
- 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去完成模块的加载:
- 从startup的模块找到全部依赖+从plugin加载的模块依赖
- 设置依赖:跟据依赖关系完成拓扑排序
- 遍历已排序模块,分别执行configureservice然后再initialize
- 这全部过程,都依赖于一个IOC容器实例,保证进程的全局统一
然后呢,多个模块就组合起来了