Java类库设计之日志框架选择

Java日志框架现状

Java日志框架的故事说来话长,做过开发的一定遇到过slf4j,log4j,logback,commons-logging,log4j2。=也一定见过这些jar包,什么log4j.jar,slf4j-api.jar,slf4j-log4j12.jar,logback-classic.jar,logback-core.jar等等。以前看到他们就头大,傻傻分不清楚,到现在搞清楚了也就这么回事,可以具体看看slf4j的实现和适配是怎么做的。

要搞清楚他们的关系,就得从Java的log历史说起。在Java的初期,是没有好用的log系统的,只有System.out.print和System.out.println可以使用。如果要设置日志级别,指定我日志的格式,指定日志的路径。都得你业务方自己去弄。后来一个大牛就写了log4j这个东西,就是现在使用的日志框架的雏形。通过配置可以很方便的完成上面的那些难题。这个东西推出之后,受到了热烈的欢迎,成了当时日志的主流。Java官方看不下去了,“借鉴”当时的设计,很快推出了自己的日志框架,并且集成到JDK中,可惜为时已晚。

潘多拉的魔盒打开之后,就呈现出一股不可收拾的局面。随着日志框架越来越多,写日志就变得越来越混乱。这个时候大牛一看,不行啊,马上推出了slf4j,所谓的门面模式。这个东西只提供一个API,你使用这个API之后你背后用的是log4j还是JDK自带的已经不重要了。就算你以前用的是log4j,你也可以很容易的切换到遵循了这个规则的其他的日志框架。后来干脆另起炉灶,推出了logback,这个东西干脆直接使用的就是slf4j的api,连适配层都省了。

为什么要选择一个合适的日志框架

在设计SDK这种Java类库的过程中,我们总是期望用户不要配置,不要改代码,就能把我们集成到自己的系统中,对业务方的入侵越轻量越好。但是,如果用户没有对日志进行配置,我们的sdk集成到业务方系统中之后,我们sdk产出的日志就会和应用日志打印在一起。例如在spring boot的web应用中,就打印在application.log中。而且日志的级别都是统一配置的,我们无法决定和左右。这就给我们的诊断和监控带来了负担。因为监控的时候,我不可能监控所有的application.log,我只关心我的sdk是否工作正常。

当然这也是一种特殊的场景,如果你的SDK就是提供一种算法,其实打在应用日志里甚至控制台里也没有什么问题。但是如果你的SDK是一个中间件产品,里面有info,warn也有error.还需要对他们进行一些监控以确定你提供的服务是否正常,我们就需要自己配置我们的日志系统,而不是和业务方(用户)的使用同一套配置。正好我现在就遇到了这么个问题。

那如果我直接在我的SDK里依赖一个日志框架呢?比如我直接使用log4j的话,又会有什么问题呢?这样带来的后果,可能是灾难性的。因为以前经常在开发的时候,要解决jar包冲突的问题,尤其是不同的日志框架之间的冲突,弄得人痛不欲生。这种冲突往往需要非常仔细的排查,才能发现谁和谁冲突,然后把他排除掉,是非常浪费时间的行为。我们在设计SDK的肯定不希望给用户带来这么大麻烦。

可选方案

方案一: 直接依赖使用log4j或者logback.

这种方案的优点是SDK的开发很简单,直接new 一个Logger,然后setAppdener等这些就可以了。但是他带来的问题就是会给集成我们类库的业务方带来冲突的问题。如果集成类库的“宿主”应用原来使用的是logback,而你选择的是log4j,那就产生了冲突了。业务方有千万个,你无法知道他使用的到底是哪一个,对不对。和一部分人匹配就会和另外一部分冲突,顾前不顾后。

方案二:使用slf4j

这个使用起来也很方便,LoggerFactor.getLogger("name")就得到了Logger对象,然后就可以记日志了。但是他的问题在于,我们无法定制日志配置。必须公用用户的日志配置,包括日志的格式,滚动规则,路径都得拜托用户去配置,这是不现实的。

例如在dubbo中,他的日志就交给用户决定。用户需要告知dubbo自己使用的是什么日志框架,并且在xml里配置appender和日志滚动规则。

slf4j为什么没有提供这种能力呢?这个就是他的设计灵魂,所以他必然是不可能给你提供这个功能的,我们可以看看他的官方说明。

Should my library attempt to configure logging?
Embedded components such as libraries not only do not need to configure the underlying logging framework, they really should not do so. They should invoke SLF4J to log but should let the end-user configure the logging environment. When embedded components try to configure logging on their own, they often override the end-user's wishes. At the end of the day, it is the end-user who has to read the logs and process them. She should be the person to decide how she wants her logging configured.

很简单,他的设计哲学是说虽然你作为类库被人集成,但是系统是业务方的系统,打出来的日志也必然是业务方自己去看,跟你没有什么关系。这个当然是非常有道理的,但是我们的需求就是很小众,需要自己设定日志路径这种能力。

方案三: 自己写日志

还有一种方式自己写日志,自己open 一个file,然后将日志写入到问题中。但是这个代价就大了,不仅工作量大,而且很容易踩到坑。相当于把log4j又重新写了一遍。我也见过这种方式的,例如阿里的Eagleeye就是这么干的。

最终方案: 适配宿主系统的日志框架

目前我看到的最完美的做法就是阿里中间件的做法,他对宿主的日志框架进行了检测,看看他到底使用的是哪个日志框架,然后根据宿主的框架来选择自己使用哪个。目前内部的HSF和Diamond都是这么做的。而早期的configserver,则直接依赖的log4j的包。

1.检测宿主系统的日志框架

检测使用的是Class.forName来检测类是否存在,如果是slf4j的话org.slf4j.impl.StaticLoggerBinder肯定存在的,否则他会抛出ClassNotFoundException。而如果是log4j的话org.apache.log4j.Level是存在的。总结如下:

slf4j : org.slf4j.impl.StaticLoggerBinder

log4j:org.apache.log4j.Level

log4j2:org.apache.logging.log4j.core.Logger

否则就是找不到了,那就抛出异常或者是其他。

2.获取日志对象

使用宿主系统的框架来getLogger,具体的是, 这可以去翻看各个框架的API了,最方便的还属于依赖了slf4j的了。

slf4j:org.slf4j.LoggerFactory.getLogger(name)

log4j:LogManager.getLogger(name)

 

3. 配置自己的日志配置.

如果是slf4j的话,就需要检测你拿到的logger到底是由谁实现的,才能用他的api来配置你appender.

logback: ch.qos.logback.classic.Logger

log4j: org.slf4j.impl.Log4jLoggerAdapter

log4j2: org.apache.logging.slf4j.Log4jLogger

接下来,你就可以设置你日志配置了,包括怎么滚动,级别,日志路径等,例如log4j的可以这么设置:

//设置按天滚动

DailyRollingFileAppender appender = new DailyRollingFileAppender();

appender.setFile(); //设置日志路径

loggger.setAppender(appender);//将日志配置设置进去

 

后面你只需要用这个logger对象进行日志记录就行了 logger.info("xxxxx");

打包不打包log4j,logback等

还有一个黑科技也必须要提一下,因为你使用了log4j和logback的api来写代码,那你不能sdk里依赖这两个吧,这样不就直接冲突了吗?我们可以在写代码的时候依赖,在打包发布我们的jar包的时候把这些都剔除掉,如果java走不到这步逻辑,由于java的类是按需加载,所以也不会报错。

例如在slf4j的绑定的过程中,在源码里我们可以看到他使用的是StaticLoggerBinder.java这个类来绑定的,但是我们在slf4j-api.jar包里,这个类是不存在的。

而且这个StaticLoggerBinder类的代码也明确说这个类不应当被打包到slf4j-api.jar:

private StaticLoggerBinder() {
    throw new UnsupportedOperationException(
        "This code should have never made it into slf4j-api.jar");
}

这个其实在写代码的时候,使用这些做一个占位符,然后在打包的时候将他们删除,让最终日志框架来实现。这个类可以在slf4j-log4j12, slf4j-jkd14, slf4j-jcl的项目中可以找到类似的”org/slf4j/impl/StaticLoggerBinder.class”

 

 

另外,我觉得还有一种做法是使用JDK自带的java.util.logger,不过这个没有提供日志滚动的功能,用得确实不多,不知道有没有什么坑。

 

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注