程序集扫描与启动治理(Aegis.Core.Infrastructure)
从 3.1.4 起,AegisApplication.Run(args) 启动时只做一次程序集扫描,扫谁由白名单决定:解决方案里的业务项目,加上 Aegis 自家的包,再加上依赖了 Aegis 的 NuGet 包。第三方库不再进入扫描面,加载失败和版本冲突会直接打在启动横幅上。绝大多数项目一行配置都不用写;扫不到的场景用 Aegis:AssemblyScan 的 Include 兜底。
组件概览
| 字段 | 说明 |
|---|---|
| 功能名称 | 程序集扫描与启动治理 |
| 真实类库 | Aegis.Core.Infrastructure(启动核心包,随核心包发布,不需要单独安装) |
| 功能定位 | 决定框架启动时"扫哪些程序集找类型",并提供扫描结果给全部组件消费 |
| 引入方式 | 使用 AegisApplication.Run(args) 即有,配置节 Aegis:AssemblyScan 可省 |
| 关键配置 | appsettings.json 的 Aegis:AssemblyScan 节 |
| 关键能力 | 白名单一次扫描、Include/Exclude 调节、加载失败与版本冲突告警、启动耗时画像、IAegisAssemblyProvider 统一出口 |
什么时候要用它
先说不用动手的情况,这些默认就覆盖了:
- 业务项目写在解决方案里(
Component.deps.json里Type为project的库) - 装了任意
Aegis.*的 NuGet 包 - 自家 NuGet 业务包里实现了 Aegis 的接口(
ComponentRegister、IMessageConsumer、[Lifetime]等),只要包引用了任何 Aegis 程序集,框架沿依赖图反向找得到,零配置接入
需要写配置的情况只有两类:
- 运行时才加载的插件 DLL:不被项目引用、直接投放到输出目录,且名字不带
Aegis.前缀 - 包不依赖 Aegis、但里面有需要被发现的类型,比如只引用了微软 SignalR、没引用
Aegis.Net.SignalR的 Hub 包
这两种都用 Include 追加。反过来,想把某个包从扫描面里踢出去(比如演示环境的 Aegis.Scheduler.Stub),用 Exclude。
默认扫描面:四路候选
| 候选来源 | 怎么判定 | 覆盖什么 |
|---|---|---|
| 业务项目 | 依赖清单里 Type == "project" 的库 | 解决方案内的 Contract、Services、Repository 等项目 |
| Aegis 前缀包 | 库名或程序集名以 Aegis. 开头 | 全部 Aegis NuGet 包 |
| 反向可达的包 | 依赖图上直接或间接引用了任意 Aegis.* 库的 NuGet 包 | 自家业务扩展包——实现了 Aegis 接口的包必然引用 Aegis 程序集,反向可达即纳入 |
| 磁盘 Aegis DLL | 输出目录下文件名匹配 Aegis.*.dll | drop-in 插件:不进依赖清单、直接拷进输出目录的 Aegis 前缀 DLL |
四个来源取并集,再套用你配置的 Include / Exclude。第三方库(FreeSql、CAP、RabbitMQ.Client 这些)默认不进扫描面——框架要找的类型(组件注册器、启动器、消费者、Hub)只可能定义在前四路里,遍历它们是纯浪费。BaseDemo 实测:白名单 43 个程序集,扫描耗时 46ms,13 个组件全部注册成功。
回退路说明:单文件发布等场景下应用的 deps.json 可能缺失(DependencyContext 为 null),此时自动回退为磁盘全量扫描减去内置运行库前缀黑名单(Microsoft.、System.、mscorlib、netstandard、WindowsBase、Netty.、Autofac.、Accessibility,字面量前缀匹配),Include / Exclude 语义不变。
配置项说明
配置节是 Aegis:AssemblyScan,两个字段都可省略:
| 字段 | 类型 | 作用 |
|---|---|---|
Include | string[] | 兜底追加进扫描面。给插件 DLL、不依赖 Aegis 但含需发现类型的包用 |
Exclude | string[] | 从扫描面裁剪,优先级最高——最后执行,连 Include 加进来的也能裁掉 |
规则写法两种,程序集名匹配不区分大小写:
- 精确名:
Aegis.Scheduler.Stub - 前缀通配:
Legacy.*,命中Legacy.开头的全部程序集,也命中叫Legacy的本体(与 NuGet 语义一致)
{
"Aegis": {
"AssemblyScan": {
"Include": [
"ThirdParty.Plugins.*",
"Company.HubPackage"
],
"Exclude": [
"Aegis.Scheduler.Stub",
"Legacy.*"
]
}
}
}
上面这段的意思:把 ThirdParty.Plugins 一族和 Company.HubPackage 追加进扫描面(它们原本不在四路候选里);再把 Aegis.Scheduler.Stub 和 Legacy 一族裁出去(即便它们命中了候选规则)。
Component.deps.json 和 appsettings.json 都加载完之后框架才开始扫描,所以 Aegis:AssemblyScan 写在 appsettings 或组件依赖文件里都行。3.1.4 之前扫描发生在配置加载前,配置根本来不及生效,这一版调整了顺序。
启动横幅怎么看
启动时控制台会多出几段输出。扫描统计一行:
程序集扫描
[INFO] 已扫描程序集 43 个 · 类型 2531 项 · 耗时 46ms(Aegis.* + 业务项目白名单,Aegis:AssemblyScan 可调)
有加载失败或版本冲突时逐条 Warn,没有就不刷屏:
[WARN] 程序集加载失败 Company.Plugin.dll: 文件不存在且默认上下文无法解析
[WARN] 程序集 Aegis.Foo 部分类型加载失败,已跳过失败类型(首因: Could not load type 'X' from assembly 'Y')
[WARN] [版本冲突:ReferenceMismatch] Aegis.Core.Infrastructure: Aegis.Webapi.Services 编译时引用 Aegis.Core.Infrastructure 3.1.0,实际加载 3.1.4-preview1——已被统一为加载版本,若出现 MissingMethodException / TypeLoadException 优先核对此条
程序集加载失败在 3.1.4 之前是静默吞掉的——DLL 加载不了,类型直接从结果里消失,表现就是"有时扫不到"。现在每一条失败都有名有姓。类型枚举降级(ReflectionTypeLoadException)同理:能加载的类型照常参与,失败的类型跳过并记下首因。
应用启动完成后还有一段耗时画像:
[INFO] 启动耗时: 配置与扫描 61ms · 服务注册 198ms · 宿主构建与中间件 342ms · Initializers 9ms
[INFO] 最慢组件: Diagnostics (96ms), Logging (41ms), EventBus (23ms)
四个阶段对应 Run 的固定时序:配置与扫描、服务注册、宿主构建与中间件、Initializers。最慢 Top3 组件按注册耗时排序(同组件的 Service 和 Middleware 两阶段取更慢的那个)。启动慢时先看这段,框架侧扫描只占几十毫秒,大头通常在组件内部的连接和预热。如果注册期有组件日志,还会追加一行留存条数提示。
版本冲突告警
只对 Aegis.* 程序集报警,第三方库的同名版本统一是 .NET 默认行为,报了也是噪音。两类:
| 类型 | 含义 | 建议动作 |
|---|---|---|
ReferenceMismatch | 某程序集编译时引用的 Aegis 版本与实际加载的不一致(NuGet 已统一到加载版本) | 不一定要改。运行正常可忽略;出现 MissingMethodException / TypeLoadException 时优先核对该条 |
ReferenceSplit | 多个程序集对同一个 Aegis 包要求不同版本,即使与加载版本一致也提示 | 建议把各项目引用的 Aegis 包版本对齐,分裂是隐患 |
ReferenceSplit 的输出形态长这样(哪个版本被谁引用一目了然):
[WARN] [版本冲突:ReferenceSplit] Aegis.EventBus: 3.1.0 ← Aegis.Webapi.Base,Aegis.Webapi.Contract; 3.1.1 ← Aegis.Webapi.Services
组件作者:统一扫描出口
3.1.4 起,框架组件不要再自己调 AppDomain.CurrentDomain.GetAssemblies()。启动扫描的结果统一从 IAegisAssemblyProvider(定义在 Aegis.Component)拿:
public interface IAegisAssemblyProvider
{
IReadOnlyList<Assembly> Assemblies { get; }
/// <summary>类型快照(一次扫描产出,已含 ReflectionTypeLoadException 降级),消费方不要再枚举 DefinedTypes。</summary>
IReadOnlyList<Type> Types { get; }
}
两个获取通道,对应两个生命周期阶段:
| 阶段 | 获取方式 | 示例(EventBus / SignalR 实际代码) |
|---|---|---|
| Service 注册(Build 前,无 IServiceProvider) | AegisBuilder.Assemblies | var assemblies = builder.Assemblies?.Assemblies ?? AppDomain.CurrentDomain.GetAssemblies(); |
| Middleware / 运行期(容器已构建) | DI 单例 GetService<IAegisAssemblyProvider>() | var provider = app.ApplicationServices.GetService<IAegisAssemblyProvider>(); |
Types 是扫描时一次产出的快照,已经处理过类型加载降级,直接用,不要再 GetTypes() 一遍。组件脱离 AegisApplication 宿主独立使用时出口为 null,回退 AppDomain 自扫即可(EventBus、SignalR、Diagnostics 的写法都是这个模式)。
自扫的问题在于时点:框架快照在 Run 起始,组件自扫在其后,AppDomain 惰性加载会让两边集合不一致,这是"有时扫不到"的另一个来源;重复扫描也白白多耗启动时间。
3.1.4 的语义变化
升级前核对这几条:
| 变化 | 旧行为 | 新行为 |
|---|---|---|
| drop-in DLL | 磁盘全量扫描,任意 DLL 投进输出目录都能被发现 | Aegis 前缀的照常发现;非 Aegis 前缀且不依赖 Aegis 的,默认不进扫描面,需 Include |
| 第三方库 | 全部被 Load 和遍历类型 | 默认不进扫描面 |
| 加载失败 | 静默吞掉 | 启动横幅逐条 Warn |
自查方法:升级后启动一次,对比横幅里的"已扫描程序集 N 个"和组件注册列表(检查遗漏服务 段会列出发现但未配置加载的组件)。组件数没少、业务功能正常,就说明扫描面收窄没有影响你;发现少了什么,把对应程序集名写进 Include。
ICustomStartup / IInitializer 的执行顺序也从这一版起确定(Order 升序,同序保持发现顺序),initializer 抛异常从启动崩溃变为 Fail 横幅后继续,详见基础设施。
接入后怎么确认生效
- 启动横幅出现"程序集扫描"段,程序集数/类型数/耗时三个数字都在
- 组件注册列表与升级前一致(BaseDemo:13 个组件全部注册)
- 没有
[版本冲突:...]告警,或告警内容已知可接受 - "启动耗时"一行四个阶段数字正常输出
- 人为配错一个
Include条目,能看到对应的加载失败 Warn(验证配置确实被读到)
常见问题
问:升级后某个组件找不到了,怎么办?
先看横幅里"找不到组件 Xxx"或"当前未加载 Xxx"的提示,再看扫描统计的程序集数有没有明显变少。确认是扫描面问题后,把定义该组件的程序集名加进 Include,重启核对。
问:ReferenceMismatch 告警必须处理吗?
不必须。它说的是"编译时按 3.1.0 编的,运行时加载了 3.1.4-preview1"。NuGet 解析版本统一是正常机制,应用能跑就不用动。它的价值在于:哪天你见到 MissingMethodException 或 TypeLoadException,回来核对这条告警,十有八九就是版本差导致的。
问:为什么我的 Hub 包没被自动扫到?
Hub 类型不是 Aegis 契约,只引用微软 SignalR 的包不依赖任何 Aegis 程序集,反向可达判定够不着它。把包名写进 Include。
问:单文件发布后扫描行为一样吗?
单文件裁剪发布时应用 deps.json 缺失,框架自动回退为磁盘全量扫描减运行库黑名单前缀,Include / Exclude 规则照常生效。
问:Include 写了通配 Foo.*,会命中 Foo 这个程序集本身吗?
会。Foo.* 命中 Foo. 开头的所有程序集和名为 Foo 的本体,与 NuGet 包语义一致。
配套阅读
- 基础设施(Aegis.Core.Infrastructure):启动时序、
ICustomStartup/IInitializer扩展点与Order排序 - Startup 启动配置:项目里哪些东西该放在 Startup
- 配置:appsettings 与配置中心的分工