【问题标题】:Logging and Dependency Injection日志记录和依赖注入
【发布时间】:2013-07-10 22:19:22
【问题描述】:

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

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

现在我遇到了在申请过程中记录一些信息的问题。我不谈论以方法调用等方式进行的一般日志记录。我知道AOP 并且我可以用它来进行方法调用跟踪等。

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

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

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

【问题讨论】:

  • 进行“手动记录”时会记录哪些信息?
  • 我所知道的所有日志框架都对具体的日志实例使用单例工厂。如果你想灵活地使用记录器实现,你可以使用像 slf4j 这样的记录外观。
  • 我不同意@Pyranja。如果您想要更大的灵活性,请定义您自己的ILogger 抽象(只包含一个成员),将您的日志框架隐藏在该抽象后面并将该接口注入需要它的类中。这使您可以正确地对该代码进行单元测试并隐藏第三方依赖项。看看this answer。
  • 我认为我们必须同意不同意。在我看来,日志记录是一项通用任务,使用自定义抽象不会带来可观的好处。像 slf4j 这样已建立的日志门面已经提供了这样的抽象(如果不喜欢静态工厂,也可以注入)。除非有特殊要求,否则我希望自定义记录器界面与现有的记录器界面非常相似,并且可以互换。将外观连接到具体的日志记录实现的繁重工作已经完成!
  • @Steven:调试信息,例如文件解析(来自哪个文件)。但随着我的思考,我意识到其中一些与 SRP 冲突

标签: java logging dependency-injection service-locator


【解决方案1】:

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

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

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

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

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

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

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

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

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

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

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

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

【讨论】:

  • 非常感谢您提供的链接。这对理解我遇到的设计问题有很大帮助。因此,我理解对于例外情况它没有意义,并且使用装饰器会更好。对于记录调试信息,仍然存在上面的问题。
  • @SimFirehawk,虽然工程师可能不会阅读日志,但其他系统可能会对其进行处理,以便更容易访问日志中包含的信息。在这样的环境中,为更重要的日志记录编写单元测试是有意义的(例如,确保在特定场景下记录某些内容)。这样的测试可能需要双倍的记录器。
【解决方案2】:

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

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 2010-11-13
    • 1970-01-01
    • 2010-10-06
    • 2021-08-03
    • 2017-07-17
    • 2020-09-03
    • 2023-02-16
    • 1970-01-01
    • 2011-12-15
    相关资源
    最近更新 更多