13

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

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

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

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

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

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

4

2 回答 2

22

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

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

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

甚至当你想记录调试信息时,有没有人会读到这个?您最终会得到包含数千行没有人会阅读的日志文件。如果他们阅读它,他们必须浏览所有这些文本行并通过它进行复杂的正则表达式搜索以获取他们正在寻找的信息。

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

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

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

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

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

确实如此,你最终会得到带有太多参数的构造函数。但是不要责怪记录器,责怪你的代码。您在这里违反了单一责任原则。您可以通过静态外观调用它来“隐藏”这种依赖关系,但这不会降低依赖关系的数量和类的整体复杂性。

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

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

于 2013-07-12T12:14:43.670 回答
0

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

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

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

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

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

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

于 2020-07-28T15:57:14.470 回答