日志记录和依赖关系注入

我试图基于Java构建和应用程序。

对于依赖注入,我使用Google Guice。

现在我想到了在应用程序期间记录一些信息的问题。我不以方法调用等方式谈论一般日志记录。我知道AOP,我可以用它来做方法调用跟踪等。

我寻找的是手动日志记录。我需要某种方式登录应用程序中的几乎每个类。所以我考虑了两个选择:

  1. 通过使用Guice注入框架获取记录器,通过构造函数(或 setter 或 private...)为我执行此操作,但这感觉就像将日志记录问题真正添加到每个类中并污染了我的构造函数
  2. 在我要调用日志的方法中使用全局服务定位器。呃,但所有DI粉丝都会讨厌我这样做

那么从实际的角度来看,最好的方法是什么呢?


答案 1

我需要某种方式登录应用程序中的几乎每个类。

再想一想。如果您认为需要登录几乎每个类,那么您的设计就有问题。这个Stackoverflow答案讨论了你的设计可能存在的问题。它在.NET的上下文中得到了答案,但答案也适用于Java。

这个答案主要讨论异常日志记录,对于非异常日志记录,我会说:防止在太多地方记录太多信息。对于要记录的每个信息或警告,请询问这是否首先不应该是异常。例如,不要记录诸如“我们不应该在这个分支中”之类的事情,而是抛出一个异常!

即使您想记录调试信息,是否有人会阅读此内容?您最终会得到包含成千上万行的日志文件,没有人会阅读。如果他们阅读它,他们必须遍历所有这些文本行,并通过它进行复杂的正则表达式搜索,以获得他们正在寻找的信息。

我看到开发人员这样做的另一个原因是掩盖他们的糟糕代码。就像注释以这种方式使用一样。我看到开发人员记录了诸如“我们已经执行了此块”或“如果跳过分支,则为”之类的内容。通过这种方式,他们可以跟踪代码和大方法。

但是,我们现在都知道方法应该很小,而不是编写大方法。不,甚至更小。此外,如果您对代码进行彻底的单元测试,则没有太多理由调试代码,并且您已经验证了它是否执行了它应该执行的操作。

再一次,好的设计可以在这方面有所帮助。使用 Stackoverflow 答案中所述的设计(使用命令处理程序)时,可以再次创建单个修饰器,该修饰器可以序列化任何任意命令消息,并在执行开始之前将其记录到磁盘。这为您提供了非常准确的日志。只需向日志中添加一些上下文信息(例如执行时间和用户名),您就有了一个审计跟踪,甚至可以在调试甚至负载测试期间重播命令。

我使用这种类型的应用程序设计已经有几年了,从那时起,我几乎没有任何理由在业务逻辑中进行额外的日志记录。时不时需要它,但这些情况非常罕见。

但这感觉就像将日志记录问题真正添加到每个类中并污染了我的构造函数

确实如此,您最终会得到参数过多的构造函数。但不要责怪伐木者,责怪你的代码。您违反了此处的单一责任原则。您可以通过静态外观调用此依赖项来“隐藏”此依赖项,但这不会降低类的依赖项数量和整体复杂性。

在我要调用日志的方法中使用全局服务定位器。呃,但所有DI粉丝都会讨厌我这样做

最后,你会为此而讨厌自己,因为每个类仍然有一个额外的依赖关系(在这种情况下是一个很好的隐藏的依赖关系)。这使得每个类都更加复杂,并且将迫使您拥有更多代码:要测试的代码更多,要有错误的代码更多,要维护的代码更多。


答案 2

日志记录的主题以及应该如何进行它实际上是一个比人们最初想象的更复杂的主题。

与许多问题一样,应该如何处理日志记录的答案是“视情况而定”。当然,有些用例可以缓解,而无需组件承担日志记录依赖关系。例如,可以使用装饰器模式解决统一记录库内所有方法调用的需要,并且可以通过将此类日志记录集中在调用堆栈的顶部来解决日志记录异常的多余使用。考虑这样的用例很重要,但它们并没有说出问题的本质,即“当我们想用强类型语言(如Java和C#)向组件添加详细的日志记录时,依赖关系是否应该通过组件的构造函数来表示?"

使用服务定位器模式被认为是一种反模式,因为它的误用会导致不透明的依赖关系。也就是说,通过服务定位器获取其所有依赖项的组件在不知道内部实现细节的情况下不会表达所需的所有内容。避免服务定位器模式是一个很好的经验法则,但此规则的拥护者应该了解何时以及为何,以免落入货物崇拜陷阱。

避免服务定位器模式的目标最终是使组件更易于使用。在构建组件时,我们不希望消费者猜测组件按预期运行所需的内容。使用我们库的开发人员不必查看实现细节来了解组件运行所需的依赖项。然而,在许多情况下,日志记录是一个辅助和可选的问题,仅用于为库维护者提供跟踪信息,以诊断问题或保留消费者既不了解也不感兴趣的使用详细信息的审核日志。在库的使用者必须提供组件主要功能不需要的依赖项的情况下,将此类依赖项表示为不变(即构造函数参数)实际上否定了通过避免服务定位器模式所寻求的目标。此外,由于日志记录是一个横切问题(这意味着在库中的许多组件中可能广泛需要这种需求),因此通过构造函数注入日志记录依赖项会进一步放大使用难度。

还有一个考虑因素是尽量减少对库的表面积 API 的更改。库的表面积 API 是构造所需的任何公共接口或类。通常情况下,库是通过 DI 容器构造的,特别是对于内部维护的库,而不是供公众使用。在这种情况下,注册模块可能由库为特定的 DI 容器提供,或者可以采用基于约定的注册等技术来隐藏顶级类型,但这并不能改变它们仍然是表面积 API 的一部分的事实。一个可以很容易地与DI容器一起使用,但也可以在没有DI容器的情况下使用的库比必须与DI容器一起使用的库更好。即使使用 DI 容器,复杂库通常也会直接引用实现类型以进行自定义注册。如果采用注入可选依赖项的策略,则每次开发人员想要将日志记录添加到新类型时,都会更改公共接口。

更好的方法是遵循大多数日志记录库(如 Serilog、log4net、NLog 等)建立的模式,并通过记录器工厂获取记录器(例如 )。这还有其他好处,例如利用每个相应库的过滤功能。有关此主题的更多讨论,请参阅此文章Log.ForContext<MyClass>();


推荐