Java日志实现知多少?
1.Java日志历史
Java 拥有功能和性能都非常强大的日志库,但不幸的是,这样的日志库有不止一个——相信每个Java程序员都曾经迷失在JUL(Java Util Log), JCL(Commons Logging), Log4j, SLF4J, Logback,Log4j2 等等的迷宫中。让我们回顾下讲讲这段“腥风血雨”的历史。
-
Java Util Log
来自官方JDK,带着标准和权威的光环,小名:JUL。从JDK1.4 才开始加入(2002年),当时各种第三方日志组件及其盛行,且JUL性能和使用的确又没有其他组件方便。虽然JDK1.5对其进行了改进,但还是不影响很多项目不选用该组件的命运。
-
Log4j 1.x
在JUL推出的前一年,Gülcü 发布了Log4j 1.x,虽然进不了JDK,但是Log4j 1.x进入了Apache 基金会顶级项目。Log4j 在设计上非常优秀,对后续的 Java Log 框架有长久而深远的影响,也产生了Log4c, Log4s, Log4perl 等到其他语言的移植。但是,后浪推前浪,前浪的性能终究还是不断被后期的日志框架赶超,比如后期的Logback
和Log4j2
。 -
JCL
那么问题来了,JDK官方自带JUL,第三方有Log4J,不同项目或者开源Jar,采用了不同的日志实现库,那么是不是意味着要整合使用,得写多个配置文件呢。正式这个问题,带来了JCL的出现。JCL,大名:Commons Logging,同样也是Apache下的项目,但是JCL 是一个Log Facade(门面Api),只提供 Log API,不提供实现。在程序中日志创建和记录都是用JCL中的接口,在真正运行时,然后有适配器Adapter 来使用 Log4j 或者 JUL 作为Log 实现。(当前ClassPath中有什么实现,如果有Log4j 就是用 Log4j, 如果啥都没有就是用 JDK 的 JUL)。是不是感觉从面向对象的高度,JCL有种很先进的感觉。这就是面向接口编程的体现。这样,在你的项目中,如果用Log4j, 就添加 Log4j 的jar包进去,然后写一个 Log4j 的配置文件;如果喜欢用JUL,就只需要写个 JUL 的配置文件。如果有其他的新的日志库出现,也只需要它提供一个Adapter,运行的时候把这个日志库的 jar 包加进去。
JCL
合久必分,分久必合。历史就是这样,日志组件在接下去的历史演进中,又出现了跌宕起伏。一种平衡替换另一种平衡。
-
SLF4J/Logback
Gülcü (对头,又是ta)认为 JCL 的 API 设计得不好,容易让使用者写出性能有问题的代码,Gülcü ,不安于现状,不基于JCL添加实现类,而是创立了SLF4J 和 Logback项目,目的就是为了提高日志组件的性能。SLF4J的全称:Simple Logging Facade for Java,看其意思就是门面API,而Logback作为其实现类。当然 Logback 则是作为 Log4j 的继承者来开发的,提供了性能更好的实现,异步 logger,Filter等更多的特性。现在事情变复杂了。我们有了两个流行的 Log Facade,以及三个流行的 Log Implementation。
jcl+slf4j
当你感觉现在差不多了吧的时候,三国时期的故事,其实又开始上演了。
-
Log4j2
维护 Log4j 的人似乎坐立不安,他们不想坐视用户一点点被 SLF4J /Logback 蚕食,继而搞出了 Log4j2。
Log4j2 和 Log4j1.x 并不兼容,设计上很大程度上模仿了 SLF4J/Logback,性能上也获得了很大的提升。
Log4j2 也做了 Facade/Implementation 分离的设计,分成了 log4j-api 和 log4j-core。
jcl+slf4j+log4f2
=========================分割线========================
Gülcü 是个追求完美的人,各种纷纷扰扰的历史看在了ta眼里,他决定让这些Log之间都能够方便的互相替换,所以做了各种 Adapter 和 Bridge 来连接:
adpater adpater-jar
到这里,日志演进总算有所停歇。
2.Spring Boot 日志使用
2.1. 依赖分析
历史回顾不是我们的目的,结合现在流行的开源框架Spring Boot,我们再来谈谈具体项目该如何结合实际,使用日志。
构建Spring Boot Web项目,版本:2.1.4。分析下Pom.xml的依赖:
logback maven
可以发现,Spring Boot采用了SLF4J+Logback的组合来完成日志的记录。并且作者把Log4j和JUL的日志组件适配到了slf4j。的确,Spring Boot为Java coder做了太多的工作。
2.2. 日志初始化过程
以上面构建的Spring Boot项目为例,添加简单日志记录代码:
@SpringBootApplication
public class SpringBootLoggerDemoApplication {
private static Logger logger = LoggerFactory.getLogger(SpringBootLoggerDemoApplication.class);
public static void main(String[] args) {
SpringApplication.run(SpringBootLoggerDemoApplication.class, args);
logger.debug("hello logger");
}
}
LoggerFactory.java
/**
* Return a logger named according to the name parameter using the
* statically bound {@link ILoggerFactory} instance.
*
* @param name
* The name of the logger.
* @return logger
*/
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
通过getILoggerFactory获取日志工厂:
/**
* Return the {@link ILoggerFactory} instance in use.
* <p/>
* <p/>
* ILoggerFactory instance is bound with this class at compile time.
*
* @return the ILoggerFactory instance in use
*/
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
performInitialization();
}
}
}
...省略...
}
这里代码定位到performInitialization:
private final static void performInitialization() {
bind();
...省略..
}
bind()具体日志实现:
private final static void bind() {
try {
Set<URL> staticLoggerBinderPathSet = null;
if (!isAndroid()) {
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
...省略...
}
}
关键代码就在findPossibleStaticLoggerBinderPathSet:
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
static Set<URL> findPossibleStaticLoggerBinderPathSet() {
// use Set instead of list in order to deal with bug #138
// LinkedHashSet appropriate here because it preserves insertion order
// during iteration
Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
try {
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
while (paths.hasMoreElements()) {
URL path = paths.nextElement();
staticLoggerBinderPathSet.add(path);
}
} catch (IOException ioe) {
Util.report("Error getting resources from path", ioe);
}
return staticLoggerBinderPathSet;
}
代码最终到最后,其实是通过ClassLoader在ClassPath里面加载指定实现类org/slf4j/impl/StaticLoggerBinder.class
来实现日志组件的加载,核心就在:
//org/slf4j/impl/StaticLoggerBinder.class
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
类路径下看看backLog代码:
lackLog code这里有个题外话,之前看过很多介绍Java SPI文章,老是把日志的加载机制归类为SPI,其实通过上面的介绍,现在可以结论,其实不是。
https://www.jianshu.com/p/46b42f7f593c
3.日志切换方法及原理
行文到此,主题介绍似乎差不多了,但是好像还有个问题,要是我要在Spring Boot换其他日志组件怎么办呐。其实Spring Boot已经为我们考虑过这个问题了,为我们提供了一个自动配置的starter:spring-boot-starter-log4j2
。
Starter for using Log4j2 for logging. An alternative to
spring-boot-starter-logging
修改方式也简单:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
log4j2 maven
依赖切换成了最后的log4j-api+log4j-core。
log4j参考: