依赖注入和命名记录器


74

我有兴趣了解有关人们如何使用依赖项注入平台注入日志的更多信息。尽管下面的链接和我的示例引用了log4net和Unity,但我不一定要使用这两个。对于依赖项注入/ IOC,我可能会使用MEF,因为这是其余项目(大型)正在建立的标准。

我对依赖关系注入/ ioc非常陌生,对C#和.NET也很陌生(在VC6和VB6大约十年后,在C#/。NET中很少编写生产代码)。我对现有的各种日志记录解决方案进行了很多调查,因此我认为我对它们的功能集有很好的了解。我只是对注入一个依赖项的实际机制还不够熟悉(或者,也许更“正确”地,是获取一个依赖项的抽象版本)。

我看过其他与日志记录和/或依赖注入相关的帖子,例如: 依赖注入和日志记录接口

记录最佳做法

Log4Net Wrapper类是什么样的?

再次关于log4net和Unity IOC配置

我的问题与“如何使用IOC工具yyy注入xxx日志记录平台”无关。而是,我对人们如何处理包装日志记录平台(通常但不总是建议)和配置(即app.config)感兴趣。例如,以log4net为例,我可以配置(在app.config中)许多记录器,然后以使用如下代码的标准方式获取这些记录器(不依赖注入):

private static readonly ILog logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

另外,如果我的记录器不是以类命名,而是以功能区域命名,则可以这样做:

private static readonly ILog logger = LogManager.GetLogger("Login");
private static readonly ILog logger = LogManager.GetLogger("Query");
private static readonly ILog logger = LogManager.GetLogger("Report");

因此,我想我的“要求”将是这样的:

  1. 我想将我的产品的来源与对日志记录平台的直接依赖相隔离。

  2. 我希望能够通过某种依赖注入(可能是MEF)直接或间接解析特定的命名记录器实例(可能在同一命名实例的所有请求者之间共享同一实例)。

  3. 我不知道这是否很难,但是我希望能够按需获取命名记录器(与类记录器不同)。例如,我可能会基于类名称为我的类创建一个记录器,但是一种方法需要特别繁重的诊断,而我想单独进行控制。换句话说,我可能希望单个类“依赖”两个单独的记录器实例。

让我们从数字1开始。我已经阅读了很多文章,主要是关于stackoverflow的,关于包装是否是个好主意。请参阅上面的“最佳实践”链接,并转到jeffrey hantin的注释中,以获取有关包装log4net的原因的观点。如果您进行了包裹(如果可以有效包裹),您是否将包裹严格地用于注射/去除直接依赖?还是您还会尝试抽象出部分或全部log4net app.config信息?

假设我要使用System.Diagnostics,可能要实现一个基于接口的记录器(甚至使用“通用的” ILogger / ILog接口),并且可能基于TraceSource,以便可以注入它。您是否会实现接口(例如通过TraceSource),并仅按原样使用System.Diagnostics app.config信息?

像这样:

public class MyLogger : ILogger
{
  private TraceSource ts;
  public MyLogger(string name)
  {
    ts = new TraceSource(name);
  }

  public void ILogger.Log(string msg)
  {
    ts.TraceEvent(msg);
  }
}

并像这样使用它:

private static readonly ILogger logger = new MyLogger("stackoverflow");
logger.Info("Hello world!")

继续前进到第二...如何解析特定的命名记录器实例?我是否应该利用所选日志平台的app.config信息(即根据app.config中的命名方案解析记录器)?因此,在使用log4net的情况下,我是否可以选择“注入” LogManager(请注意,我知道这是不可能的,因为它是静态对象)?我可以包装LogManager(称为MyLogManager),为其提供ILogManager接口,然后解析MyLogManager.ILogManager接口。我的其他对象可能在ILogManager上有依赖关系(以MEF的说法导入)(从实现它的程序集导出)。现在我可以有这样的对象:

public class MyClass
{
  private ILogger logger;
  public MyClass([Import(typeof(ILogManager))] logManager)
  {
    logger = logManager.GetLogger("MyClass");
  }
}

每当调用ILogManager时,它将直接委派给log4net的LogManager。或者,包装的LogManager可以基于app.config获取它获取的ILogger实例,然后按名称将其添加到(a?)MEF容器中。稍后,当请求同名记录器时,将查询包装的LogManager以获取该名称。如果ILogger在那,则以这种方式解决。如果MEF可以做到这一点,这样做有什么好处吗?

在这种情况下,实际上,仅“注入”了ILogManager,它可以按照log4net正常方式分发ILogger实例。这种类型的注入(基本上是工厂注入)与注入命名的记录器实例相比如何?这确实允许更轻松地利用log4net(或其他日志记录平台)的app.config文件。

我知道我可以像这样从MEF容器中获取命名实例:

var container = new CompositionContainer(<catalogs and other stuff>);
ILogger logger = container.GetExportedValue<ILogger>("ThisLogger");

但是如何将命名实例放入容器?我知道基于属性的模型,在该模型中我可以有ILogger的不同实现,每个实现都被命名(通过MEF属性),但这对我没有帮助。有没有一种方法可以创建类似app.config(或其中的一部分)的内容,该名称可以按名称列出记录器(所有实现都相同),并且MEF可以读取?可以/应该有一个中央“管理器”(例如MyLogManager),该管理器通过基础app.config解析命名的记录器,然后将解析的记录器插入MEF容器中?这样,其他可以访问同一MEF容器的人就可以使用它(尽管在MyLogManager不了解如何使用log4net的app.config信息的情况下,

这已经很长了。我希望它是连贯的。请随时分享有关您的依赖项如何向应用程序中注入日志记录平台(我们很可能考虑使用log4net,NLog或基于System.Diagnostics构建的某些东西(希望是瘦的))的任何特定信息。

您是否注入了“管理器”并使它返回记录器实例?

您是否在自己的config部分或DI平台的config部分中添加了一些自己的配置信息,以使其更容易/可以直接注入记录器实例(即,使依赖项位于ILogger而非ILogManager上)。

如果拥有一个静态或全局容器,该容器中包含ILogManager接口或其中包含一组命名的ILogger实例,该怎么办?因此,不是按常规意义(通过构造函数,属性或成员数据)进行注入,而是根据需要显式解决了日志依赖项。这是依赖注入的好方法还是坏方法。

我将其标记为社区Wiki,因为它似乎不是一个有明确答案的问题。如果有人有其他感觉,请随时进行更改。

谢谢你的帮助!

Answers:


38

我正在使用Ninject来解析记录器实例的当前类名称,如下所示:

kernel.Bind<ILogger>().To<NLogLogger>()
  .WithConstructorArgument("currentClassName", x => x.Request.ParentContext.Request.Service.FullName);

NLog实现的构造函数可能如下所示:

public NLogLogger(string currentClassName)
{
  _logger = LogManager.GetLogger(currentClassName);
}

我猜,这种方法也应该与其他IOC容器一起使用。


6
我曾经x.Request.ParentContext.Plan.Type.FullName获得具体的类名而不是接口名。
查德威克2013年

我总是将ParentContext设为null。我有从实现ILogger的Loggerbase继承的NLogLogger。
元帅

如果基类在另一个项目中,请确保再次初始化记录器,例如:[assembly:log4net.Config.XmlConfigurator(Watch = true)]
Orhan

16

也可以使用Common.Logging门面或Simple Logging Facade

两者都使用服务定位器样式模式来检索ILogger。

坦白说,日志记录是那些依赖项之一,我认为自动注入几乎没有价值。

我大多数需要日志记录服务的类如下所示:

public class MyClassThatLogs {
    private readonly ILogger log = Slf.LoggerService.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName);

}

通过使用简单日志记录外观,我将项目从log4net切换到了NLog,并且除了使用NLog记录应用程序之外,还添加了使用log4net的第三方库的日志记录。也就是说,外墙为我们服务很好。

一个难以避免的警告是丢失特定于一个或另一个日志框架的功能,也许最常见的例子是自定义日志级别。


感谢您提供有关SLF和Common.Logging的提示。实际上,我最近已经对这些库进行了实验,它们都很酷。最后,我认为我们的项目可能不会使用DI来获取日志,因此我们最终可能会使用SLF或Common.Logging。
10年

13

这对于任何想弄清楚如何在要注入的记录器提供了log4net或NLog等日志记录平台的情况下注入记录器依赖项的人都是有益的。我的问题是,当我知道特定ILogger的解析将取决于了解依赖于ILogger的类的类型时,我不明白如何使类(例如MyClass)依赖于ILogger类型的接口(例如MyClass)。DI / IoC平台/容器如何获得正确的ILogger?

好吧,我查看了Castle和NInject的来源,并了解了它们的工作原理。我也看过AutoFac和StructureMap。

Castle和NInject都提供了日志记录的实现。两者都支持log4net和NLog。Castle还支持System.Diagnostics。在这两种情况下,当平台解析给定对象的依赖项时(例如,当平台正在创建MyClass且MyClass依赖于ILogger时),它将依赖项的创建(ILogger)委托给ILogger“提供者”(解析器可能更多)。通用术语)。然后,ILogger提供程序的实现负责实际实例化ILogger的实例并将其分发出去,然后注入到依赖类(例如MyClass)中。在这两种情况下,提供者/解析器都知道依赖类的类型(例如MyClass)。因此,在创建MyClass并解决其依赖项后,ILogger“解析器”知道该类为MyClass。在使用Castle或NInject提供的日志记录解决方案的情况下,这意味着日志记录解决方案(通过log4net或NLog的包装实现)获取类型(MyClass),因此可以将其委托给log4net.LogManager.GetLogger()或NLog.LogManager.GetLogger()。(不是100%肯定log4net和NLog的语法,但是您知道了)。

尽管AutoFac和StructureMap不提供日志记录功能(至少我可以通过观察得知),但它们似乎确实提供了实现自定义解析器的功能。因此,如果您想编写自己的日志记录抽象层,则还可以编写相应的自定义解析器。这样,当容器要解析ILogger时,将使用您的解析器来获取正确的ILogger并且它可以访问当前上下文(即,当前满足什么对象的依赖关系-哪个对象依赖ILogger)。获取对象的类型,并准备将ILogger的创建委派给当前配置的日志记录平台(您可能已经在接口后面进行了抽象,并为此编写了解析器)。

因此,我怀疑需要几个关键要点,但我以前没有完全掌握:

  1. 最终,DI容器必须以某种方式知道要使用的日志平台。通常,这是通过指定“ ILogger”由特定于日志记录平台的“解析器”来完成的(因此,Castle具有log4net,NLog和System.Diagnostics“解析器”(以及其他))。可以通过配置文件或以编程方式指定要使用的解析器。

  2. 解析器需要知道要为其解决依赖项(ILogger)的上下文。也就是说,如果已经创建MyClass并且它依赖于ILogger,则当解析程序尝试创建正确的ILogger时,它(解析程序)必须知道当前的类型(MyClass)。这样,解析器可以使用基础的日志记录实现(log4net,NLog等)来获取正确的记录器。

这些要点对于那些DI / IoC用户而言可能很明显,但是我现在才刚接触它,所以花了我一些时间才得以解决。

我还没有弄清的一件事是MEF如何或是否可能实现这种效果。我可以有一个依赖于接口的对象,然后在MEF创建对象之后并在解决接口/依赖关系的情况下执行我的代码吗?因此,假设我有一个这样的课:

public class MyClass
{
  [Import(ILogger)]
  public ILogger logger;

  public MyClass()
  {
  }

  public void DoSomething()
  {
    logger.Info("Hello World!");
  }
}

当MEF为MyClass解析导入时,我是否可以有一些自己的代码(通过属性,通过ILogger的实现上的附加接口,在其他地方?)执行并解析ILogger的导入,当前处于上下文中并返回(可能)与为YourClass检索的(可能)不同的ILogger实例的MyClass?我是否实施某种MEF提供程序?

在这一点上,我仍然不了解MEF。


看来MEF和Unity(MS的Patterns&Practices组的IoC)就像比较苹果和橘子。对于此特定问题,可能需要一个真正的IoC容器,该容器具有可扩展性的点来添加自定义依赖项解析。 stackoverflow.com/questions/293051/...
诺曼^ h

哇...有些人确实使简单的事情变得比他们需要的困难得多。我想知道当您只想向文件中写入一些文本时,所有这些功能到底真的有用吗……
Ed S.


0

我通过注册Log4Net记录器以由MEF进行依赖项注入的提供程序创建了自定义ServiceExportProvider。因此,您可以将记录器用于不同类型的进样。

注射示例:

[Export]
public class Part
{
    [ImportingConstructor]
    public Part(ILog log)
    {
        Log = log;
    }

    public ILog Log { get; }
}

[Export(typeof(AnotherPart))]
public class AnotherPart
{
    [Import]
    public ILog Log { get; set; }
}

用法示例:

class Program
{
    static CompositionContainer CreateContainer()
    {
        var logFactoryProvider = new ServiceExportProvider<ILog>(LogManager.GetLogger);
        var catalog = new AssemblyCatalog(typeof(Program).Assembly);
        return new CompositionContainer(catalog, logFactoryProvider);
    }

    static void Main(string[] args)
    {
        log4net.Config.XmlConfigurator.Configure();
        var container = CreateContainer();
        var part = container.GetExport<Part>().Value;
        part.Log.Info("Hello, world! - 1");
        var anotherPart = container.GetExport<AnotherPart>().Value;
        anotherPart.Log.Fatal("Hello, world! - 2");
    }
}

结果进入控制台:

2016-11-21 13:55:16,152 INFO  Log4Mef.Part - Hello, world! - 1
2016-11-21 13:55:16,572 FATAL Log4Mef.AnotherPart - Hello, world! - 2

ServiceExportProvider的实现:

public class ServiceExportProvider<TContract> : ExportProvider
{
    private readonly Func<string, TContract> _factoryMethod;

    public ServiceExportProvider(Func<string, TContract> factoryMethod)
    {
        _factoryMethod = factoryMethod;
    }

    protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition, AtomicComposition atomicComposition)
    {
        var cb = definition as ContractBasedImportDefinition;
        if (cb?.RequiredTypeIdentity == typeof(TContract).FullName)
        {
            var ce = definition as ICompositionElement;
            var displayName = ce?.Origin?.DisplayName;
            yield return new Export(definition.ContractName, () => _factoryMethod(displayName));
        }
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.