什么时候能用 log 对象的静态引用

这是一篇2011年1月9日开始至今没有完成的翻译,十多年了,今天我在整理草稿时发现它还躺在这里,然后我尝试点击查看英文原文试图完成它时,原来的英文原文地址已经不存在了,然后就去搜索标题试图找到英文原文,发现已经有朋友翻译过它了:什么时候使用静态的Log对象。那这篇就不继续了。


英文原文:When Static References to Log objects can be used When Static References to Log objects can be used

有两个非常常见的使用日志的模式:

public class Foo {
 private Log log = LogFactory.getLog(Foo.class);
 ....
}

public class Foo {
 private static Log log = LogFactory.getLog(Foo.class);
 ....
}

静态限定符的使用在某些情况下是有益的。然而,在其它一些情况下,它却确确实实是一个糟糕的主意,并且可能会有意想不到的后果。本页面描述了各个方案在什么情况下是合适的。

静态的问题

技术上,使用静态的结果是很明显的:在这个类的所有实例间共享这个仅有的 Log 引用。这很明显节省了内存;不管创建多少个实例都只需要一个引用(4 或者 8 个字节)。并且 CPU 角度也非常有效率;查找 Log 实例只需要在这个类第一次被引用的时候做一次。

当编写一个独立的应用程序代码时,使用静态是个好主意。

然而,当相关 java 代码是一个可能部署在某种容器(比如 J2EE 服务器)里的类库时,那么问题就来了。容器创建一个 java ClassLoader 对象的分级结构,比如每个“应用”所部署的容器有自己的 ClassLoader 但又有一些共享的 ClassLoader 作为所有部署着的“应用”的祖先,是很常见的事。这种情况下,当一个持有了 Log 实例的静态引用的类被部署在“应用程序”级(比如在一个 servlet 或 j2ee 容器的“webapp”级),这还没有问题;如果多个应用部署了这个有问题的类,那么他们每个都有这个类的一份副本,并且和其它应用没有交互。

然而考虑到这样一种情况,当一个使用了“private static Log log = …”的类被一个 ClassLoader 部署为多个看上去像独立的“应用”的祖先。这种情况下,log 成员仅被初始化一次,因为只有一个这个类的副本。这个初始化(典型地)发生在任何代码第一次试图实例化或调用一个静态方法时。当这个类的初始化发生时,这个 log 成员应该设置成什么?有以下选择:

引用属于“容器”层配置的 Log 对象,而不和任何“应用”关联。

引用在层次结构上和当前应用相关的应用里配置的 Log 对象

引用一个“代理”对象,每次调用 log 方法时都去找出当前应用,并委派给其下的 Log 对象

第一个选择是说日志不能配置在每个应用层。无论日志配置如何设置都会将每个应用的日志设成和容器里配置的一样,所有的从这些应用里的输出都会合并到一块。这是一个大问题。

第二个选择是说 Log 对象将配置成和第一个被调用的应用里配置的。容器里的其它应用将把它们的输出发送到第一个应用配置的目标里。显而易见,这是一个大问题。

第三个选择允许每个应用都有各自的日志配置,并正确地过滤和输出需要的日志信息。可是付出的性能损失非常大,这不是一个可以接受的解决方案。

注意,我们还没有讨论到“Thread Context ClassLoaders”或者其它的技术要点。这个问题不详述了,只给出一个概念性的结论:当 Log 对象是在多个应用间共享时,是不可能做到各个应用各配各的的,并且有性能问题。

这个问题真正的根源是数据(通过类的静态成员)在所谓独立的“应用”间共享。如果没有类是在应用间“共享”的,那么问题就不存在了。然而,它似乎(我个人的经验)容器提供商继续鼓励使用共享类,并且应用程序开发人员继续在使用它。

另一个解决方法是:在任何可能部署到共享 classpath 的代码里避免使用 Log 对象的“静态”引用。

SLF4J 有这个问题吗?

有。SLF4J 也正受着上述问题的困扰,并且有着相同的建议:在可能部署到共享 classpath 的代码中避免使用 log 对象的静态引用。

这里有几个可能的场景。首先我们需要定义一个术语“TCCL 感知”。日志类库是“TCCL 感知”的,当它初始化代码时使用 Thread Context ClassLoader 来定位配置文件,而不是类库自身里的类的 ClassLoader。举个例子,在它的标准形式里,log4j 是不能“TCCL 感知”的。不过它可以通过下面描述的一个“Contextural RepositorySelector”来做到“TCCL 感知”:

http://www.qos.ch/logging/sc.jsp

现在来看看一些可能的场景:

假设 SLF4J 是部署在一个共享的层,有一个使用了静态 Logger 引用的类在共享的 classpath 里,也有一个使用了静态 logger 的类在应用程序 classpath 里。当日志类库不是 TCCL 感知的,那么就仅读取在“共享” classpath 里的日志配置,所有部署着的应用程序都输出调试信息到全局配置里了。输出是正确的,但是就不能仅为一个应用开启调试日志了,并且从所有的应用程序里来的所有输出都是混在一起的。当日志类库是 TCCL 感知的,那么就可以针对各个应用程序配置自己的日志了。另外,来自这些类的输出都输出在应用程序层,这是正确的。然而,那些部署在共享层的类将有它们自己的 Log 对象的初始化,使用第一个调用该类的应用程序的配置;当其它应用程序调用相同的类时其日志输出将跑到第一个应用程序里去了。不好。

如果 SLF4J 同时部署在共享和应用程序层,并且首先先加载了一个父层的,那么行为和上面的一样。但是如果先加载的是应用层的,如果日志类库是 TCCL 感知的那么结果还可以承受。然而如果是先加载的应用层的,并且日志类库不是 TCCL 感知的,那么结果就是这样:从共享类里的输出仅能配置在全局,所有应用层的输出都跑到一起去了。

不幸的是,在大多数情况下,类库代码的作者不能指定类库用户说代码必须部署在哪一层,必须使用怎样的加载顺序,或者应该用什么行为的日志类库。因此有必要和 commons-loggings 一样避免使用 SLF4J logger 的静态引用。

无论如何,不当使用 SLF4J 所导致的问题确实要比 commons-logging 少。原因如下:

当前 TCCL 感知的日志类库并不多,并且目前部署在共享 classpath 里的类库相对较少地使用了 SLF4J。

commons-logging 本身是 TCCL 感知的,允许应用层的日志类库配置是非 TCCL 感知的;实际上,使用 commons-logging 时每个日志类库都可以 TCCL 感知。commons-logging 也经常被很多部署在共享 classloader 的类库所使用。

请注意,这段不是想要批评 SLF4J。如前所述,这个问题是因为应用程序间使用共享静态日志引用的隔离问题造成的冲突,并且何况在这些场景下 SLF4J 和 commons-logging 一样面临着这个尴尬。

翻译未完成。要阅读完整版本,请看这篇翻译:什么时候使用静态的Log对象

#java, #logging