替代3)空构件
另一种方法是依靠一个空的commons-logging.jar构件。这个聪明的办法首先 由Erik van Oosten想出和最初支持。
空构件可从一个http://version99.qos.ch高可用性Maven仓库,复制在位于不同地域的多个主机。
下面的声明添加version99库设定的Maven的搜索远程资源库。 该存储库中包含的commons-logging和log4j的空构件。 顺便说一句,如果你使用的version99库,请在<version99 AT qos.ch>给我们一行。
1 |
<repositories> |
2 |
<repository>
|
3 |
<id>version99</id>
|
4 |
<!-- highly available repository serving empty artifacts -->
|
5 |
<url>http: //version99.qos.ch/</url>
|
6 |
</repository>
|
7 |
</repositories> |
在你的项目里的<dependencyManagement>部分,声明99-empty版本中的commons-logging的将为commons-logging指向所有传递的依赖以输入版本9999-empty,从而很好地解决的commons-logging排斥问题。对于commons-logging的类将由jcl-over-slf4j提供。 以下行声明的commons-logging版本99-empty(在依赖管理部分),并声明jcl-over-slf4j作为一个依赖。
01 |
<dependencyManagement> |
02 |
<dependencies>
|
03 |
<dependency>
|
04 |
<groupId>commons-logging</groupId>
|
05 |
<artifactId>commons-logging</artifactId>
|
06 |
<version><strong> 99 -empty</strong></version>
|
07 |
</dependency>
|
08 |
... other declarations...
|
09 |
</dependencies>
|
10 |
</dependencyManagement>
|
11 |
12 |
<!-- Do not forget to declare a dependency on jcl-over-slf4j in the -->
|
13 |
<!-- dependencies section. Note that the dependency on commons-logging -->
|
14 |
<!-- will be imported transitively. You don't have to declare it yourself. -->
|
15 |
<dependencies>
|
16 |
<dependency>
|
17 |
<groupId>org.slf4j</groupId>
|
18 |
<artifactId>jcl-over-slf4j</artifactId>
|
19 |
<version> 1.7 . 21 </version>
|
20 |
</dependency>
|
21 |
... other dependency declarations
|
22 |
</dependencies>
|
关于SLF4J API
1.为什么日志器接口的打印方法不接收对象类型的消息,但只接收String类型消息?
在SLF4J 1.0beta4版里,Logger interface 中例如debug(),info(),warn()的打印方法被修改以用于只接收String类型的消息,而不是Object类型的消息。
因此,DEBUG级别的打印方法设置变为:
1 |
debug(Stringmsg); |
2 |
debug(String format,Objectarg); |
3 |
debug(String format,Object arg1,Object arg2); |
4 |
debug(Stringmsg,Throwable t); |
之前,上述方法中的第一个参数是Object类型。
这个改变强制使人认为日志系统是关于装饰和处理String类型消息,而不是关于任何(Object)类型。
同样重要的是,新的一套签名方法提供重载方法间的透明区别,而以前选择 被调用的Java方法,由于Java的重载规则通常总是不易遵循。
很容易犯错误。例如,之前这样写是合法的:
1 |
logger.debug(newException( "some error" ));
|
不幸的是,上述调用不会打印堆栈跟踪的异常。因此,潜在的关键信息片可能会丢失。当第一个参数被限制为String类型时,只有如下方法: debug(Stringmsg,Throwable t);
可用于记录异常,注意,此方法可确保每个日志异常是伴随着一个描述性的消息。
2.我可以输出一个没有伴随消息的异常日志吗?
简言之,不可以。
如果e是一个异常,你想输出一个ERROR级别的异常,你必须增加一个伴随消息。例如:
1 |
logger.debug(newException( "some error" ));
|
你可能合理地辩解不是所有的异常都有一个有含义的消息伴随着它们。此外,良好的异常应该已经包含一个自我解释说明。因此,伴随的消息可以被认为是多余的。
当这些参数是有效参数时,有3个相反的参数也是值得考虑的。首先,在很多场合,但不是所有场合,伴随消息可以很好滴传达有用的信息,在异常里补充描述。通常情况下,在其中异常日志点,开发者获得比在异常被抛出点更多的上下文信息。其次,很难或多或少想象普通消息,例如”Exception caught”, “Exception follows”,可能会用作给错误调用的第一个参数(String msg,Throwable t)。第三,许多日志输出格式显示一行消息,在紧跟着的一行输出异常。因此,消息行没有消息,看起来不一致。
简言之,如果允许用户打印一个没有伴随消息的异常,这将是日志系统的工作:创造一个消息。这实际上是java.util.logging包中的throwing方法(String sourceClass, String sourceMethod, Throwable thrown) 所做的。(它取决于自身的伴随消息是字符串“THROW”。)
开始时可能会奇怪地要求一个伴随消息以记录一个异常。 然而,这是在所有的log4j的衍生系统,如java.util.logging,logkit等,当然还有的log4j本身的通行做法。 如此看来,目前的共识认为需要一个伴随的消息是一个很好的做法(TM)。
3.输出日志(关闭日志)的最快方法是什么?
SLF4J提出一个叫参数化日志的高级特点,可以显著提升有缺陷的日志语句的日志性能。
对于一些Logger logger,写作,
1 |
logger.debug( "Entry number: " +i+ " is " +String.valueOf(entry[i]));
|
引起构造消息参数的开销,就是转换integer i和entry[i]为String,和连接中间的字符串。这一点,不管消息是否记录。
一种可能的方法,以避免构造参数的开销是通过测试中包裹日志语句。例如
1 |
if (logger.isDebugEnabled()){
|
2 |
logger.debug( "Entry number: " +i+ " is " +String.valueOf(entry[i]));
|
3 |
} |
如果logger关闭调试,这样的话就不会引起构造参数的开销。在另一方面,如果logger对DEBUG级别开启,无论logger是否开启,都会引起开销,而且是两次:一次是debugEnable开销,一次是debug里的开销。这是微不足道的开销,因为评估日志器花费的时间不到实际过程中记录语句的时间的1%。
更好的是,使用参数化的消息
这里存在一个基于消息格式的很方便的选择。假设entry是一个对象,你可以这样写:
1 |
Object entry =newSomeObject(); |
2 |
logger.debug( "The entry is {}." , entry);
|
在评价是否记录日志后,只有当结论是肯定的时候,日志器实现将格式化消息,并将’{}’替换为entry的字符值。换句话说,假设日志声明是关闭的,这个格式不会引起构建参数的开销,
下面两行代码将产生确切相同的输出。然而,假设关闭日志声明,第二种形式将将至少超过第一种形式的30倍。
1 |
logger.debug( "The new entry is " +entry+ "." );
|
2 |
logger.debug( "The new entry is {}." , entry);
|
双参数 版本也是可用的。例如,你可以这样写:
1 |
logger.debug( "The new entry is " +entry+ "." );
|
2 |
logger.debug( "The new entry is {}." , entry);
|
如果需要传递3个或更多的参数时,你要使用Object…variant 打印方法。例如,你可以这样写:
1 |
logger.debug( "The new entry is {}. It replaces {}." , entry,oldEntry);
|
这中形式引起隐藏的构建一个Objec[](对象数组)的开销,通常情况下这个开销是很小的。一个和两个参数版本的这种形式不会引起这个隐藏的开销,同时由于这个原因(效率)会完全存在。只有Object…variant的情况下,Slf4j-api会更小/更简洁。
数组形式的参数,包括多维数组,也是支持的。
SLF4J使用自身的消息格式实现,这个和java平台的格式化输出是不一样的。已被证明的事实是:SLF4J的实现平台性能是java平台的十倍,但开销会无标准,弹性也会更小。
摆脱“{}”对
“{}”对叫做格式化锚。它用于指定位置:指定需要用消息样式替换的参数的地方。
SLF4J值关心格式化锚,这就是“{”字符立即跟着字符“}”。因此,如果你的消息包含“{”或“}”字符,不需要做任何特殊处理,除非“}”立即跟着“}”字符。例如:
1 |
logger.debug( "Set {1,2} differs from {}" , "3" );
|
这将打印“Set {1,2} differs from 3”。
在极其罕见的情况下,“{}”对很自然地出现在你的文本中,你希望禁用格式化锚的特殊意义,你需要将“{”字符替换为“\”,就是反斜杠字符。
只有“\”应该替换。不需要替换“}”字符。例如:
1 |
logger.debug( "Set \\{} differs from {}" , "3" );
|
将会打印为“Set {} differs from 3”。注意在java代码里,反斜杠字符需要写为“\\”。
在少见的情况下:“\{}”在消息中出现,你可以使用两个反斜杠,这样它才会保持它原本的含义。例如:
1 |
logger.debug( "File name is C:\\\\{}." , "file.zip" );
|
将会打印“File name is C:\file.zip”。
4.如何打印单独(可能是复杂的)对象的字符串内容日志?
需要以字符串格式记录对象的消息,这是相对少见的格式,那么可以使用适当级别的参数化打印方法。假设complexObject是一个已确定复杂度的对象,对于一个DEBUG级别的日子语句。你可以这样写:
1 |
logger.debug( "{}" ,complexObject);
|
在日志系统已查明日志系统已开启后,它将调用complexObject.toString()方法。否则,complexObject.toString()转换的开销将有力地避免。
5.为什么org.slf4j.Logger接口没有FATAL级别的方法?
标记接口,是org.slf4j包的一部分,指出FATAL级别很大程度上是多余的。如果一个给定的错误需要的注意超过了分配给普通错误的注意,使用特殊指定的标记来简单标记日志语句就可以命名“FATAL”或其他你喜欢的名字.
例如:
01 |
import org.slf4j.Logger;
|
02 |
import org.slf4j.LoggerFactory;
|
03 |
import org.slf4j.Marker;
|
04 |
import org.slf4j.MarkerFactory;
|
05 |
06 |
classBar{ |
07 |
void foo(){
|
08 |
Marker fatal =MarkerFactory.getMarker( "FATAL" );
|
09 |
Loggerlogger=LoggerFactory.getLogger( "aLogger" );
|
10 |
11 |
try {
|
12 |
... obtain a JDBC connection
|
13 |
} catch (JDBException e){
|
14 |
logger.error(fatal, "Failed to obtain JDBC connection" , e);
|
15 |
}
|
16 |
}
|
17 |
} |
虽然标记是SLF4J API的一部分,只有logback支持现成的标记。例如,如果你添加%marker转换字到它的样式中,logback的PatterenLayout将添加标记数据到它的输出口。标记数据可以用于过滤消息 ,或者甚至在单个事务结束后 发出 电子邮件。
在日志框架的组合体中,例如log4j和java.util.logging,是不支持标记的。标记数据将被默默地忽略掉。
相比于五个值,即错误、警告、信息,调试和跟踪,这是所允许的日志级别,为了处理日志语句,标记增加了一个新的具有无限可能值的维度。目前。只有logback支持标记数据。然而,没有什么阻止其他日志框架使用标记数据。
6.为什么TRACED级别的日志只在SLF4J 1.4.0版本介绍?
添加TRACED级别已成为频繁和激烈讨论的要求。通过研究各种项目,我们得出TRACE级别被用于某些类禁用日志输出,而不需要为这些类配置日志。实际上,在log4j和logback里,TRACE级别默认是禁止,这在大部分其他日志系统中也是禁止的。在配置文件里增加合适的指令,相同的结果也可以获得到。
因此,在许多情况下,TRACE级别像DEBUG级别一样带有相同的语意含义。在这些情况下,TRACE级别只是保存一些配置指令。在其他更有趣的场合,TRACE级别相比于DEBUG级别带有不同的含义,Marker对象可以用于传递所需的含义。然而,如果用标记你不被打扰,希望使用比DEBUG级别低的日志级别,那么TRACE级别可以完成这个任务。
注意,虽然评估禁用日志请求的开销是在几纳秒内。在密集的循环中,使用TRACE级别(或此种事务下的任何其他日志级别)是不鼓励的,这里日志请求可能被计算数百万次。如果日志请求是开启的,那么它将用大量输出来淹没目标位置。如果请求被禁用,它将浪费资源。
简言之,尽管我们仍不鼓励使用TRACE级别日志,因为存在可选择的日志级别,或者在许多情况里,TRACE级别的日志请求是浪费的,由于人们一直在问它,我们决定屈服于大众的需求。
7.SLF4J日志API支持I18N(国际化)吗?
是的,作为的1.5.9版本,SLF4J附带一个包叫做org.slf4j.cal10n,它增加了本地化/国际化日志记录 的支持,在内置薄薄的一层CAL10N API .
8.不通过LoggerFactory里的静态方法,可以重新获取日志器吗?
是的, LoggerFactory本质上是一个围绕ILoggerFactory实例的包装。使用ILoggerFactory实例是由SLF4J底层的静态绑定约定确定的。查看LoggerFactory的getSingletong() 方法获取详情 .
然而,没有什么能阻止你使用自己的ILoggerFactory实例。注意,通过调用LoggerFactory.getILoggerFactory() 方法,你也可以获得LoggerFactory类使用的ILoggerFactoyr的一个引用。
因此,如果SLF4J绑定约定不符合你的需求,或者你需要额外的拓展性,那么请考虑使用ILoggerFactoyrj接口,作为你自己创造的日志API的一个可选择接口。
9.存在错误/抛出异常时,可以参数化日志声明吗?
是的,像SLF4J1.6.0版本一样,但这之前的版本里不是这样。在异常出现的地方,SLF4J API支持参数化,假设异常时最后的参数吗。因此,
1 |
String s = "Hello world" ;
|
2 |
try {
|
3 |
Integer i = Integer.valueOf(s);
|
4 |
} catch (NumberFormatException e) {
|
5 |
logger.error( "Failed to format {}" , s, e);
|
6 |
}
|
如期望的一样,将会打印NumberFormatException,同时也包括它的栈追踪信息。Java编译器将调用传入一个字符串和两个对象变量的error方法 。SLF4J按照程序猿的最大可能意图,将像一个抛出异常一样解释NumberFOrmatException实例,而不是简单的忽略掉。
如果异常不是最后的变量,它将被认为是一个平常的对象,它的栈信息将不会被打印。当然,这种情况在实际操作中应该不会发生。
实现SLF4J API
1.如何使我的日志底层框架SLF4J有可兼容性?
为SLF4J添加支持是So Easy的一件事。本质上,你复制一个已存在的绑定,裁剪绑定的一点点就完成这个小伎俩(如下解释)。
假设你的日志系统有日志器的概念,称为MyLogger,你需要为MyLogger到org.slf4.Logger接口提供一个适配器。参考slf4j,slf4j-jdk14,和slf4j-log4j12模块中适配器的例子。
一旦你写完一个合适的适配器,叫MyLoggerAdapter,你需要提供一个工厂类,它实现org.slf4j.IloggerFactory接口。这个工厂应该返回MyLoggerAdater的实例。使MyLoggerFactoyr是你工厂类的名字。
一旦有了名为MyLoggerAdater的适配器,和一个名为MyLoggerFactoyr的工厂类,最后剩下的步骤就是改变StaticLoggerBinder类,这样它会返回一个MyLoggerFactory新实例。你也需要修改loggerFactoryClassStr变量。
对于Marker(标记)或MDC支持,你可以使用已有的NOP(slf4j.nop.jar是sl4f-api.jar其相应的接口实现)实现中的一个。
综上,为你的日志系统创建一个SLF4J绑定,步骤如下:
- 开始复制一个已有的模块,
- 创建一个基于你日志系统和slf4j.Logger接口间的适配器
- 为上一步骤里的适配器创建一个工厂类
- 修改StatciLoggerBinder类,以使用上一步骤创建的工厂类。
2.如何使我的日志系统添加支持Marker接口?
标记设立了一个革命性的概念,这是由logback支持的,但其他已有日志系统是不支持这个的。所以,SLF4J相容日志系统允许忽略用户通过标记数据。
然而,即使标记数据可能被忽略,用户仍必须被允许指定标记数据。否则,用户将不能在支持标记的日志系统和不支持标记的日志系统间切换。
MakrerIgnoringBase类可作为适配器或缺少标志支持的日志系统的本地实现的基类。在MarkerIgnoringBase类里,带有标记数据的方法简单地调用没有标记参数、丢弃作为参数传入的任何标记数据的相关方法,你的SLF4J适配器可以继承MakrerIgnoringBase类来快速实现org.slf4j.Logger里的方法,org.slf4j.Logger需要标记作为第一个参数。
3.SLF4J的版本检查机制如何工作?
SLF4J初始化期间,它执行的版本检查是可选过程。相容的SLF4J实现可选择不不参加,在这种情况下,将不会执行版本检查。
然而,如果SLF4J实现决定参与,那么它需要声明一个叫REQUESTED_API_VERSION的变量,同时得在StaticLoggerBinder类的拷贝里声明。次变量的值应该等于slf4j-api编译的版本。如果实现被更新到新的slf4j-api版本,那么你也需要更新REQUESTED_API_VERSION的值。
对每一个版本,SLF4J API都保持兼容版本的列表。如果请求的版本在兼容列表中没被找到,SLF4J将发出一个单本不匹配警告。即使SLF4J绑定比SLF4J有一个不同的发布,假设每6到12个月,你更新SLF4J版本,你仍可以在没触发版本不匹配的情况下,参与版本检查。例如,logback又一个不同的发布日程,但是仍然会参与版本检查。
对于SLF4J 1.5.5,所有绑定在SLF4J分布,例如slf4j-log4j12,slf4j-simple和slf4j-jdk14里传递,声明REQUESTED_API_VERSION,它的值等于它们的SLF4J版本。它遵循的是,例如如果slf4j-simple-1.5.8与 slf4k-api-1.6.0,jar混合使用,基于1.5.8不在SLF4J版本1.6.x的兼容列表,将会触发版本不匹配警告。
注意,1.5.5之前的SLF4J版本没有版本检查机制。只有slf4j-api-1.5.5.jar以后的版本可触发版本不匹配警告。
关于日志的一般性问题
1.类的日志器成员变量应该声明为静态变量吗?
我们通常推荐的是loggers成员声明为实例变量,而不是静态变量。进一步分析后,在其他方法上,我们不再推荐一种方法。
下面是每一个方法的有点和缺点。
Advantages for declaring loggers as static声明logger为静态变量的优点 | Disadvantages for declaring loggers as static声明logger为静态变量的缺点 |
1.Possible to take advantage of repository selectors even for libraries shared between applications. However, repository selectors only work if the underlying logging system is logback-classic. Repository selectors do not work for the SLF4J+log4j combination.2.IOC-friendly1. 通用、很好建立的惯用语句2. 更小的CPU开销:在主机类初始化过程中,loggers获取和分配只要一次3. 更小的内存开销:logger声明每个类消耗一个引用 | 1.For libraries shared between applications, not possible to take advantage of repository selectors. It should be noted that if the SLF4J binding and the underlying API ships with each application (not shared between applications), then each application will still have its own logging environment.2.not IOC-friendly1.在应用间共享库,不能发挥仓库选择器的优势。应该注意,如果SLF4J绑定和底层API附带每一个应用(不在应用间分享),那么每个应用仍将有自己的日志环境。2.IOC不友善 |
Advantages for declaring loggers as instance variables声明loggers为实例变量的优势 | Disadvantages for declaring loggers as instance variables声明loggers为实例变量的缺点 |
1. 可发挥仓库选择器的优势,甚至应用间的共享库。然而,只有底层日志系统是logback-classic的,仓库选择器才起作用。SLF4J+log4j相结合的方式,仓库选择器将不起作用。 2. IOC友好 |
1.Less common idiom than declaring loggers as static variables2.higher CPU overhead: loggers are retrieved and assigned for each instance of the hosting class
1. 比声明loggers为静态变量有更少的惯用语句 2. 更高的CPU开销:loggers为主机类的每个实例进行获取和分配 3. 更高的内存开销:logger声明主机类的每个实例都消耗一个引用 |
说明
静态日志成员为类的所有实例花费一个单独的变量引用,而实例日志成员将为类的每个实例花费一个变量引用。对简单的类进行数以千计次的初始化可能出现一次明显的区别。
然而,最近的日志系统,例如log4j或logback,为每个运行在应用服务器上的应用提供一个不同的logger上下文。因此,即使一份log4j.jar或logback-classic.jar的单拷贝有效地运用在服务器上,日志系统能够在应用间进行区分,同时为每个应用提供一个不同的日志环境。
更特别的是,每次,通过调用LoggerFactory.getLogger()方法可以重新获得logger,底层日志系统将给当前应用返回适当的实例。请注意,在相同的应用里获取一个给定名字的logger通常会返回相同的logger.对于给定的名字,对不同的应用将返回不同的logger.
如果logger是静态的,当主机类已加载到内存时,那么它将只被获得一次。如果主机类只在一个应用里使用,那么这里没有太多需要关心的地方。然而,如果主机类在几个应用间共享,那么所有共享类的实例将记录到应用的上下文里,这都发生在第一次加载共享类到内存里的时候-几乎是不被用户期望的行为。
不幸的是,对于SLF4J API的非本地实现,名字为slf4j-log4j12,log4j的仓库选择器将不能正常地完成它的工作,因为slf4j-log4j12,是非本地SLF4J绑定,将在字典里存储logger,短路循环依赖logger上下文重新获取。对于本地SLF4J实现方法,例如logback-classic,仓库选择器将如期工作。
Apache Commons wiki有一篇覆盖这个问题的文章 。
Logger序列化
与静态变量相反,实例变量默认是序列化的。对于SLF4J版本1.5.3,
Logger实例变量幸好是初始化的。因此,主机类的初始化不再需要任何特别的指令,即使当loggers被声明为实例变量。在之前的版本里,logger实例需要在主机类里声明为transient类型变量。
总结
综上,声明logger成员为静态变量需要更少的CPU时间,有一个更小的内存开销。另一方面,声明logger成员为实例变量需要更多的CPU时间和更高的内存开销。然而,实例变量使得为每一个应用构建一个不同的logger环境,甚至为共享库里声明的logger成为可能。可能,比之前涉及的因素更重要的是,实例变量是IOC友好的,而静态变量却不是。
查看commons-logging wiki里相关讨论 。
2.类里的声明日志器有推荐的习语吗?
以下是推荐的记录声明成语。原因如上所述,这是留给用户确定记录器是否被声明为静态变量或没有。
1 |
package some. package ;
|
2 |
import org.slf4j.Logger;
|
3 |
import org.slf4j.LoggerFactory;
|
4 |
5 |
public class MyClass {
|
6 |
final ( static ) Logger logger = LoggerFactory.getLogger(MyClass. class );
|
7 |
... etc
|
8 |
9 |
} |
不幸的是,由于主机类的名称是记录器声明的一部分,上面logger声明习语是不能在类之间抵抗剪切和粘贴。