ABP 模块系统源码学习:启动时模块是如何加载和排序的

从入口看起:AbpApplicationFactory.Create

csharp 复制代码
using var app = AbpApplicationFactory.Create<BusinessModule>(options =>
{
    options.UseAutofac();
});
app.Initialize();

这行代码的背后发生了什么?直接看源码 AbpApplicationFactory.cs

csharp 复制代码
public static IAbpApplicationWithInternalServiceProvider Create<TStartupModule>(
    Action<AbpApplicationCreationOptions>? optionsAction = null)
{
    return new AbpApplicationWithInternalServiceProvider(typeof(TStartupModule), optionsAction);
}

它创建了 AbpApplicationWithInternalServiceProvider,这个类在构造时做了两件关键的事:

  1. 创建 ModuleLoader 并调用 LoadModules 加载所有模块
  2. 按拓扑排序确定模块初始化顺序

深度解析 ModuleLoader.LoadModules

源码位于 framework/src/Volo.Abp.Core/Volo/Abp/Modularity/ModuleLoader.cs,核心逻辑只有三行:

csharp 复制代码
public IAbpModuleDescriptor[] LoadModules(IServiceCollection services, Type startupModuleType, PlugInSourceList plugInSources)
{
    var modules = GetDescriptors(services, startupModuleType, plugInSources);  // 1. 发现所有模块
    modules = SortByDependency(modules, startupModuleType);                     // 2. 拓扑排序
    return modules.ToArray();                                                    // 3. 返回有序列表
}

第一步:递归发现

GetDescriptors 内部分两步:

FillModules ------从入口模块开始,递归查找所有 [DependsOn] 依赖:

csharp 复制代码
// AbpModuleHelper.cs
private static void AddModuleAndDependenciesRecursively(List<Type> moduleTypes, Type moduleType, ...)
{
    moduleTypes.Add(moduleType);
    foreach (var dependedModuleType in FindDependedModuleTypes(moduleType))
    {
        AddModuleAndDependenciesRecursively(moduleTypes, dependedModuleType, ...);
    }
}

FindDependedModuleTypes 通过反射读取当前类上的所有 [DependsOn] 属性:

csharp 复制代码
public static List<Type> FindDependedModuleTypes(Type moduleType)
{
    var dependencyDescriptors = moduleType.GetCustomAttributes().OfType<IDependedTypesProvider>();
    foreach (var descriptor in dependencyDescriptors)
    {
        dependencies.AddIfNotContains(descriptor.GetDependedTypes());
    }
}

这意味着不仅 [DependsOn] 会被识别,任何实现了 IDependedTypesProvider 的自定义属性都能用来声明依赖。

第二步:拓扑排序

csharp 复制代码
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;
}

这里使用了 SortByDependencies 扩展方法(来自 Volo.Abp.Core 的工具类),是一个标准的拓扑排序实现。排序后,入口模块被移到最后一位,保证它最后初始化。

如果在模块依赖图中存在循环依赖,拓扑排序会抛出异常------这在启动时就能发现,不会让问题带到运行时。

第三步:创建模块实例

csharp 复制代码
protected virtual IAbpModule CreateAndRegisterModule(IServiceCollection services, Type moduleType)
{
    var module = (IAbpModule)Activator.CreateInstance(moduleType)!;
    services.AddSingleton(moduleType, module);  // 每个模块以 Singleton 注册到 DI
    return module;
}

每个模块实例被注册为 Singleton,这意味着模块在整个应用生命周期中只创建一次。

生命周期钩子的调用顺序

模块加载完毕后调用 Initialize,进入生命周期阶段。在 AbpApplicationBase.cs 中:

复制代码
阶段 1:ConfigureServices
  for each module in sortedModules:
    module.Instance.PreConfigureServices(context)
  for each module in sortedModules:
    module.Instance.ConfigureServices(context)  
  for each module in sortedModules:
    module.Instance.PostConfigureServices(context)

阶段 2:构建 IServiceProvider

阶段 3:OnApplicationInitialization  
  for each module in sortedModules:
    module.Instance.OnPreApplicationInitialization(context)
    module.Instance.OnApplicationInitialization(context)
    module.Instance.OnPostApplicationInitialization(context)

注意:三个阶段都按排序后的顺序执行,入口模块永远在最后。

实战要点

1. 模块不可循环依赖

如果模块 A 依赖 B,B 又依赖 A,启动时直接抛出 AbpException。这比运行时发现要好得多。

2. AdditionalAssembly 的用途

除了 [DependsOn],还有 [AdditionalAssembly] 属性。它不声明依赖,只是告诉框架"这个模块的程序集也加载进来"。用于解决模块类在另一个程序集中的场景。

3. 模块实例是 Singleton

模块中定义的状态在应用生命周期内保持。不要在模块中存储请求级别的数据。

4. PreConfigureServices 的应用

PreConfigureServices 可以用于设置默认选项,让其他模块在 ConfigureServices 中可以覆盖:

csharp 复制代码
public override void PreConfigureServices(ServiceConfigurationContext context)
{
    // 设置默认值,允许下游模块覆盖
    context.Services.AddSingleton(new DefaultOptions { ... });
}

验证结果

运行练习示例,输出清晰展示了顺序:

复制代码
[LoggingModule] ConfigureServices          ← 被依赖的模块先执行
[DataAccessModule] ConfigureServices
[BusinessModule] ConfigureServices         ← 入口模块最后执行

即使打乱 [DependsOn] 的书写顺序,输出顺序不变。原因就是 SortByDependencies 的拓扑排序保证了依赖顺序,和代码中的书写顺序无关。

相关推荐
5系暗夜孤魂z5 小时前
优雅的.net REST API之FastEndpoints
log4j·.net
雾里0不看花6 小时前
.NET通过HTTP操作MINIO
http·.net·iphone
不在逃避q8 小时前
使用.NET实现自带思考的Tool 并且提供mcp streamable http服务
网络协议·http·.net
界面开发小八哥20 小时前
界面控件DevExpress Blazor v26.1新版亮点 - 辅助功能增强
.net·界面控件·blazor·devexpress·ui开发
「KISSSHOT」1 天前
抽象与性能:从 LINQ 看现代 .NET 的优化之道
java·.net·linq
比卡超哥1 天前
记一次 .NET 某光谱检测软件 内存暴涨分析
.net
驱动小百科1 天前
Windows运行库合集下载 VC++、DirectX、.NET运行库安装教程
c++·windows·.net·windows运行库合集下载·windows运行库安装
你有哈莫吗1 天前
记一次 .NET 某自动化智能制造软件 卡死分析
自动化·.net·制造