Java Log简介

Java Log 简介

[TOC]

初衷及目的

初衷

为什么项目里会引入这么多关于log的jar包?
都是干嘛吃的?
日志圈真!!!

1
2
3
4
5
6
7
8
9
10
commons-logging-1.2.jar
log4j-1.2.17.jar
log4j-1.2-api-2.3.jar
log4j-api-2.1.jar
log4j-core-2.1.jar
log4j-over-slf4j-1.7.7.jar
log4j-slf4j-impl-2.1.jar
jcl-over-slf4j-1.7.7.jar
jul-to-slf4j-1.7.7.jar
slf4j-api-1.7.7.jar

目的

  1. 介绍java日志体系的脉络和关系
  2. 重点介绍Log4j2的配置
  3. Log4j2的原理浅析

Java日志体系分为日志门面和日志实现。下面将分别介绍它们。

日志门面

  • 代理系统,自身不实现具体的日志打印逻辑。接受日志请求,将真正的打印逻辑交给具体的日志实现系统。
  • 可插拔,业务可以通过配置或者加入一些jar包的方式切换到其他日志实现框架,而不需要修改代码逻辑,解耦

目前主要有两套通用日志门面:common-logging、slf4j。

###1. common-logging:Apache Commons Logging
使用方式:

1
Log log = LogFactory.getLog(xx.class);

特点:

  1. 简单、非常轻的一种桥接(动态查找)方式。
  2. 限制:不支持OSGI,模块化应用
  3. 功能单一:只能支持到具体日志系统间的桥接,而不能支持从具体日志系统到其自身的桥接。

###2. SLF4J:Simple Logging Facade for JAVA
使用方式:

1
Logger logger = LoggerFactory.getLogger(xx.class);

特点:

  1. 功能强大,不仅支持到具体日志实现之间的桥接,同时也支持从具体日志系统到自身的桥接
  2. 支持OSGI模块化应用
  3. 通过静态绑定的桥接方式

日志实现

###1. Jdk-logging
JDK自带的一种自定义的、可扩展的日志框架(java.util.logging)。但是其API并不完善,不是很很友好,而且对于日志的级别分类也不是很清晰
JDK Logging深入分析

###2. Log4j1
Log4j也是Apache的开源项目,最早被广泛应用的日志解决方案。

  1. 日志输出目的地可控;
  2. 输出格式可控;
  3. 通过日志级别,更细致控制日志的生成过程
  4. 通过配置文件来灵活地进行配置日志输出,而不需要修改程序代码。

1.x系列在15年8月被Apache宣布停止维护(Apache Blog

###3. LogBack
也是出自Log4j的创始人,最初的意图是用来替代Log4j。在日志性能、功能、可用性上有了很大程度的提升: Reasons to prefer logback over log4j

Logback当前分为三个模块:logback-core, logback-classic and logback-access;
logback-core模块是其他两个模块的基石;
logback-classic是log4j的改良版,别外,其也实现了SLF4J API,使其可以便捷地更换其他日志实现系统
logback-access与Servlet容器集成,提供通过Http来访问日志的功能

###4. Log4j2
Log4j2也是Apache的开源项目,它对Log4j1做了巨大的提升。并且也提供了很多Logback的改进,并且改进了Logback框架存在的一些问题。

  1. 性能提升:包含下一代基于the LMAX Disruptor library的异步日志框架。多线程场景下,吞吐量是 Log4j1和Logback的18倍,响应时间也比它们小很多。性能报告:http://logging.apache.org/log4j/2.x/performance.html
  2. 插件式结构:可以根据需要扩展Log4j2. 可以实现Appender、Logger、Filter
  3. 动态加载配置:可以动态地加载修改的配置,并且在重配置时,不会丢失日志时间
  4. 支持多种API,Log4j2自己实现了Log4j1、Common-logging、Slf4j的桥接(log4j-1.2-api、log4j-jcl、log4j-slf4j-impl)

桥接

日志门面通过什么方法与具体的日志实现进行绑定。

1.Common-logging 桥接

其通过动态查找的机制,在程序运行时自动找出真正使用的日志库。也是比较粗略的一种方式
其运行原理:

1
Log log = LogFactory.getLog(xx.class);

其首先获取一个日志对象的工厂,然后通过工厂来生成对象

1
return getFactory().getInstance(clazz);

getFactory()方法中展示了日志工厂的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//1.全局系统变量中寻找 org.apache.commons.logging.LogFactory
String factoryClass = getSystemProperty(FACTORY_PROPERTY, null);

//2. 找 META-INF/services/org.apache.commons.logging.LogFactory 文件中定义的LogFactory名称,
// log4j2的桥接就提供了这样的文件
final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);
if( is != null ) {
BufferedReader rd;
try {
rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
} catch (java.io.UnsupportedEncodingException e) {
rd = new BufferedReader(new InputStreamReader(is));
}
String factoryClassName;
try {
factoryClassName = rd.readLine();
} finally {
rd.close();
}
factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader);
}
}

// 3.找用户配置的属性文件中key为org.apache.commons.logging.LogFactory的配置值作为日志工厂
String factoryClass = props.getProperty(FACTORY_PROPERTY);
if (factoryClass != null) {
factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
}

// 4. 默认的日志工厂 org.apache.commons.logging.impl.LogFactoryImpl
factory = newFactory(FACTORY_DEFAULT, thisClassLoader, contextClassLoader);

获取工厂对象后,就由工厂对象跟据相应的配置来生成日志对象。以默认的工厂类LogFactoryImpl为例,我们来分析生成日志对象的过程:
其核心代码在discoverLogImplementation(String logCategory)方法中:

1
2
3
4
5
6
7
//1.首先 判断用户是否自定义了日志实现类 org.apache.commons.logging.Log、org.apache.commons.logging.log 为key的用户配置文件项或系统属性
String specifiedLogClassName = findUserSpecifiedLogClassName();
//2. 如果没有,就按以下顺序去尝试创建日志对象,创建成功后立即返回相应日志对象
"org.apache.commons.logging.impl.Log4JLogger",
"org.apache.commons.logging.impl.Jdk14Logger",
"org.apache.commons.logging.impl.Jdk13LumberjackLogger",
"org.apache.commons.logging.impl.SimpleLog"

总结:

  1. 首先寻找日志工厂类
    通过检查系统属性、执行路径文件和用户配置文件,看是否定义了相应的日志工程类。若有则加载此类,并通过其来生成日志对象;否则选择默认的日志工厂:org.apache.commons.logging.impl.LogFactoryImpl
  2. 默认日志工厂获取日志适配器的过程:首先去寻找用户是否自己配置了日志适配器。若有则加载此类并初始化返回;若没有,则按一个固定的顺序来尝试来实例化日志适配器,若实例化成功则返回。

2. SLF4J 桥接

下图展示的是SLF4J和其他实现日志框架的桥接示意图:
可以看到通过jar包和其他日志实现框架起桥梁作用。这些起桥接作用的jar包是怎么起作用的呢?
set up-w520

是因为StaticLoggerBinder的作用!
从SLF4J的入口来寻找答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static Logger getLogger(String name) {
//获取日志工厂
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
public static ILoggerFactory getILoggerFactory() {
...
//加载StaticLoggerBinder类
performInitialization();
...
//初始化StaticLoggerBinder,并获取其初始化的日志工厂
return StaticLoggerBinder.getSingleton().getLoggerFactory();
...
}
}

performInitialization 初始化日志系统, 通过加载项目中引入的桥接jar包中的StaticLoggerBinder,绑定特定日志实现框架的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private final static void performInitialization() {
//绑定操作
bind();
...
}
private final static void bind() {
try {
Set<URL> staticLoggerBinderPathSet = null;
if (!isAndroid()) {
//获取项目中所有的“org/slf4j/impl/StaticLoggerBinder.class”文件
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
//当检测到有多个StaticLoggerBinder,会发出警告信息
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// 实现绑定
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
fixSubstituteLoggers();
replayEvents();
// release all resources in SUBST_FACTORY
SUBST_FACTORY.clear();
} catch (NoClassDefFoundError ncde) {
...
}

private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
static Set<URL> findPossibleStaticLoggerBinderPathSet() {
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 {
//获取项目中所有"org/slf4j/impl/StaticLoggerBinder.class"文件
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;
}

与SLF4J相关的桥接包里都有org/slf4j/impl/StaticLoggerBinder类,同时所有的StaticLoggerBinder都实现了LoggerFactoryBinder接口,方法getLoggerFactory()用于返回桥接包内的具体工厂对象(这些工厂对象都在StaticLoggerBinder对象的构造函数中进行了初始化),比如Log4jLoggerFactory、JDK14LoggerFactory、JDK14LoggerFactory…

log

SLF4j官方给出了通过SLF4J桥接其他日志实现框架所依赖的jar。这些包文件中无一例外地包含类StaticLoggerBinder
set up-w888

总结:

  1. SLF4j是通过StaticLoggerBinder来与具体日志实现框架进行桥接的。
  2. 当SLF4J扫描到有多个StaticLoggerBinder的实现时,会发出报警。同时会随机选一个。因此应该尽量杜绝这种情况发生。

另外,SLF4J也给出了在业务代码中也使用了其他日志API的情况下,桥接到SLF4J所依赖的包。
原理是:在这些包内部(log4j-over-slf4j…)实现相应日志API的类(相同的类名称)。比如log4j-over-slf4j 实现了和log4j1同类名的org.apache.log4j.LogManager,而在log4j-over-slf4jLogManager里就实现了桥接SLF4J。
set up-w888

3. 补充说Log4j2的桥接

Log4j2的特殊是因为Log4j2在SLF4j和Common-logging等之后诞生,而Log4j2为了适配这些通用日志接口,不得不做配合它们的适配工作。
下图展示的是 通过其他日志接口桥接到日志实现框架Log4j2所需要引入的依赖。
set up-w580

从图中可以看出:

  1. Log4j2为了桥接Common-logging 提供了Log4j-jcl.jar,资源文件:META-INF/services/org.apache.commons.logging.LogFactory内容:

    1
    org.apache.logging.log4j.jcl.LogFactoryImpl

    对应的即为 上文 Common-logging包 getFactory()方法的第二步。
    set up-w520

  2. Log4j2为了桥接SLF4j 也提供了log4j-slf4j-impl.jar。其包内容如下:提供了org.slf4j.impl.StaticLoggerBinder.class;以起到桥接的作用。

    set up-w520

总结

简化:

1
2
3
4
5
6
7
8
9
10
commons-logging-1.2.jar         X
log4j-1.2.17.jar X
log4j-1.2-api-2.3.jar X
log4j-api-2.1.jar
log4j-core-2.1.jar
log4j-over-slf4j-1.7.7.jar
jcl-over-slf4j-1.7.7.jar
jul-to-slf4j-1.7.7.jar
log4j-slf4j-impl-2.1.jar
slf4j-api-1.7.7.jar
您的支持是我创作源源不断的动力