ABP VNext + Fody AOP:编译期织入与性能监控
ABP VNext + Fody AOP:编译期织入与性能监控 🚀
📚 目录
- ABP VNext + Fody AOP:编译期织入与性能监控 🚀
- 1. 引言 💡
- 2. 环境与依赖 🛠
- 3. 启用 Fody(项目配置) ⚙️
- 📊 编译期织入流程
- 4. 编译期切面 Attribute 🔍
- 日志 + 性能计时 + 异常捕获
- 📊 方法调用运行流程
- 5. 应用范围 🎯
- 6. Serilog 接入 📜
- 7. 阈值配置化 🎚
- 8. Benchmark 测试 🏎
- 9. 常见问题与规避 ⚠
1. 引言 💡
TL;DR
用 Fody + MethodDecorator.Fody 在编译期为 ABP ApplicationService
自动注入:日志、耗时计时、异常捕获。
- ⚡ 极少反射/零侵入:仅在切面初始化时使用少量反射获取方法信息;不依赖运行时动态代理。
- 🛠 支持 同步/异步 方法;可与 ABP 模块化/DI 并存。
- 📊 附 BenchmarkDotNet 对比,量化织入开销。
为什么不是运行时 AOP?
运行时代理(Castle DynamicProxy)会引入启动延迟、调用额外开销,还可能和第三方拦截器冲突;编译期 IL 织入更轻、更可控。
2. 环境与依赖 🛠
-
平台:.NET 6.x、ABP v6.x
-
NuGet 包
dotnet add package Fody dotnet add package MethodDecorator.Fody dotnet add package Serilog dotnet add package Serilog.Extensions.Hosting dotnet add package Serilog.Sinks.Console dotnet add package BenchmarkDotNet
-
注意
-
📂
FodyWeavers.xml
放在项目根目录(与.csproj
同级)。 -
推荐
.csproj
配置:<PropertyGroup><LangVersion>latest</LangVersion><Nullable>enable</Nullable> </PropertyGroup><ItemGroup><PackageReference Include="Fody" Version="*" PrivateAssets="all" /><PackageReference Include="MethodDecorator.Fody" Version="*" PrivateAssets="all" /> </ItemGroup>
-
3. 启用 Fody(项目配置) ⚙️
FodyWeavers.xml
示例(限制织入范围,防止误伤):
<?xml version="1.0" encoding="utf-8" ?>
<Weavers><MethodDecorator><IncludeAssemblies>MyCompany.MyApp.Application|MyCompany.MyApp.Domain</IncludeAssemblies><ExcludeAssemblies>Volo.Abp.*|Castle.*</ExcludeAssemblies></MethodDecorator>
</Weavers>
📊 编译期织入流程
4. 编译期切面 Attribute 🔍
日志 + 性能计时 + 异常捕获
using MethodDecorator.Fody.Interfaces;
using System;
using System.Diagnostics;
using System.Reflection;
using Serilog;[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = false)]
public sealed class LogPerformanceAttribute : Attribute, IMethodDecorator
{private Stopwatch? _sw;private string _methodName = string.Empty;private readonly ILogger _log;private const int SlowThresholdMs = 200; // 可改为从配置读取public LogPerformanceAttribute(){_log = Log.ForContext<LogPerformanceAttribute>();}public void Init(object instance, MethodBase method, object[] args){_methodName = $"{method.DeclaringType?.Name}.{method.Name}";}public void OnEntry(){_sw = Stopwatch.StartNew();_log.Debug("🚀 [Enter] {Method}", _methodName);}public void OnExit(){if (_sw == null) return;_sw.Stop();var elapsed = _sw.ElapsedMilliseconds;if (elapsed >= SlowThresholdMs)_log.Warning("🐢 [Slow] {Method} took {Elapsed} ms", _methodName, elapsed);else_log.Information("✅ [Exit] {Method} in {Elapsed} ms", _methodName, elapsed);}public void OnException(Exception exception){_sw?.Stop();_log.Error(exception, "💥 [Error] {Method}", _methodName);}
}
⚠ 注意:async void
方法无法保证触发 OnExit()
,不建议织入此类方法。
📊 方法调用运行流程
5. 应用范围 🎯
-
程序集级别:
[assembly: LogPerformance]
-
类级别:
[LogPerformance] public class ProductAppService : ApplicationService { }
-
方法级别:
public class OrderAppService : ApplicationService {[LogPerformance]public async Task<OrderDto> GetAsync(Guid id) { /* ... */ } }
💡 建议仅对 Application 层 织入,避免与 ABP 审计/日志拦截器重复打点。
6. Serilog 接入 📜
using Serilog;Log.Logger = new LoggerConfiguration().MinimumLevel.Information().WriteTo.Async(a => a.Console()).Enrich.FromLogContext().Enrich.WithEnvironmentName().CreateLogger();try
{builder.Host.UseSerilog();
}
finally
{Log.CloseAndFlush();
}
初始化时机要在
HostBuilder
创建之前,避免被 ABP 默认日志覆盖。
7. 阈值配置化 🎚
public static class AopSettings
{public static int SlowThresholdMs { get; private set; } = 200;public static void Load(IConfiguration config)=> SlowThresholdMs = config.GetValue<int?>("AopMonitoring:SlowThresholdMs") ?? 200;
}
AopSettings.Load(builder.Configuration);
8. Benchmark 测试 🏎
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Order;[SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net80)]
[MemoryDiagnoser]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[IterationCount(20)]
public class PerfTests
{private readonly ProductAppService _svc = new();[Benchmark(Baseline = true)]public Task NoAop_GetAsync() => _svc.GetAsync(Guid.NewGuid());[Benchmark]public Task FodyAop_GetAsync() => _svc.GetAsync(Guid.NewGuid());
}public static class Program
{public static void Main() => BenchmarkRunner.Run<PerfTests>();
}public class ProductAppService
{public virtual Task<int> GetAsync(Guid id) => Task.FromResult(42);
}
测试方法:
- 关闭织入 → 编译 → 运行 Benchmark
- 开启织入 → 编译 → 运行 Benchmark
- 对比吞吐/耗时差异 📊
9. 常见问题与规避 ⚠
- 冲突:用
IncludeAssemblies
限制范围,或关闭 ABP 某些拦截器。 - 日志风暴:使用阈值日志、方法白名单。
- 调试策略:Debug 全织入,Release 精准织入。
- AOT/Trim 注意:NativeAOT 或裁剪发布时,需要保留反射元数据,否则 MethodBase 获取可能失效。