日志框架体系及部分框架实现
一直有一个疑惑,日志为什么会有那么多的框架,到今天为止,心里终于有个比较明确的答案了。在这里我把我所理解的日志体系整理描述一下,自己总结一下,也希望对大家学习能有所帮助。
日志框架的分类
个人理解,日志框架可分为两类:
- 门面型日志框架:
- jakartaCommonsLoggingImpl(jcl),
- Simple Logging Facade for Java (SLF4J)
- 记录型日志框架:
- Log4j–Log4JLogger
- jul–Jdk14Logger
- Log4j 2
- logback
还有一些其他的框架,有些是比较老了如Jdk13LumberjackLogger,有些用的较少,这里不做介绍。记录型日志框架不做具体介绍,这里详细说明门面型日志框架.
jcl
jcl=Jakarta commons-logging ,是apache公司开发的一个抽象日志通用框架,本身不实现日志记录,但是提供了记录日志的抽象方法即接口(info,debug,error…),底层通过一个数组存放具体的日志框架的类名,然后循环数组依次去匹配这些类名是否在app中被依赖了,如果找到被依赖的则直接使用,所以他有先后顺序。我们通过代码看一下:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class JCL {
public static void main(String[] args) {
Log log = LogFactory.getLog("jcl");
log.info("jcl");
}
}
上面是一个例子,可以看到我们可以通过jcl提供的一个工厂LogFactory获取到一个日志对象,那么这个日志对象是怎么获取到的呢,门面型究竟是什么意思?接下来我们对源码进行分析,先将源码贴出来:
//org.apache.commons.logging.LogFactory 669行
/**
* Convenience method to return a named logger, without the application
* having to care about factories.
*
* @param name Logical name of the <code>Log</code> instance to be
* returned (the meaning of this name is only known to the underlying
* logging implementation that is being wrapped)
* @throws LogConfigurationException if a suitable <code>Log</code>
* instance cannot be returned
*/
public static Log getLog(String name) throws LogConfigurationException {
return getFactory().getInstance(name);
}
通过方法获取一个工厂
//org.apache.commons.logging.LogFactory 417行
public static LogFactory getFactory() throws LogConfigurationException {
...部分代码省略
return factory;
}
获取到工厂之后,通过工厂获取一个日志对象
//org.apache.commons.logging.impl.LogFactoryImpl 289行
/**
* <p>Construct (if necessary) and return a <code>Log</code> instance,
* using the factory's current set of configuration attributes.</p>
*
* <p><strong>NOTE</strong> - Depending upon the implementation of
* the <code>LogFactory</code> you are using, the <code>Log</code>
* instance you are returned may or may not be local to the current
* application, and may or may not be returned again on a subsequent
* call with the same name argument.</p>
*
* @param name Logical name of the <code>Log</code> instance to be
* returned (the meaning of this name is only known to the underlying
* logging implementation that is being wrapped)
*
* @exception LogConfigurationException if a suitable <code>Log</code>
* instance cannot be returned
*/
public Log getInstance(String name) throws LogConfigurationException {
Log instance = (Log) instances.get(name);
if (instance == null) {
instance = newInstance(name);
instances.put(name, instance);
}
return instance;
}
进入newInstance方法
//org.apache.commons.logging.impl.LogFactoryImpl 537行
protected Log newInstance(String name) throws LogConfigurationException {
Log instance;
try {
if (logConstructor == null) {
instance = discoverLogImplementation(name);
}
else {
Object params[] = { name };
instance = (Log) logConstructor.newInstance(params);
}
if (logMethod != null) {
Object params[] = { this };
logMethod.invoke(instance, params);
}
return instance;
} catch (LogConfigurationException lce) {
// this type of exception means there was a problem in discovery
// and we've already output diagnostics about the issue, etc.;
// just pass it on
throw lce;
} catch (InvocationTargetException e) {
// A problem occurred invoking the Constructor or Method
// previously discovered
Throwable c = e.getTargetException();
throw new LogConfigurationException(c == null ? e : c);
} catch (Throwable t) {
handleThrowable(t); // may re-throw t
// A problem occurred invoking the Constructor or Method
// previously discovered
throw new LogConfigurationException(t);
}
}
进入discoverLogImplementation方法
//org.apache.commons.logging.impl.LogFactoryImpl 771行
private Log discoverLogImplementation(String logCategory)
throws LogConfigurationException {
...部分代码省略
for(int i=0; i<classesToDiscover.length && result == null; ++i) {
result = createLogFromClass(classesToDiscover[i], logCategory, true);
}
if (result == null) {
throw new LogConfigurationException
("No suitable Log implementation");
}
return result;
}
这里最重要的就是其中的for循环,有一个常量
/**
* The names of classes that will be tried (in order) as logging
* adapters. Each class is expected to implement the Log interface,
* and to throw NoClassDefFound or ExceptionInInitializerError when
* loaded if the underlying logging library is not available. Any
* other error indicates that the underlying logging library is available
* but broken/unusable for some reason.
*/
private static final String[] classesToDiscover = {
"org.apache.commons.logging.impl.Log4JLogger",
"org.apache.commons.logging.impl.Jdk14Logger",
"org.apache.commons.logging.impl.Jdk13LumberjackLogger",
"org.apache.commons.logging.impl.SimpleLog"
};
由此可以看出,循环其中的常量调用createLogFromClass方法
//org.apache.commons.logging.impl.LogFactoryImpl 960行
private Log createLogFromClass(String logAdapterClassName,
String logCategory,
boolean affectState)
throws LogConfigurationException {
for(;;) {
...部分代码省略
Class c;
try {
c = Class.forName(logAdapterClassName, true, currentCL);
} catch (ClassNotFoundException originalClassNotFoundException) {
String msg = originalClassNotFoundException.getMessage();
logDiagnostic("The log adapter '" + logAdapterClassName + "' is not available via classloader " +
objectId(currentCL) + ": " + msg.trim());
try {
c = Class.forName(logAdapterClassName);
} catch (ClassNotFoundException secondaryClassNotFoundException) {
// no point continuing: this adapter isn't available
msg = secondaryClassNotFoundException.getMessage();
logDiagnostic("The log adapter '" + logAdapterClassName +
"' is not available via the LogFactoryImpl class classloader: " + msg.trim());
break;
}
}
...部分代码省略
return logAdapter;
}
上面这个方法会对前面的那个常量的每一个类进行加载,如果加载到则返回,否则会继续进入下一个值进行加载。由此可以知道,jcl可以通过forName将按顺序将存在的log创建出来以供APP应用。
**jcl总结:**通过源码分析,我们知道jcl是固定的加载四种log,当然四种足够用了,因为其中有两个是JDK自带的,如Jdk14Logger是JDK自带的,所以肯定可以获取到。但是,jcl无法继续扩展了,对于新的日志框架如Log4j 2,logback等都无法满足了。
因此出现了一个新的门面型日志框架slf4j。
slf4j
不同与jcl的固定四种,slf4j有两个概念:
- binding(绑定):通过绑定器可以绑定到具体的记录日志框架
- bridging(桥接):通过桥接器可以将记录日志框架桥接到slf4j上
slf4j是通过通过绑定器绑定一个具体的日志记录来完成日志记录,如
- slf4j-log4j12-1.8.0-beta4.jar可以绑定到log4j 2
- slf4j-jdk14-1.8.0-beta4.jar可以绑定到jul
- slf4j-jcl-1.8.0-beta4.jar可以绑定到jcl
- …
绑定详细可以参考下图:
那么,桥接器的作用是为了将一个记录日志框架桥接到slf4j上,以便绑定到一个新的需要的记录日志框架,这个一帮是在项目整合或者使用新的日志框架会使用,如:
- jcl可以通过jcl-over-slf4j.jar桥接到slf4j
- jul可以通过jul-to-slf4j桥接到slf4j
- log4j可以通过log4j-over-slf4j桥接到slf4j
- …
桥接详细请参考下图
由此可知,通过绑定器以及桥接器,我们可以灵活的整合多种日志框架。
比如,有一个场景,旧的应用是用的log4j记录日志,现在要与一个新的应用整合在一起,而新的应用是通过slf4j绑定的jul记录的日志,如下图。
现在想将旧应用也修改成jul记录日志,那么就可以通过slf4j直接将log4j的桥接到slf4j,这样就可以绑定到jul记录日志,完美的解决问题。如下图:
spring4与spring5日志技术实现区别
- spring4是依赖原生的jcl技术;
- spring5使用的是spring-jcl(spring改了jcl的代码)来记录日志的
两者的区别:
spring4是原生的jcl,通过上面的介绍知道,原生jcl固定配置了四种日志框架,分别是:
- log4j
- jul-Jdk14Logger
- jul-Jdk13LumberjackLogger
- simple-log
而spring5使用的是spring通过改造jcl形成的spring-jcl,类似于原生jcl,它也是固定配置了几种日志框架,只是种类和顺序不同,看下源码:
//spring-5.1.x org.apache.commons.logging.LogAdapter 37行
final class LogAdapter {
private static final String LOG4J_SPI = "org.apache.logging.log4j.spi.ExtendedLogger";
private static final String LOG4J_SLF4J_PROVIDER = "org.apache.logging.slf4j.SLF4JProvider";
private static final String SLF4J_SPI = "org.slf4j.spi.LocationAwareLogger";
private static final String SLF4J_API = "org.slf4j.Logger";
private static final LogApi logApi;
static {
if (isPresent(LOG4J_SPI)) {
if (isPresent(LOG4J_SLF4J_PROVIDER) && isPresent(SLF4J_SPI)) {
// log4j-to-slf4j bridge -> we'll rather go with the SLF4J SPI;
// however, we still prefer Log4j over the plain SLF4J API since
// the latter does not have location awareness support.
logApi = LogApi.SLF4J_LAL;
}
else {
// Use Log4j 2.x directly, including location awareness support
logApi = LogApi.LOG4J;
}
}
else if (isPresent(SLF4J_SPI)) {
// Full SLF4J SPI including location awareness support
logApi = LogApi.SLF4J_LAL;
}
else if (isPresent(SLF4J_API)) {
// Minimal SLF4J API without location awareness support
logApi = LogApi.SLF4J;
}
else {
// java.util.logging as default
logApi = LogApi.JUL;
}
}
...
}
通过代码可以看到,spring通过一段静态域按顺序加载对应的日志框架,最后默认的是jul,其顺序是:
- log4j 2
- slf4j
- jul(默认)
##mybatis日志技术实现
//org.apache.ibatis.logging.LogFactory 24行
public final class LogFactory {
/**
* Marker to be used by logging implementations that support markers
*/
public static final String MARKER = "MYBATIS";
private static Constructor<? extends Log> logConstructor;
static {
tryImplementation(new Runnable() {
@Override
public void run() {
useSlf4jLogging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useCommonsLogging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useLog4J2Logging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useLog4JLogging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useJdkLogging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useNoLogging();
}
});
}
...
}
类似spring-jcl,mybatis也是通过一段静态域去加载,其主要方法源码如下:
//org.apache.ibatis.logging.LogFactory 120行
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
由源码可以看到,mybatis日志框架的加载顺序是:
- slf4j
- jcl
- log4j 2
- log4j
- jul
- nolog
mybatis提供很多日志的实现类,用来记录日志,取决于初始化的时候load到的class,如下图:
上图红色箭头可以看到JakartaCommonsLoggingImpl中引用了jcl 的类,如果在初始化的时候load到类为JakartaCommonsLoggingImpl那么则使用jcl 去实现日志记录,但是也是顺序的,顺序参考源码
这里留个问题,大家可以思考一下:
spring4+log4j 可以正常打印log4j日志,但是spring5+log4j 是无法打印log4j日志,通过以上的各个技术的是实现分析,大家能得出答案吗?