作为一名开发人员,相信大家对日志工具不会陌生 , Java 也 拥有功能和性能都非常强大的日志库; 不过这么多日志工具&第三方的包,怎样保证每个 组件里都能使用约定好的日志工具?
本文将和大家介绍一下 Java 主流的日志工具,以及相对应的使用场景。
基本介绍
在java的世界里有许多实现日志功能的工具,最早得到广泛使用的是 log4j,现在比较流行的是slf4j+logback。作为开发人员,我们有时候需要封装一些组件(二方包)提供给其他人员使用,但是那么多的日志工具,根本没法保证每个组件里都能使用约定好的日志工具,况且还有很多第三方的包,鬼知道他会用什么日志工具。假如一个应用程序用到了两个组件,恰好两个组件使用不同的日志工具,那么应用程序就会有两份日志输出了,蛋疼吧。。
下面简单介绍下常见的日志工具:
JUL 全称 java.util.logging.Logger,JDK 自带的日志系统,从 JDK1.4 就有了。因为 log4j 的存在,这个 logger 一直沉默着,其实在一些测试性的代码中,jdk自带的 logger 比 log4j 更方便。JUL是自带具体实现的,与 log4j、logback 等类似,而不是像 JCL、slf4j 那样的日志接口封装。
import java.util.logging.Level; import java.util.logging.Logger; private static final Logger LOGGER = Logger.getLogger(MyClass.class.getName());
相同名字的Logger对象全局只有一个;
一般使用圆点分隔的层次命名空间来命名 Logger ; Logger 名称可以是任意的字 符串,但是它们一般应该基于被记录组件的包名或类名,如 java.net 或 javax.swing ;
配置文件默认使用 jre/lib/logging.properties ,日志级别默认为 INFO ;
可以通过系统属性 java.util.logging.config.file 指定路径覆盖系统默认文件;
日志级别由高到低 依次为: SEVERE (严重)、 WARNING (警告)、 INFO (信息)、 CONFIG (配置)、 FINE (详细)、 FINER (较详细)、 FINEST (非常详细)。 另外还有两个全局开关: OFF 「关闭日志记录」和 ALL 「启用所有消息日志记录」。
《 logging.properties 》文件中,默认日志级别可以通过 .level= ALL 来控制,也可以基于层次命名空间来控制,按照 Logger 名字进行前缀匹配,匹配度最高的优先采用; 日志级别只认大写;
JUL 通过 handler 来完成实际的日志输出,可以通过配置文件指定一个或者多个 hanlder ,多个 handler 之间使用逗号分隔; handler 上也有一个日志级别,作为该 handler 可以接收的日志最低级别,低于该级别的日志,将不进行实际的输出; handler 上可以绑定日志格式化器,比如 java.util.logging.ConsoleHandler 就是使 用的 String.form at 来支持的;
配置文件示例:
handlers= java.util.logging.ConsoleHandler .level= ALL com.suian.logger.jul.xxx.level = CONFIG com.suian.logger.jul.xxx.demo2.level = FINE com.suian.logger.jul.xxx.demo3.level = FINER java.util.logging.ConsoleHandler.level = ALL java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter java.util.logging.SimpleFormatter.format=%1$tF %1$tT [%4$s] %3$s - %5$s %n
之前叫Jakarta Commons Logging,简称JCL,是Apache提供的一个通用日志API,可以让应用程序不再依赖于具体的日志实现工具。
commons-logging包中对其它一些日志工具,包括Log4J、Avalon LogKit、JUL等,进行了简单的包装,可以让应用程序在运行时,直接将JCL API打点的日志适配到对应的日志实现工具中。
common-logging通过动态查找的机制,在程序运行时自动找出真正使用的日志库。这一点与slf4j不同,slf4j是在编译时静态绑定真正的Log实现库。
commons-logging包里的包装类和简单实现列举如下:
org.apache.commons.logging.impl.Jdk14Logger,适配JDK1.4里的JUL;
org.apache.commons.logging.impl.Log4JLogger,适配Log4J;
org.apache.commons.logging.impl.LogKitLogger,适配avalon-Logkit;
org.apache.commons.logging.impl.SimpleLog,common-logging自带日志实现类,它实现了Log接口,把日志消息都输出到系统错误流System.err中;
org.apache.commons.logging.impl.NoOpLog,common-logging自带日志实现类,它实现了Log接口,其输出日志的方法中不进行任何操作;
如果只引入Apache Commons Logging,也没有通过配置文件《commons-logging.properties》进行适配器绑定,也没有通过系统属性或者SPI重新定义LogFactory实现,默认使用的就是jdk自带的java.util.logging.Logger来进行日志输出。
JCL使用配置文件commons-logging.properties,可以在该文件中指定具体使用哪个日志工具。不配置的话,默认会使用JUL来输出日志。配置示例:
Log4j2体系结构:
使用场景
最简单的场景,正式系统一般不会这么用,自己写点小demo、测试用例啥的是可以这么用。不要任何第三方依赖,jdk原生支持。
需要引入commons-logging包,示例如下:
<dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>
需要引入commons-logging包和log4j包,示例如下:
<dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
该模式下可以使用的打点api:
org.apache.commons.logging.Log,commons-logging里的api;
org.apache.log4j.Logger,log4j里的api;
无论使用哪种api打点,最终日志都会通过log4j进行实际的日志记录。推荐用commons-logging里的api,如果直接用log4j里的api,就跟单用log4j没区别,就没有再引入commons-logging包的必要了。
既然最终是通过log4j实现日志记录,那么日志输出的level、target等也就是通过log4j的配置文件进行控制了。下面是一个log4j配置文件《log4j.properties》的简单示例:
#log4j.rootLogger = error,console log4j.logger.com.suian.logtest = trace,console #输出源console输出到控制台 log4j.appender.console = org.apache.log4j.ConsoleAppender log4j.appender.console.Target = System.out log4j.appender.console.layout = org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss,SSS} [%t] %-5p %c - [log4j]%m%n
<span>org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger</span>
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>${logback.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency>
Log4j2感觉就是SLF4J+Logback。log4j-api等价于SLF4J,log4j-core等价于Logback。
需要引入第三方依赖:
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.6.2</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.6.2</version> </dependency>
理论上各种日志输出方式是可以共存的,比如log4j和log4j2以及logback等,但是麻烦的是我们得维护多个配置文件,必须充分了解每个组件使用的是那种日志组件,然后进行对应的配置文件配置。
如何解决呢?每一个想做通用日志解决方案的,都对兼容性问题进行了特殊处理。目前只有slf4j和log4j2提供了这样的整合机制,其他的基本都很弱。
代码中可能使用的日志打点Api列举:
java.util.logging.Logger,jdk自带的;
org.apache.commons.logging.Log,commons-logging包里的api;
org.apache.log4j.Logger,log4j包里的api;
org.apache.logging.log4j.Logger,log4j2提供的api,在log4j-api包里;
org.slf4j.Logger,slf4j提供的api,在slf4j-api包里;
上述打点方式,在一个应用中是有可能共存的,即使自己写的代码可以确保都使用同一类api,但是引入的第三方依赖里就可能各式各样了。该怎么处理呢?
前面已经提过了,现在能够对各类冲突支持比较到位的就是slf4j和log4j2,他们都提供了很多的绑定器和桥接器。
所谓的绑定器,也可以称之为适配器或者包装类,就是将特定api打点的日志绑定到具体日志实现组件来输出。比如JCL可以绑定到log4j输出,也可以绑定到JUL输出;再比如slf4j,可以通过logback输出,也可以绑定到log4j、log4j2、JUL等;
所谓的桥接器就是一个假的日志实现工具,比如当你把 jcl-over-slf4j.jar 放到 CLASS_PATH 时,即使某个组件原本是通过 JCL 输出日志的,现在却会被 jcl-over-slf4j “骗到”SLF4J 里,然后 SLF4J 又会根据绑定器把日志交给具体的日志实现工具。
将JUL日志整合到slf4j统一输出,需要引入slf4j提供的依赖包:
<dependency> <groupId>org.slf4j</groupId> <artifactId>jul-to-slf4j</artifactId> <version>1.7.22</version> </dependency>
建立jdk14-logger的配置文件《logger.properties》,加入handler配置以及日志级别配置;
<span>handlers= org.slf4j.bridge.SLF4JBridgeHandler</span>
<span>.level= ALL</span>
在启动程序或容器的时候加入JVM参数配置-Djava.util.logging.config.file = /path/logger.properties;当然也可以使用编程方式进行处理,可以在main方法或者扩展容器的listener来作为系统初始化完成;此种方式有些场景下不如配置JVM参数来的彻底,比如想代理tomcat的系统输出日志,编程方式就搞不定了。
<dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.22</version> </dependency>
将log4j日志整合到slf4j统一输出,需要引入slf4j提供的依赖包:
<dependency> <groupId>org.slf4j</groupId> <artifactId>log4j-over-slf4j</artifactId> <version>1.7.22</version> </dependency>
将log4j2日志整合到slf4j统一输出,slf4j没有提供桥接包,但是log4j2提供了,原理是一样的,首先引入log4j2的桥接包:
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-to-slf4j</artifactId> <version>2.6.2</version> </dependency>
log4j2提供的依赖包有org.apache.logging.log4j:log4j-api和org.apache.logging.log4j:log4j-core,其作用看包名就清楚了。log4j-core是log4j-api的标准实现,同样log4j-to-slf4j也是log4j-api的一个实现。
log4j-to-slf4j用于将log4j2输出的日志桥接到slf4j进行实际的输出,作用上来讲,log4j-core和log4j-to-slf4j是不能共存的,因为会存在两个log4j2的实现。
经测试,就测试结果分析,共存也是木有问题的,何解?log4j2加载provider的时候采用了优先级策略,即使找到多个也能决策出一个可用的provider来。在所有提供log4j2实现的依赖包中,都有一个META-INF/log4j-provider.properties配置文件,里面的FactoryPriority属性就是用来配置provider优先级的,幸运的是log4j-to-slf4j(15)的优先级是高于log4j-core(10)的,因此测试结果符合预期,log4j2的日志桥接到了slf4j中进行输出。
同样,为确保系统的确定性,不会因为log4j2的provider决策策略变更导致问题,建议还是要在classpath里排掉log4j-core,log4j2也是推荐这么做的。
log4j2整合日志输出
将JUL日志整合到log4j2统一输出,需要引入log4j2提供的依赖包:
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-jul</artifactId> <version>2.6.2</version> </dependency>
log4j2整合JUL日志的方式与slf4j不同,slf4j只是定义了一个handler,仍旧依赖JUL的配置文件;log4j2则直接继承重写了java.util.logging.LogManager。
使用时,只需要通过系统属性java.util.logging.manager绑定重写后的LogManager(org.apache.logging.log4j.jul.LogManager)即可,感觉比slf4j的方式要简单不少。
将JCL日志整合到log4j2统一输出,需要引入log4j2提供的依赖包:
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-jcl</artifactId> <version>2.6.2</version> </dependency>
基于log4j-jcl包整合JCL比较简单,只要把log4j-jcl包扔到classpath就可以了。看起来slf4j的整合方式优雅多了,底层原理是这样的:JCL的LogFactory在初始化的时候,查找LogFactory的具体实现,是分了几种场景的,简单描述如下:
首先根据系统属性 org.apache.commons.logging.LogFactory 查找 LogFactory 实现类;
如果找不到,则以 SPI 方式查找实现类, META-INF/services/org.apache.commons.logging.LogFactory ; log4j-jcl 就是以这种方式支撑的;此种方式必须确保整个应用中,包括应用依赖的第三方 jar 包中, org.apache.commons.logging.LogFactory 文件只有一个,如果存在多个的话,哪个先被加载则以哪个为准。万一存在冲突的话,排查起来也挺麻烦的。
还找不到,则读取《 commons-logging.properties 》配置文件,使用 org.apache.commons.logging.LogFactory 属性指定的 LogFactory 实现类;
最后再找不到,就使用默认的实现 org.apache.commons.logging.impl.LogFactoryImpl 。
将log4j 1.x日志整合到log4j2统一输出,需要引入log4j2提供的依赖包:
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-1.2-api</artifactId> <version>2.6.2</version> </dependency>
log4j2里整合log4j 1.x日志,也是通过覆写log4j 1.x api的方式来实现的,跟slf4j的实现原理一致。因此也就存在类库冲突的问题,使用log4j-1.2-api的话,必须把classpath下所有log4j 1.x的包清理掉。
将slf4j日志整合到log4j2统一输出,需要引入log4j2提供的依赖包:
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.6.2</version> </dependency>
log4j-slf4j-impl基于log4j2实现了slf4j的接口,其就是slf4j-api和log4j2-core之间的一个桥梁。这里有个问题需要注意下,务必确保classpath下log4j-slf4j-impl和log4j-to-slf4j不要共存,否则会导致事件无止尽地在SLF4J和Log4j2之间路由。
日志打点API绑定实现
slf4j-api和log4j-api都是接口,不提供具体实现,理论上基于这两种api输出的日志可以绑定到很多的日志实现上。slf4j和log4j2也确实提供了很多的绑定器。简单列举几种可能的绑定链:
slf4j → logback
slf4j → slf4j-log4j12 → log4j
slf4j → log4j-slf4j-impl → log4j2
slf4j → slf4j-jdk14 → jul
slf4j → slf4j-jcl → jcl
jcl → jul
jcl → log4j
log4j2-api → log4j2-cor
log4j2-api → log4j-to-slf4j → slf4j
来个环图:
手淘行业与智能运营团队
在阿里,如果不经历电商,那么你可能就失去了一半的工作乐趣; 做电商,如果不搞商业智能,那么你可能就失去了链接人、货、场、商,给几亿用户创造更美好生活的机会! 但是现在,一个充满乐趣和机会的岗位就摆在你的面前 —— 它就是,行业与智能运营团队,电商中最智能的技术团队! 大家走过路过,不要错过! 我们要从上到下打造一支幸福感极强的团队 —— 如果你在追求幸福感,找我们,没毛病! 未来已来,淘系技术部行业与智能运营团队,这支即将成为阿里最具幸福感的技术团队,期待具有好奇心和思考力的你的加入!
面向社会+校园招聘,base杭州阿里巴巴西溪园区!
投喂简历给我们:
mingkai.wmk@alibaba-inc.com✿ 拓展阅读
作者| 王明凯(苍唐)
编辑| 橙子君
出品| 阿里巴巴新零售淘系技术