Scrutor隐藏技巧:用装饰器模式给.NET Core服务加日志竟如此简单

📅 发布时间:2026/7/11 15:02:17 👁️ 浏览次数:
Scrutor隐藏技巧:用装饰器模式给.NET Core服务加日志竟如此简单
Scrutor隐藏技巧用装饰器模式给.NET Core服务加日志竟如此简单最近在重构一个老项目时我遇到了一个典型的痛点需要在几十个服务接口的方法调用前后统一添加执行时间日志。如果一个个去修改实现类不仅工作量巨大还容易引入错误更违背了开闭原则。就在我准备硬着头皮写一堆重复代码时团队里的一位架构师拍了拍我肩膀“试试Scrutor的装饰器模式一行注册全局生效。” 我将信将疑地试了试结果只用了一个装饰器类和一个Decorate调用就优雅地解决了所有问题原有业务代码一行未动。这种“无侵入式增强”的体验让我对.NET Core的依赖注入容器有了新的认识。这篇文章我就来拆解这个被很多人忽略的Scrutor隐藏技巧手把手带你用装饰器模式像搭积木一样为服务添加日志、监控、缓存等横切关注点。1. 为什么是装饰器模式从AOP的困境到DI容器的优雅解在面向切面编程AOP的世界里我们总想在不触碰核心业务逻辑的情况下为方法织入一些通用行为比如日志记录、性能监控、事务管理或缓存。传统的实现方式无论是动态代理如Castle DynamicProxy还是基于编译时织入的PostSharp都或多或少有些“重”它们要么需要引入额外的运行时代理框架增加复杂度和性能开销要么需要购买商业许可或进行复杂的编译配置。而装饰器模式这个来自《设计模式》经典教科书中的结构型模式恰恰提供了一种轻量级、编译时安全的AOP实现途径。其核心思想是定义一个与原始服务接口相同的装饰器类它内部持有一个原始服务的实例并在调用原始服务方法的前后执行额外的逻辑。这听起来很简单但难点在于如何让依赖注入容器自动管理这种装饰关系避免手动创建和组装对象的繁琐。这就是Scrutor大显身手的地方。Scrutor并没有发明新东西它只是巧妙地利用了Microsoft.Extensions.DependencyInjectionMSDI容器已有的能力并通过Decorate扩展方法将装饰器模式的注册变得声明式和自动化。它解决了手动实现装饰器时最头疼的问题如何让容器在解析IMyService时自动将LoggingDecorator(MyService)这个组合对象返回给你。通过Scrutor你只需告诉容器“嘿当有人要IMyService时请先用LoggingDecorator把它包起来。” 剩下的依赖解析和对象构建容器全帮你搞定。注意Scrutor的装饰器模式是纯粹的“编译时装饰”它不创建子类或代理类而是在依赖解析链中动态包装实例。这意味着它对性能的影响微乎其微且能获得完整的IDE智能提示和编译检查。2. 实战为服务方法自动注入执行时间日志理论说得再多不如一行代码。让我们从一个最实用的场景开始为服务方法记录执行耗时。假设我们有一个处理订单的核心服务。首先定义我们的业务接口和实现public interface IOrderProcessor { TaskOrderResult ProcessAsync(OrderRequest request); } public class OrderProcessor : IOrderProcessor { public async TaskOrderResult ProcessAsync(OrderRequest request) { // 模拟复杂的业务逻辑 await Task.Delay(100); return new OrderResult { Success true, OrderId Guid.NewGuid() }; } }现在我们需要在不修改OrderProcessor类的前提下为ProcessAsync方法添加耗时日志。第一步是创建装饰器public class TimingLoggingDecorator : IOrderProcessor { private readonly IOrderProcessor _inner; private readonly ILoggerTimingLoggingDecorator _logger; public TimingLoggingDecorator(IOrderProcessor inner, ILoggerTimingLoggingDecorator logger) { _inner inner; _logger logger; } public async TaskOrderResult ProcessAsync(OrderRequest request) { var stopwatch Stopwatch.StartNew(); _logger.LogInformation(开始处理订单请求ID: {RequestId}, request.Id); try { var result await _inner.ProcessAsync(request); stopwatch.Stop(); _logger.LogInformation(订单处理成功。请求ID: {RequestId}, 耗时: {ElapsedMs}ms, request.Id, stopwatch.ElapsedMilliseconds); return result; } catch (Exception ex) { stopwatch.Stop(); _logger.LogError(ex, 订单处理失败。请求ID: {RequestId}, 耗时: {ElapsedMs}ms, request.Id, stopwatch.ElapsedMilliseconds); throw; } } }这个装饰器做了几件事通过构造函数注入被装饰的原始服务实例_inner和日志器。在方法开始时启动计时器并记录开始日志。调用原始服务方法。无论成功与否都记录耗时和结果。关键步骤来了如何使用Scrutor进行注册。在Startup.cs或Program.cs的依赖注入配置中using Scrutor; public void ConfigureServices(IServiceCollection services) { // 1. 首先像往常一样注册原始服务 services.AddScopedIOrderProcessor, OrderProcessor(); // 2. 然后使用Scrutor的Decorate方法注册装饰器 services.DecorateIOrderProcessor, TimingLoggingDecorator(); // ... 其他服务注册 }是的就这么简单。两行代码。现在当你在控制器或其他服务中通过构造函数注入IOrderProcessor时你实际得到的是一个被TimingLoggingDecorator包装过的OrderProcessor。调用ProcessAsync方法时日志会自动记录。为了更清晰地理解这个过程我们看看容器解析服务时内部发生了什么步骤容器行为结果对象结构1. 收到解析IOrderProcessor的请求查找IOrderProcessor的注册发现最终实现是TimingLoggingDecorator2. 构建TimingLoggingDecorator发现它依赖IOrderProcessor即_inner容器开始递归解析_inner3. 解析内部的IOrderProcessor查找注册找到OrderProcessor创建OrderProcessor实例4. 完成装饰器构建将OrderProcessor实例注入TimingLoggingDecorator返回TimingLoggingDecorator实例整个调用链对使用者完全透明这才是依赖注入和装饰器模式结合的威力。3. 进阶技巧构建可组合、可配置的装饰器管道单个日志装饰器可能只是开始。在实际项目中我们可能需要对同一个服务叠加多个增强功能例如先进行参数验证再记录日志最后进行性能监控。手动嵌套装饰器类会很快导致代码混乱。Scrutor的优雅之处在于它天然支持装饰器链。假设我们还有两个装饰器需求ValidationDecorator: 对输入参数进行基础验证。CircuitBreakerDecorator: 添加简单的熔断器逻辑防止连续失败。我们可以分别创建这些装饰器并按特定的顺序进行注册public void ConfigureServices(IServiceCollection services) { services.AddScopedIOrderProcessor, OrderProcessor(); // 装饰器注册顺序决定了它们的执行顺序从外到内 // 1. 最先执行熔断器最外层 services.DecorateIOrderProcessor, CircuitBreakerDecorator(); // 2. 其次执行日志记录 services.DecorateIOrderProcessor, TimingLoggingDecorator(); // 3. 最后执行最先调用原始服务参数验证最内层 services.DecorateIOrderProcessor, ValidationDecorator(); }此时的解析对象结构就像一个洋葱CircuitBreakerDecorator-TimingLoggingDecorator-ValidationDecorator-OrderProcessor当调用发生时请求会依次穿过熔断器、日志、验证层最后到达核心业务逻辑。响应则按相反顺序返回。这种管道模式让每个装饰器职责单一易于测试和维护。但有时我们可能希望装饰器是有条件生效的。例如只在开发环境记录详细日志或者只为特定实现类的服务添加装饰器。Scrutor通过与TryDecorate和条件扫描的结合也能轻松实现。// 示例仅当原始实现是 SpecificProcessor 时才添加装饰器 services.AddScopedIOrderProcessor, OrderProcessor(); services.AddScopedIOrderProcessor, SpecificProcessor(); // 使用 TryDecorate 并配合条件判断 services.TryDecorateIOrderProcessor((inner, provider) { // 只有当内部实例是 SpecificProcessor 类型时才进行装饰 if (inner is SpecificProcessor specific) { var logger provider.GetRequiredServiceILoggerConditionalDecorator(); return new ConditionalDecorator(inner, logger); } // 否则返回原始实例 return inner; });TryDecorate方法提供了更大的灵活性你可以在工厂委托中访问到原始服务实例inner和服务提供者provider从而做出动态决策。4. 避坑指南装饰器模式下的生命周期与陷阱虽然Scrutor装饰器用起来很爽但如果不理解其背后的生命周期管理很容易掉进坑里。最常见的问题与作用域Scoped生命周期和装饰器构造函数依赖有关。陷阱一装饰器导致作用域实例被意外延长假设原始服务A是Scoped生命周期它依赖另一个Scoped服务B。如果装饰器DecoratorA也是Scoped并且在其构造函数或方法中注入了新的依赖比如一个DbContext这可能会导致B被装饰器间接持有从而影响其释放时机。关键在于理解装饰器实例和原始服务实例的生命周期是独立的但通过依赖关系关联。最佳实践建议尽量保持装饰器为Transient瞬态或Scoped作用域并与原始服务的生命周期保持一致以减少复杂度。避免在装饰器中持有对IDisposable资源的长期引用除非你很清楚它的生命周期。陷阱二装饰器构造函数中的依赖解析装饰器在构造时其内部包装的原始服务_inner可能尚未完全解析尤其是当存在装饰链时。因此不要在装饰器的构造函数中调用_inner的方法。所有对_inner的调用都应该在装饰器的方法内部进行。陷阱三对已解析实例的装饰Decorate方法只在服务注册阶段生效。如果你已经从一个ServiceProvider中解析出了某个服务的实例然后再对容器调用Decorate这个已存在的实例不会被重新装饰。装饰必须在构建ServiceProvider之前完成。为了帮助你规避这些陷阱下面列出了一些常见问题的症状和检查点内存泄漏检查装饰器是否无意中持有了大对象或数据库连接。生命周期异常例如在Singleton服务中使用了Scoped的DbContext。确保装饰器引入的依赖其生命周期是兼容的。装饰未生效确认services.Decorate的调用是否在BuildServiceProvider()之前并且注册的服务接口/类型完全匹配。提示在开发环境可以通过在Decorate处打断点或者输出容器的服务描述信息来验证装饰器是否按预期注册。一个简单的调试方法是在装饰器构造函数里打印日志观察其被创建的时机和次数。5. 超越日志装饰器模式的多样化应用场景掌握了基础的日志装饰器后你会发现这个模式的应用场景远不止于此。它本质上是一种行为增强的手段。以下是我在项目中实际应用过的几个例子希望能激发你的灵感。场景一自动缓存层为数据查询服务添加一层透明缓存是装饰器的经典用例。public class CachingUserServiceDecorator : IUserService { private readonly IUserService _inner; private readonly IMemoryCache _cache; private readonly TimeSpan _defaultExpiration TimeSpan.FromMinutes(5); public CachingUserServiceDecorator(IUserService inner, IMemoryCache cache) { _inner inner; _cache cache; } public async TaskUser GetUserByIdAsync(int id) { var cacheKey $user_{id}; return await _cache.GetOrCreateAsync(cacheKey, async entry { entry.AbsoluteExpirationRelativeToNow _defaultExpiration; return await _inner.GetUserByIdAsync(id); }); } } // 注册services.DecorateIUserService, CachingUserServiceDecorator();场景二重试与弹性处理对于调用外部API或数据库可能失败的操作添加自动重试逻辑。public class RetryPolicyDecorator : IExternalApiService { private readonly IExternalApiService _inner; private readonly ILoggerRetryPolicyDecorator _logger; public RetryPolicyDecorator(IExternalApiService inner, ILoggerRetryPolicyDecorator logger) { _inner inner; _logger logger; } public async TaskApiResponse CallApiAsync() { var retryCount 0; var maxRetries 3; while (true) { try { return await _inner.CallApiAsync(); } catch (HttpRequestException ex) when (retryCount maxRetries) { retryCount; _logger.LogWarning(ex, API调用失败正在进行第 {RetryCount} 次重试, retryCount); await Task.Delay(1000 * retryCount); // 指数退避 } } } }场景三审计与数据脱敏在返回敏感数据如用户信息前自动过滤掉密码、手机号等字段。public class DataMaskingDecorator : IUserDataService { private readonly IUserDataService _inner; public DataMaskingDecorator(IUserDataService inner) _inner inner; public async TaskUserProfile GetProfileAsync(int userId) { var profile await _inner.GetProfileAsync(userId); // 脱敏处理 if (!string.IsNullOrEmpty(profile.PhoneNumber)) { profile.PhoneNumber MaskPhoneNumber(profile.PhoneNumber); } profile.Email MaskEmail(profile.Email); return profile; } private string MaskPhoneNumber(string phone) phone.Length 7 ? ${phone[..3]}****{phone[^4..]} : phone; private string MaskEmail(string email) ******.com; // 简化示例 }这些场景的共同点是它们都是横切关注点与核心业务逻辑正交。使用装饰器模式你可以将这些通用功能模块化像插件一样按需添加到任何服务上极大地提升了代码的复用性和可维护性。下次当你发现自己在多个服务中复制粘贴相似的“辅助代码”时不妨停下来想想这能不能用一个Scrutor装饰器来优雅地解决