java日志框架总览
绑定和桥接
绑定(Binder)和桥接(Bridge):
绑定: 日志门面接口绑定到具体的日志实现;
桥接: 某个日志实现桥接到日志门面。
绑定很好理解, 桥接是干什么用的呢?
比如说你的应用是slf4j+log4j2, 但你引入的一个依赖包没有使用slf4j, 使用的是log4j. 怎么搞? 难道你项目又得搞两套配置了? 这个时候你就应该排除依赖包的log4j, 然后引入log4j到slf4j的桥接包就行了. 实际上桥接的原理就是把对应的门面接口重新实现了一遍,包名、类名、接口都一样,只是具体的实现委托给门面slf4j。
使用日志框架的注意点
1. 避免循环依赖.
如果你的应用使用的是slf4和log4j,但同时引入了绑定报slf4j-log4j12和桥接包log4j-over-slf4j, 那么就会循环依赖,项目启动报错.
2. 引入了多个绑定包
引入多个绑定包系统会加载多个StaticLoggerBinder.class(后面会分析绑定过程), 不会报错, 但此时无法按照预期打印日志,但不能确定你的文件系统到底会使用哪个.StaticLoggerBinder,所以造成打印日志结果的不确定性.
3. 具体的日志实现依赖应该设置为optional和使用runtime scope. 设为optional,依赖不会传递. 这样如果你是个lib项目,然后别的项目使用了你这个lib,不会被引入不想要的Log Implementation 依赖;Scope设置为runtime,是为了防止开发人员在项目中直接使用Log Implementation中的类,而不适用Log Facade中的类。
4. 如果有必要, 排除依赖的第三方库中的Log Impementation依赖
第三方库的开发者未必会把具体的日志实现或者桥接器的依赖设置为optional,然后你的项目继承了这些依赖——具体的日志实现未必是你想使用的,比如他依赖了Log4j,你想使用Logback,这时就很尴尬。另外,如果不同的第三方依赖使用了不同的桥接器和Log实现,也极容易形成循环依赖.
绑定的原理分析
从 Logger logger = LoggerFactory.getLogger("xxxx")
开始看起:
/**
* 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);
}
这里就是在通过静态绑定实例获取实际的Logger实现. 进入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();
}
}
}
switch (INITIALIZATION_STATE) {
case SUCCESSFUL_INITIALIZATION:
return StaticLoggerBinder.getSingleton().getLoggerFactory();
case NOP_FALLBACK_INITIALIZATION:
return NOP_FALLBACK_FACTORY;
case FAILED_INITIALIZATION:
throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
case ONGOING_INITIALIZATION:
// support re-entrant behavior.
// See also http://jira.qos.ch/browse/SLF4J-97
return SUBST_FACTORY;
}
throw new IllegalStateException("Unreachable code");
}
这个方式是返回ILoggerFactory实例, 并且根据注释,这个实例的具体类型是在编译期间就确定了. (为什么这么说?看完后面就知道了.因为编译期间就能确定使用哪个静态日志绑定StaticLoggerBinder了. 具体的LoggerFactory实现是有LoggerFactory确定的). 继续进入performInitialization方法,看看ILoggerFactory的初始化过程:
private final static void performInitialization() {
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
versionSanityCheck();
}
}
private final static void bind() {
// 去掉无关代码,只显示核心代码
// 找到当前classpath下org/slf4j/impl/StaticLoggerBinder.class 的所有实现的路径
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
// the next line does the binding
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
}
进入findPossibleStaticLoggerBinderPathSet()方法:
// We need to use the name of the StaticLoggerBinder class, but we can't
// reference
// the class itself.
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;
}
很简单,这个方法其实就是找到所有的名为”org/slf4j/impl/StaticLoggerBinder.class”的classpath路径. 进入reportMultipleBindingAmbiguity()方法:
/**
* Prints a warning message on the console if multiple bindings were found
* on the class path. No reporting is done otherwise.
*
*/
private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
Util.report("Class path contains multiple SLF4J bindings.");
for (URL path : binderPathSet) {
Util.report("Found binding in [" + path + "]");
}
Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
}
}
这个方法就是当找到多个StaticLoggerBinder类的话,就会打印warning日志. 下一行 StaticLoggerBinder.getSingleton();
就是在初始化StaticLoggerBinder实例了. 这里要注意两点:
- 如果系统找到了多个StaticLoggerBinder类, 那这里会初始化哪个类呢? 答案是不确定. 那要看你操作系统最先找到哪个类就是哪个类咯. 所以项目要避免多个绑定包的情况.
- 这行代码是在slf4j中调用的, 但slf4j本身没有StaticLoggerBinder这个类, 那这个包怎么打出来的,不会编译报错? 这个就要去github看看slf4j的源码了.玄机就在于在源码的poim.xml中.去看下gitlab的slf4j源码: https://github.com/qos-ch/slf4j/blob/v_1.7.21/slf4j-api/pom.xml 中的pom文件. 里面使用了maven-antrun-plugin插件在编译之后删掉了org.slf4j.impl包.
这样就分析完成了. 最后通过StaticLoggerBinder就拿到了日志实现的真正日志工程Log4jLoggerFactory.
桥接的原理分析
桥接的原理就是把对应的门面接口重新实现了一遍,包名、类名、接口都一样,只是具体的实现委托给门面slf4j。
以log4j-over-slf4j为例,看下Logger.info("xxx")
的实现:
/**
* Delegates to {@link org.slf4j.Logger#info(String)} method in SLF4J.
*/
public void info(Object message) {
differentiatedLog(null, CATEGORY_FQCN, LocationAwareLogger.INFO_INT, message, null);
}
注释中已经写明了是slf4j的代理了. 进入differentiatedLog方法看看:
void differentiatedLog(Marker marker, String fqcn, int level, Object message, Throwable t) {
String m = convertToString(message);
if (locationAwareLogger != null) {
locationAwareLogger.log(marker, fqcn, level, m, null, t);
} else {
switch (level) {
case LocationAwareLogger.TRACE_INT:
slf4jLogger.trace(marker, m);
break;
case LocationAwareLogger.DEBUG_INT:
slf4jLogger.debug(marker, m);
break;
case LocationAwareLogger.INFO_INT:
slf4jLogger.info(marker, m);
break;
case LocationAwareLogger.WARN_INT:
slf4jLogger.warn(marker, m);
break;
case LocationAwareLogger.ERROR_INT:
slf4jLogger.error(marker, m);
break;
}
}
}
里面就直接调用了slf4j的接口方法. 所以说桥接的作用就是把日志实现桥接到日志门面上去.
参考
https://zhuanlan.zhihu.com/p/24272450
https://zhuanlan.zhihu.com/p/24275518