java日志系统经常遇到SLF4j,JCL,logback,log4j2等等。一些人可能要晕了怎么选择,这里简单说下。
发展
这些都要从Java日志框架的元老log4j说起。java1.3之前打日志都是采用System.out.println(), System.err.println()等做法。
此时log4j诞生,定义了Logger、Appender、Level等概念,至今还在使用。可以说是一下子脱离了刀耕火种时代。
毕竟还不是java的标准,因此随着IT发展,开始有多种设计和实现
从上图可以看出,这个过程出了两个统一的接口,并有不同实现
SLF4j和JCL
JCL,Jakarta Commons Logging。
SLF4j和JCL对使用者封装了统一接口,进行解耦。两者定位一样,在不更改代码的基础上,随意切换日志系统。也各自拥有忠实的原生实现logback和log4j2。
SLF4j
SLF4j采用静态绑定机制
1.SLF4j为各个日志输出系统提供了适配库
2.SLF4j会加载org.slf4j.impl.StaticLoggerBinder作为输出日志的实现类(老版本)。新版本通过java SPI实现。自然而然地就能与具体的日志输出实现绑定起来。
JCL
JCL采用动态绑定机制
1.启动时,尝试寻找当前factory中名叫org.apache.commons.logging.Log配置属性的值
2.找不到,则寻找系统中属性中名叫org.apache.commons.logging.Log的值
3.依旧找不到,如果应用程序的classpath中有log4j,则使用相关的包装(wrapper)类(Log4JLogger)
4.还没找到的话,使用相关的包装类(Jdk14Logger)
log4j2与logback
log4j2比较新,比logback晚出来几年。也吸收了logback的有点,也有大量改进。在多线程环境下和使用异步日志时性能比较好
log4j2和logback各有优缺点
桥接与适配
因为理念和实现的差异性,不可避免的存在一些不兼容。另外不同的java库也依赖不同的日志输出服务,也有兼容性问题。因此两个门面在与实现进行组合时就需要一些桥接器和适配器。
完美情况下,应该是
这个是最理想的实现方案,比如:
dependencies {
compile "org.slf4j:slf4j-api:1.7.30"
compile "ch.qos.logback:logback-classic:1.2.3"
compile "ch.qos.logback:logback-core:1.2.3"
}
然而现实是多样的。因为理念、设计思想、实现思路等各不相同。一定会出现兼容性问题。
适配器
系统jar包之间肯定存在统一接口与日志输出系统不兼容的
此时就需要在接口和日志输出系统之间,做一层适配。
示例代码
适配到log4j依赖:
repositories {
mavenCentral()
maven { url "http://maven.aliyun.com/nexus/content/groups/public/" }
}
dependencies {
compile "org.slf4j:slf4j-api:1.7.30"
compile "org.slf4j:slf4j-log4j12:1.7.30" // 自带了 log4j
compile "log4j:log4j:1.2.17"
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyMain {
private static Logger logger = LoggerFactory.getLogger(MyMain.class);
public static void main(String[] args) {
logger.info("ka---------ka");
}
}
log4j.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration PUBLIC "-//log4j/log4j Configuration//EN" "log4j.dtd">
<log4j:configuration>
<!-- 日志输出到控制台 -->
<appender name="console" class="org.apache.log4j.ConsoleAppender">
<!-- 日志输出格式 -->
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="[%p][%d{yyyy-MM-dd HH:mm:ss SSS}][%c]-[%m]%n"/>
</layout>
</appender>
<!--
1. 指定logger的设置,additivity是否遵循缺省的继承机制
2. 当additivity="false"时,root中的配置就失灵了,不遵循缺省的继承机制
3. 代码中使用Logger.getLogger("logTest")获得此输出器,且不会使用根输出器
-->
<logger name="com.hust" additivity="false">
<level value ="INFO"/>
<appender-ref ref="console"/>
</logger>
<!-- 根logger的设置,若代码中未找到指定的logger,则会根据继承机制,使用根logger-->
<root>
<appender-ref ref="console"/>
</root>
</log4j:configuration>
或者适配到log4j2:
dependencies {
compile "org.slf4j:slf4j-api:1.7.30"
compile "org.apache.logging.log4j:log4j-slf4j-impl:2.13.1"// 适配到 log4j
compile "org.apache.logging.log4j:log4j-core:2.13.1"
compile "org.apache.logging.log4j:log4j-api:2.13.1"
}
此时log4j2.xml 配置反而更简单:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="INFO">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
简单来说就是把slf4j收到的日志请求转向到合适的日志系统中,目前主要有
slf4j-jdk14:slf4j适配到jdk-logging
slf4j-log4j12:slf4j适配到 log4j1
log4j-slf4j-impl:slf4j 适配到 log4j2
logback-classic:slf4j 适配到 logback
slf4j-jcl:slf4j 适配到 commons-logging
桥接器
由于历史遗留的jar包、日志输出实现选择等问题,肯定存在java服务直接使用日志输出系统。
比如当前系统要使用slf4j,但是依赖的某个jar使用了log4j。此时slf4j就收集不到这个jar的日志,怎么办?此时就需要桥接,简单说就是把日志重定向输出
示例,注意这里Log是 common-logging里的类
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class MyMain {
// commons.logging 的类
private static Log logger = LogFactory.getLog(MyMain.class);
public static void main(String[] args) {
logger.info("ka---------ka");
}
}
改造:
dependencies {
compile "commons-logging:commons-logging:1.2"
compile "org.slf4j:jcl-over-slf4j:1.7.30"// commons-logging到slf4j的桥梁
compile "org.slf4j:slf4j-api:1.7.30" // slf4j 可以直接输出到logback
compile "ch.qos.logback:logback-classic:1.2.3"
compile "ch.qos.logback:logback-core:1.2.3"
}
桥接器自动把已经存在的日志输出系统实现类似屏蔽,对此java服务是无感知的.
同样的现有log4j的也可以无感知的桥接到slf4j,然后转到日志输出系统
import org.apache.log4j.Logger;
public class MyMain {
// apache 的 log4j 包的类
private static Logger logger = Logger.getLogger(MyMain.class);
public static void main(String[] args) {
logger.info("ka---------ka");
}
}
对应依赖
dependencies {
compile "log4j:log4j:1.2.17"
compile "org.slf4j:log4j-over-slf4j:1.7.30" // log4j1到slf4j的桥梁
compile "org.slf4j:slf4j-api:1.7.30"
compile "ch.qos.logback:logback-classic:1.2.3"
compile "ch.qos.logback:logback-core:1.2.3"
}
对于jul来说也是一样,稍微不同的是要提前注册一个类。另外日志 两边都输出
import org.slf4j.bridge.SLF4JBridgeHandler;
import java.util.logging.Logger;
public class MyMain {
static {
// 在使用之前,必须提前注册这个处理器,
SLF4JBridgeHandler.install();
// 该类集成了jdk-logging里面的java.util.logging.Handler,这个是jdk-logging处理日志中的一个处理器
}
// jdk 自带的 Logger 类
private static Logger logger = Logger.getLogger(MyMain.class.getName());
public static void main(String[] args) {
logger.info("ka---------ka");
}
}
依赖如下:
dependencies {
compile "org.slf4j:jul-to-slf4j:1.7.30" // jdk-logging到slf4j的桥梁
compile "org.slf4j:slf4j-api:1.7.30"
compile "ch.qos.logback:logback-classic:1.2.3"
compile "ch.qos.logback:logback-core:1.2.3"
}