这是我参与更文挑战的第2天,活动详情查看: 更文挑战
Java日志体系
日志发展史
由于Java 中日志框架不统一,各种各样,主要以日志门面和日志实现展开,日志门面相当于定义了一套标准的API规范,类似接口。日志实现就是对接口规范的实现。
日志门面 | 日志实现 |
---|---|
Slf4j | Log4j Logback Log4j2 JUL(Java Util logging) |
JCL(Jakarta Commons Logging) | |
从网上百度了下Java的日志历史
-
1996年早期,欧洲安全电子市场项目组决定编写它自己的程序跟踪API(Tracing API)。经过不断的完善,这个API终于成为一个十分受欢迎的Java日志软件包,即Log4j(由Ceki创建)。后来Log4j成为Apache基金会项目中的一员,Ceki也加入Apache组织。后来Log4j近乎成了Java社区的日志标准。据说Apache基金会还曾经建议Sun引入Log4j到Java的标准库中,但Sun拒绝了。
-
2002年Java1.4发布,Sun推出了自己的日志库JUL(Java Util Logging),其实现基本模仿了Log4j的实现。在JUL出来以前,Log4j就已经成为一项成熟的技术,使得Log4j在选择上占据了一定的优势。接着,Apache推出了Jakarta Commons Logging,JCL只是定义了一套日志接口(其内部也提一个SimpleLog 的简单实现),支持运行时动态加载日志组件的实现,也就是说,在你应用代码里,只需调用Commons Logging 的接口,底层实现可以是Log4j,也可以是Java Util Logging。
- 后来(2006年),Ceki不适应Apache的工作方式,离开了Apache。然后先后创建了Slf4j(日志门面接口,类似于Commons Logging)和Logback(Slf4j的实现)两个项目,并回瑞典创建了QOS公司,QOS官网上是这样描述Logback的:The Generic,Reliable Fast&Flexible Logging Framework(一个通用,可靠,快速且灵活的日志框架)。
其中JCL日志门面由于API设计以及性能问题已经退出了历史舞台,留下了Slfj4一家独大。虽然Slfj4定义了日志门面,但除了Logbak是Slfj4作者自己实现的,完全采用了Slfj4的API外,其他的日志实现,都没有采用Slfj4的API接口进行实现,规范你可以定,尊不遵守规范就看我自己了。由此有两个大问题摆在了Slf4j面前:
- 新建项目,如何在使用Slf4j日志门面下,自由切换不同搞得日志实现?
- 以前老项目用的Log4j,现在又有需求了,需要将日志统一改为Slf4j规范,和logback实现怎么做?
日志绑定
SPI方式解决日志绑定问题
先看第一个问题,这不就是接口有多实现如何加载多实现的问题么,简单称这个问题为日志绑定,如果看了前一章的《Dubbo公共契约SPI》很容易可以想到使用SPI的方式进行加载,并且还仅仅只用加载一个即可,不需要多个。但由于有些日志实现不愿意遵守Slf4j的日志门面,所以就只能自己去额外写一个第三方包来使用Slf4j绑定日志实现,下面进行详细设计。
如上图汇总,蓝色表示项目依赖的jar包,红色表示项目源代码中使用Slf4j的日志门面接口进行开发。如果想使用log4j作为日志实现,那么可以通过maven引入log4j的实现jar(最右侧的jar),但由于log4j中没有使用Slf4j日志门面,所以需要再额外引入一个jar包(中间的Slf4j-log4j-impl),里面的内容就是使用log4j实现Slf4j日志门面。但是如何让Slf4j加载这个实现呢?答案就是SPI。
在我们开发中,通常会使用Slf4j的API接口,**Logger log = LogFactory.getLogger(XXXX.class);**在Slf4j-log4j-impl中可以实现log4j的 LogFactory,然后在Slf4j中通过SPI机制加载这个实现,最后别忘了在Slf4j-log4j-impl中定义SPI的接口配置文件。由此就可以实现日志绑定,动态的切换日志实现而不需要修改源代码。
Slf4j解决日志绑定问题
那么Slf4j的作者会使用这种方式么?当然不是拉。
新建一个maven项目,引入slf4j-api包
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
复制代码
从入口方法org.slf4j.LoggerFactory#getLogger(java.lang.Class<?>)进入,会找到以下加载LogFactory的方法:
没错,你真的没看错,在这个jar中,这个地方会因为找不到类而票红,因为确实也灭有这个类。而Slf4j的作者也是没有使用SPI方式加载实现,而是通过创建一个同包名,同类名,同方法名的StaticLoggerBinder实现的日志绑定。这个原理当同时引入Slf4j以及Slf4j-log4j-impl时,App类加载器会从Slf4j-log4j-impl中加载StaticLoggerBinder类,从而加载log4j的实现。
引入日志绑定实现包
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.30</version>
</dependency>
复制代码
可以看到有一个同包名类名的实现,并且日志实现使用了Log4j。
日志桥接
有一个老项目,使用的log4j日志实现,没有引入任何日志门面,某天公司基础平台组基于性能以及日志集成问题,要求所有业务方统一使用Slf4j日志门面以及logback日志实现,该怎么办呢?
最直观的一个想法就是,去掉logback依赖,引入Slf4j和logback,并且将所有使用到log4j的地方全都换成Slf4j,想法木问题,但是工作量是相当的大。那么有木有别的方案,可以不用一个一个更改呢?
有,创建一个第三方库,将所有用到log4j代码的地方,在第三方库中创建一个同包名,通类名,同方法名的类。但是,方法的实现使用Slf4j完成。然后,将log4j的依赖移除,引入这个第三方库。而Slf4j的作业也是使用这个方法做到日志桥接。
什么是日志适配
现在有个用户注册的项目引入一个第三方依赖,比如dubbo,假设dubbo中使用logback打印日志,而项目里面使用的log4j做日志实现,就会出现日志混乱的情况,两者可以共存,但需要同时维护两份日志配置,以及使用不同的日志API进行日志打印,非常不便于维护。所以通常第三方包在编写的时候,都会将第三方库中的日志实现保持和使用者项目中的日志实现一致。
Dubbo如何做日志适配
假设你是Dubbo开发这,你会如何解决日志适配的问题呢?比如使用者项目使用了logback作为日志实现,如何保证Dubbo中的日志实现也是使用的logback呢?
方法就是,第三方库自己封装一个日志打印的接口,然后用目前所有流行的日志进行实现,然后在获取实现时,把每一种日志实现都加在一遍,如果抛出ClassNotFoundException的异常就表明使用者用的也不是这种日志实现,直接跳过,当某个实现可以加载,就表明用户也是用的这种日志实现,从而做到日志适配。最后第三方库在进行所有日志实现必然要引入日志实现的依赖,需要将这种依赖的scope设置为provider(表明只在Dubbo源码中编译集成,不进行传播,即使用这者引入Dubbo时不需要引入日志实现的依赖)。
可以看到pom中使用的provider级别的scope。
在dubbo源码的org.apache.dubbo.common.logger.LoggerFactory类中也可以找到加载日志实现。
static {
// 可以通过properties参数指定日志实现
String logger = System.getProperty("dubbo.application.logger", "");
switch (logger) {
case "slf4j":
setLoggerAdapter(new Slf4jLoggerAdapter());
break;
case "jcl":
setLoggerAdapter(new JclLoggerAdapter());
break;
case "log4j":
setLoggerAdapter(new Log4jLoggerAdapter());
break;
case "jdk":
setLoggerAdapter(new JdkLoggerAdapter());
break;
case "log4j2":
setLoggerAdapter(new Log4j2LoggerAdapter());
break;
default:
// 未指定时候将所有实现加载一遍
List<Class<? extends LoggerAdapter>> candidates = Arrays.asList(
Log4jLoggerAdapter.class,
Slf4jLoggerAdapter.class,
Log4j2LoggerAdapter.class,
JclLoggerAdapter.class,
JdkLoggerAdapter.class
);
for (Class<? extends LoggerAdapter> clazz : candidates) {
try {
// 尝试使用某个日志实现,如果跑出ClassNotFound异常,则继续寻找
setLoggerAdapter(clazz.newInstance());
break;
} catch (Throwable ignored) {
}
}
}
}
复制代码
总结
Java的日志比较混乱,之前用过python,日志都是用的python官方提供的日志,就没有这么多的问题。基本所有第三方库中都会做日志适配,并且实现的方法套路基本都是一幕一幕一点没变。