背景
在Java项目中,一般都会用到日志框架,面临使用哪一种日志框架,多种日志框架适配的问题。
这篇文章的本意是梳理常用的日志框架并梳理日志适配的原理,并整理常遇到的问题。
5种常用日志框架适配图
其中
- jcl-over-slf4j.jar 桥接Commons Logging的logger到SLF4j logger
- jul-to-slf4j.jar 桥接java.util.logging的logger到SLF4j logger
- log4j-to-sfl4j.jar 桥接Log4j2的logger到SLF4j logger
- log4j-over-sfl4j.jar 桥接Log4j 1.x的logger到SLF4j logger
- log4j-jcl-2.x.jar 桥接Commons Logging的logger到Log4j2 logger
- log4j-jul-2.x.jar 桥接java.util.logging的logger到Log4j2 logger
- log4j-slf4j-impl.jar 桥接SLF4j的logger到Log4j2 logger
- log4j-1.2-api-2.x.jar 桥接Log4j 1.x的logger到Log4j2 logger
- logback-classic.jar SLF4j API的原生实现
- log4j-core-2.x.jar Log4j2 API的原生实现
日志桥接适配原理
Commons Logging
通过Java的SPI机制实现加载不同的日志实现,在commons-logging.jar的org.apache.commons.logging.LogFactory#getFactory方法中,通过SERVICE_ID(META-INF/services/org.apache.commons.logging.LogFactory)获取到LogFactory的实现,具体如下:
// Second, try to find a service by using the JDK1.3 class
// discovery mechanism, which involves putting a file with the name
// of an interface class in the META-INF/services directory, where the
// contents of the file is a single line specifying a concrete class
// that implements the desired interface.
if (factory == null) {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] Looking for a resource file of name [" + SERVICE_ID +
"] to define the LogFactory subclass to use...");
}
try {
final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);
if( is != null ) {
// This code is needed by EBCDIC and other strange systems.
// It's a fix for bugs reported in xerces
BufferedReader rd;
try {
rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
} catch (java.io.UnsupportedEncodingException e) {
rd = new BufferedReader(new InputStreamReader(is));
}
String factoryClassName = rd.readLine();
rd.close();
如果在应用的运行时不包含commons-logging.jar,因为jcl-over-slf4j.jar包含org.apache.commons.logging.LogFactory类,并且使用SLF4JLogFactory作为实现,也是能正常工作的。
jcl-over-slf4j.jar和log4j-jcl-2.x.jar中,都包含SERVICE_ID文件
slf4j使用 org.apache.commons.logging.impl.SLF4JLogFactory作为日志工厂的实现类
log4j2使用 org.apache.logging.log4j.jcl.LogFactoryImpl 作为日志工厂的实现类
java.util.logging
jul是JDK自带的日志实现,使用不同的java.util.logging.Handler来实现对日志的不同操作。
针对jul的handler,需要手动处理
slf4j使用SLF4JBridgeHandler,手动处理过程如下:
/*
FINEST -> TRACE
FINER -> DEBUG
FINE -> DEBUG
INFO -> INFO
WARNING -> WARN
SEVERE -> ERROR
*/
if (!SLF4JBridgeHandler.isInstalled()) {
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
// it's important, default the logger level is INFO
java.util.logging.Logger.getLogger("").setLevel(java.util.logging.Level.FINEST);
}
log4j2使用Log4jBridgeHandler,手动处理过程如下:
/*
ALL --> ALL
FINEST --> FINEST
FINER --> TRACE
FINE --> DEBUG
CONFIG --> CONFIG
INFO --> INFO
WARNING --> WARN
SEVERE --> ERROR
OFF --> OFF
*/
if (!isInstalled()) {
Log4jBridgeHandler.install(true, ".", true);
// because set propagateLevels = true, will propagate log4j2's level setting to jul
}
另,
在spring boot中,通过 org.springframework.boot.logging.Slf4JLoggingSystem#configureJdkLoggingBridgeHandler来处理jul的日志桥接,所以平时感知不到,具体如下:
private void configureJdkLoggingBridgeHandler() {
try {
if (isBridgeJulIntoSlf4j()) {
removeJdkLoggingBridgeHandler();
SLF4JBridgeHandler.install();
}
}
catch (Throwable ex) {
// Ignore. No java.util.logging bridge is installed.
}
}
/**
* Return whether bridging JUL into SLF4J or not.
* @return whether bridging JUL into SLF4J or not
* @since 2.0.4
*/
protected final boolean isBridgeJulIntoSlf4j() {
return isBridgeHandlerAvailable() && isJulUsingASingleConsoleHandlerAtMost();
}
Log4j 1.x
因为log4j 1.x直接耦合了具体日志工厂实现,所以为了兼容这部分,slf4j和log4j2都是重写了这部分。
即,在应用的运行时,不能把log4j-1.x.jar放在classpath中,不然的话,依然使用原log4j 1.x的实现。
在log4j-over-sfl4j.jar中,具体的重写如下:
在log4j-1.2-api-2.x.jar中,实现时,保留了原有的Hierarchy、Category,最终都是通过log4j2的LoggerContext获取logger。
内部实现叫复杂,个人以为完全可以摒弃原有的实现,类似slf4j,包装log4j2的logger来实现。
具体如下:
一些遇到的问题
日志桥接循环
- log4j-slf4j-impl.jar和log4j-to-sfl4j.jar不能同时存在,如果同时放到运行时,会导致运行路由桥接
log4j 1.x的日志不生效
- log4j 1.x 和 log4j-1.2-api-2.x.jar、log4j-over-sfl4j.jar中都有同一个org.apache.log4j.Logger的实现,在运行时,哪一个jar先加载到,就先使用哪一个实现。会报如下错误:
log4j:WARN No appenders could be found for logger(com.pieland.loggings.slf4japi.log4j2impl.LotsLoggerRun1).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
以上梳理过程的代码都记录在github,参见 https://github.com/gladpieland/lotslogging
参考
https://stackoverflow.com/questions/42912335/how-to-programmatically-setup-slf4j-logger-with-slf4jbridgehandler
https://logging.apache.org/log4j/2.x/faq.html
https://stackoverflow.com/questions/31044619/difference-between-slf4j-log4j12-and-log4j-over-slf4j
https://logging.apache.org/log4j/2.x/log4j-slf4j-impl/index.html