我有生以来第一次发现自己处于编写开源 Java API 的位置。希望被包括在许多其他项目中。
对于日志记录,我(实际上是与我一起工作的人)一直使用 JUL (java.util.logging
) 并且从未遇到任何问题。但是现在我需要更详细地了解我应该为我的 API 开发做些什么。我已经对此进行了一些研究,并且根据我获得的信息,我变得更加困惑。因此这篇文章。
因为我来自七月,所以我对此有偏见。我对其余部分的了解并不大。
根据我所做的研究,我提出了人们不喜欢 JUL 的以下原因:
“早在 Sun 发布 JUL 之前,我就开始使用 Java 进行开发,我继续使用 logging-framework-X 比学习新东西更容易”。唔。我不是在开玩笑,这实际上是人们所说的。有了这个论点,我们都可以做 COBOL。 (但我当然可以将其视为一个懒惰的家伙)“我不喜欢 JUL 中的日志记录级别的名称”。好吧,说真的,这还不足以成为引入新依赖项的理由。 “我不喜欢 JUL 输出的标准格式”。唔。这只是配置。您甚至不必在代码方面做任何事情。 (的确,在过去,您可能必须创建自己的 Formatter 类才能正确处理)。 “我使用其他也使用 logging-framework-X 的库,所以我认为使用它更容易”。这是一个循环论证,不是吗?为什么“每个人”都使用 logging-framework-X 而不是 JUL? “其他人都在使用 logging-framework-X”。对我来说,这只是上述情况的一个特例。大多数人并不总是正确的。
所以真正的大问题是为什么不是 JUL?我错过了什么?日志记录立面(SLF4J、JCL)存在的理由是历史上已经存在多个日志记录实现,而在我看来,其原因实际上可以追溯到 7 月之前的时代。如果 JUL 是完美的,那么日志立面将不存在,还是什么?更令人困惑的是,JUL 在某种程度上本身就是一个外观,它允许交换处理程序、格式化程序甚至 LogManager。
与其采用多种方式来做同样的事情(记录),我们难道不应该首先质疑为什么它们是必要的吗? (看看这些原因是否仍然存在)
好的,到目前为止,我的研究导致了一些我可以看到可能是 JUL 真正问题的事情:
表现。有人说 SLF4J 的性能优于其他性能。在我看来,这似乎是过早优化的情况。如果您需要每秒记录数百兆字节,那么我不确定您是否走在正确的道路上。 JUL 也在不断发展,您在 Java 1.4 上所做的测试可能不再适用。您可以在此处阅读有关它的信息,并且此修复程序已将其纳入 Java 7。许多人还谈到了日志记录方法中字符串连接的开销。然而,基于模板的日志记录避免了这种成本,它也存在于 JUL 中。就我个人而言,我从来没有真正编写过基于模板的日志记录。太懒了。例如,如果我使用 JUL 执行此操作: log.finest("Lookup request from username=" + username + ", valueX=" + valueX + ", valueY=" + valueY));我的 IDE 会警告我并询问是否应将其更改为:log.log(Level.FINEST, "Lookup request from username={0}, valueX={1}, valueY={2}", new Object[] {用户名,valueX,valueY}); ..我当然会接受。许可授予 !谢谢您的帮助。所以我自己实际上并没有编写这样的语句,这是由 IDE 完成的。总之,关于性能问题,我没有发现任何表明 JUL 的性能与竞争对手相比不好的东西。来自类路径的配置。开箱即用的 JUL 无法从类路径加载配置文件。只需几行代码就可以做到这一点。我明白为什么这可能很烦人,但解决方案简短而简单。输出处理程序的可用性。 JUL 带有 5 个开箱即用的输出处理程序:控制台、文件流、套接字和内存。这些可以扩展或编写新的。例如,这可能是写入 UNIX/Linux 系统日志和 Windows 事件日志。我个人从未有过这个要求,也没有见过它使用过,但我当然可以理解为什么它可能是一个有用的功能。例如,Logback 带有一个用于 Syslog 的附加程序。尽管如此,我仍然认为 99.5% 的输出目的地需求都被 JUL 开箱即用的内容所覆盖。特殊需求可以通过 JUL 之上的自定义处理程序来满足,而不是在其他东西之上。对我来说,没有任何迹象表明为 JUL 编写 Syslog 输出处理程序比为另一个日志框架编写的时间更长。
我真的很担心我忽略了一些东西。除了 JUL 之外,日志门面和日志实现的使用如此广泛,以至于我不得不得出结论,就是我不明白。恐怕这不是第一次了。 :-)
那么我应该如何处理我的 API 呢?我希望它成功。我当然可以“顺其自然”并实现 SLF4J(这似乎是当今最流行的),但为了我自己,我仍然需要确切地了解今天的 JUL 到底有什么问题,这保证了所有的模糊性?为我的图书馆选择 JUL 会破坏自己吗?
测试性能
(nolan600 于 2012 年 7 月 7 日添加的部分)
Ceki 下面有一个关于 SLF4J 的参数化比 JUL 快 10 倍或更多的参考。所以我开始做一些简单的测试。乍一看,这种说法肯定是正确的。以下是初步结果(但请继续阅读!):
执行时间 SLF4J,后端 Logback:1515
执行时间 SLF4J,后端 JUL:12938
执行时间7月:16911
上面的数字是毫秒,所以越少越好。所以 10 倍的性能差异首先实际上非常接近。我的第一反应是:太多了!
这里是测试的核心。可以看出一个整数和一个字符串在一个循环中构造,然后在 log 语句中使用:
for (int i = 0; i < noOfExecutions; i++) {
for (char x=32; x<88; x++) {
String someString = Character.toString(x);
// here we log
}
}
(我希望 log 语句同时具有原始数据类型(在本例中为 int)和更复杂的数据类型(在本例中为 String)。不确定它是否重要,但你有它。)
SLF4J 的日志语句:
logger.info("Logging {} and {} ", i, someString);
JUL 的日志语句:
logger.log(Level.INFO, "Logging {0} and {1}", new Object[]{i, someString});
在实际测量完成之前,JVM 已通过执行一次相同的测试“预热”。在 Windows 7 上使用了 Java 1.7.03。使用了最新版本的 SLF4J (v1.6.6) 和 Logback (v1.0.6)。标准输出和标准错误被重定向到空设备。
但是,现在小心,事实证明 JUL 大部分时间都花在 getSourceClassName()
上,因为 JUL 默认会在输出中打印源类名称,而 Logback 不会。所以我们在比较苹果和橙子。我必须再次进行测试并以类似的方式配置日志记录实现,以便它们实际上输出相同的内容。然而,我确实怀疑 SLF4J+Logback 仍然会排在首位,但与上面给出的初始数字相去甚远。敬请关注。
顺便说一句:该测试是我第一次实际使用 SLF4J 或 Logback。一次愉快的经历。当你刚开始的时候,JUL 肯定不会那么受欢迎。
测试性能(第 2 部分)
(nolan600 于 2012 年 7 月 8 日添加的部分)
事实证明,在 JUL 中如何配置模式对性能并不重要,即它是否包含源名称。我尝试了一个非常简单的模式:
java.util.logging.SimpleFormatter.format="%4$s: %5$s [%1$tc]%n"
这根本没有改变上述时间。我的分析器显示,即使这不是我的模式的一部分,记录器仍会花费大量时间调用 getSourceClassName()
。图案无所谓。
因此,我在性能问题上得出结论,至少对于经过测试的基于模板的日志语句,JUL(慢)和 SLF4J+Logback(快)之间的实际性能差异似乎大约是 10 倍。就像Ceki说的那样。
我还可以看到另一件事,即 SLF4J 的 getLogger()
调用比 JUL 的同上要贵得多。 (如果我的分析器准确,则为 95 毫秒对 0.3 毫秒)。这是有道理的。 SLF4J 必须在底层日志实现的绑定上做一些时间。这并不吓到我。这些调用在应用程序的生命周期中应该很少见。牢度应该在实际的日志调用中。
定论
(nolan600 于 2012 年 7 月 8 日添加的部分)
感谢您的所有回答。与我最初的想法相反,我最终决定将 SLF4J 用于我的 API。这是基于许多事情和您的输入:
它提供了在部署时选择日志实现的灵活性。在应用程序服务器中运行时,JUL 的配置缺乏灵活性的问题。 SLF4J 肯定比上面详述的要快得多,特别是如果你将它与 Logback 结合使用。即使这只是一个粗略的测试,我有理由相信在 SLF4J+Logback 上的优化要比在 JUL 上付出更多的努力。文档。 SLF4J 的文档更加全面和精确。模式灵活性。当我做测试时,我开始让 JUL 模仿 Logback 的默认模式。此模式包括线程的名称。事实证明,JUL 不能开箱即用。好的,直到现在我还没有错过它,但我认为它不应该是日志框架中缺少的东西。时期!今天大多数(或许多)Java 项目都使用 Maven,因此添加依赖项并不是什么大事,尤其是如果该依赖项相当稳定,即不会不断更改其 API。这对于 SLF4J 来说似乎是正确的。 SLF4J jar 和朋友的体积也很小。
所以发生的奇怪的事情是,在使用了 SLF4J 之后,我实际上对 JUL 非常不满。我仍然很遗憾 JUL 必须这样。 JUL 远非完美,但可以胜任。只是不够好。以 Properties
为例也可以这样说,但我们不考虑将其抽象化,以便人们可以插入他们自己的配置库和你拥有的东西。我认为原因是 Properties
出现在条形上方,而今天的 JUL 则相反……过去它出现为零,因为它不存在。
InternalLoggerFactory.java
。
java.lang.System.Logger
,这是一个 接口,只要该框架被捕获,它就可以重定向到您想要的任何实际日志框架up 并提供该接口的实现。结合模块化,如果您更喜欢不同的框架,您甚至可以使用不包含 java.util.logging
的捆绑 JRE 部署应用程序。
免责声明:我是 log4j、SLF4J 和 logback 项目的创始人。
偏爱 SLF4J 有客观原因。一方面,SLF4J 允许最终用户自由选择底层日志框架。此外,精明的用户往往更喜欢 logback which offers capabilities beyond log4j,而 jul 则远远落后。对于某些用户来说,功能方面的 jul 可能就足够了,但对于许多其他用户来说,它就不是了。简而言之,如果日志记录对您很重要,您可能希望使用带有 logback 的 SLF4J 作为底层实现。如果日志记录不重要,那么 jul 就可以了。
但是,作为 oss 开发人员,您需要考虑用户的偏好,而不仅仅是您自己的偏好。因此,您应该采用 SLF4J 并不是因为您确信 SLF4J 比 jul 更好,而是因为目前(2012 年 7 月)大多数 Java 开发人员更喜欢 SLF4J 作为他们的日志 API。如果您最终决定不关心民意,请考虑以下事实:
喜欢 jul 的人这样做是出于方便,因为 jul 与 JDK 捆绑在一起。据我所知,没有其他支持 jul 的客观论据,您自己对 jul 的偏好就是这样,一种偏好。
因此,将“确凿的事实”置于舆论之上,虽然看似勇敢,但在这种情况下却是一个逻辑谬误。
如果仍然不相信,JB Nizet 提出了一个额外的有力论据:
除了最终用户可能已经为他自己的代码或使用 log4j 或 logback 的另一个库完成了此自定义。 jul 是可扩展的,但是必须扩展 logback、jul、log4j 和上帝只知道其他哪个日志框架,因为他使用四个库,使用四个不同的日志框架很麻烦。通过使用 SLF4J,您允许他配置他想要的日志框架,而不是您选择的那个。请记住,一个典型的项目使用无数的库,而不仅仅是您的。
如果出于某种原因您讨厌 SLF4J API 并且使用它会扼杀您工作中的乐趣,那么请务必选择 jul 毕竟,有办法redirect j.u.l to SLF4J。
顺便说一句,jul 参数化至少比 SLF4J 慢 10 倍,这最终会产生明显的差异。
java.util.logging 是在 Java 1.4 中引入的。在此之前有记录的用途。这就是存在许多其他日志记录 API 的原因。这些 API 在 Java 1.4 之前被大量使用,因此拥有很大的市场份额,而在 1.4 发布时不仅下降到零。七月的开始并没有那么好。你提到的许多事情在 1.4 中要糟糕得多,在 1.5 中才变得更好(我猜在 6 中也是如此,但我不太确定)。 JUL 不太适合在同一个 JVM 中具有不同配置的多个应用程序(想想不应该交互的多个 Web 应用程序)。 Tomcat 需要跳过一些障碍才能使其正常工作(如果我理解正确,则有效地重新实现 JUL)。你不能总是影响你的库使用的日志框架。因此,使用 SLF4J(实际上只是其他库之上的一个非常薄的 API 层)有助于保持整个日志世界的一致图景(因此您可以决定底层日志框架,同时仍然在同一系统中拥有库日志)。库不能轻易改变。如果以前版本的库曾经使用 logging-library-X,则它不能轻易切换到 logging-library-Y(例如 JUL),即使后者显然更胜一筹。该库的任何用户都需要学习新的日志记录框架并(至少)重新配置他们的日志记录。这是一个很大的禁忌,尤其是当它没有给大多数人带来明显的收益时。
说了这么多,我认为 JUL 至少是当今其他日志记录框架的有效替代品。
恕我直言,使用像 slf4j 这样的日志外观的主要优点是您可以让库的最终用户选择他想要的具体日志实现,而不是将您的选择强加给最终用户。
也许他在 Log4j 或 LogBack(特殊格式化程序、附加程序等)上投入了时间和金钱,并且更喜欢继续使用 Log4j 或 LogBack,而不是配置 jul。没问题:slf4j 允许这样做。在 jul 上使用 Log4j 是一个明智的选择吗?也许,也许不是。但你不在乎。让最终用户选择他喜欢的东西。
我开始,就像我怀疑的你一样,使用 JUL,因为它是最容易立即开始的。然而,多年来,我开始希望我能多花一点时间来选择。
我现在的主要问题是我们有大量用于许多应用程序的“库”代码,它们都使用 JUL。每当我在 web 服务类型的应用程序中使用这些工具时,日志记录就会消失或进入不可预测或奇怪的地方。
我们的解决方案是在库代码中添加一个外观,这意味着库日志调用不会更改,而是动态重定向到任何可用的日志记录机制。当包含在 POJO 工具中时,它们会被定向到 JUL,但当部署为 Web 应用程序时,它们会被重定向到 LogBack。
我们的遗憾 - 当然 - 是库代码不使用参数化日志记录,但现在可以在需要时对其进行改造。
我们使用 slf4j 来构建外观。
我在 logback-1.1.7 上针对 slf4j-1.7.21 运行了 jul,输出到 SSD、Java 1.8、Win64
jul 运行 48449 毫秒,1M 循环的 logback 27185 毫秒。
尽管如此,对我来说,更快的速度和更好的 API 不值得 3 个库和 800K。
package log;
import java.util.logging.Level;
import java.util.logging.Logger;
public class LogJUL
{
final static Logger logger = Logger.getLogger(LogJUL.class.getSimpleName());
public static void main(String[] args)
{
int N = 1024*1024;
long l = System.currentTimeMillis();
for (int i = 0; i < N; i++)
{
Long lc = System.currentTimeMillis();
Object[] o = { lc };
logger.log(Level.INFO,"Epoch time {0}", o);
}
l = System.currentTimeMillis() - l;
System.out.printf("time (ms) %d%n", l);
}
}
和
package log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogSLF
{
static Logger logger = LoggerFactory.getLogger(LogSLF.class);
public static void main(String[] args)
{
int N = 1024*1024;
long l = System.currentTimeMillis();
for (int i = 0; i < N; i++)
{
Long lc = System.currentTimeMillis();
logger.info("Epoch time {}", lc);
}
l = System.currentTimeMillis() - l;
System.out.printf("time (ms) %d%n", l);
}
}
logger.info()
的单参数重载。因此,您故意削弱 jul 性能以弥补 slf4j 界面中的缺陷。相反,您应该按照惯用的编码方式对这两种方法进行编码。