Java日志框架学习--日志门面--中
- JCL
- JCL简介
- JCL案例
- 源码实现
- SLF4J
- 门面模式(外观模式)
- 日志门面
- 常见的日志框架及日志门面
- SLF4J简介
- SLF4J桥接技术
- 使用演
- 占位符
- 异常打印
- 集成其他日志框架
- nop禁止日志打印
- 集成Log4j
- 集成JDK14做JUL适配器
- 通过桥接模块解决项目日志重构
- 源码分析桥接过程
JCL
JCL简介
全称为Jakarta Commons Logging,是Apache提供的一个通用日志API。
用户可以自由选择第三方的日志组件作为具体实现,像log4j,或者jdk自带的jul, common-logging会通过动态查找的机制,在程序运行时自动找出真正使用的日志库。
当然,common-logging内部有一个Simple logger的简单实现,但是功能很弱。所以使用common-logging,通常都是配合着log4j以及其他日志框架来使用。
使用它的好处就是,代码依赖是common-logging而非log4j的API, 避免了和具体的日志API直接耦合,在有必要时,可以更改日志实现的第三方库。
JCL 有两个基本的抽象类:
- Log:日志记录器
- LogFactory:日志工厂(负责创建Log实例)
JCL案例
代码语言:javascript复制<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
代码语言:javascript复制 Log log = LogFactory.getLog(Log4jTest.class);
log.info("你好");
源码实现
那么具体JCL是如何帮助我们动态完成日志框架底层选型的切换的呢?
代码语言:javascript复制LogFactory.getLog(Log4jTest.class);
在上面这段源码的调用链中我们可以看到JCL是如何按照优先级选择合适的日志技术实现的
我们来看看关键的代码:
代码语言:javascript复制 private Log discoverLogImplementation(String logCategory)
throws LogConfigurationException {
if (isDiagnosticsEnabled()) {
logDiagnostic("Discovering a Log implementation...");
}
initConfiguration();
Log result = null;
// See if the user specified the Log implementation to use
//如果用户自己指定了具体的日志框架选型的话,就优先采用用户自己指定的
String specifiedLogClassName = findUserSpecifiedLogClassName();
if (specifiedLogClassName != null) {
....
return result;
}
//如果用户没有特殊指定,那么就挨个遍历classesToDiscover数组,寻找可以用的日志框架实现
//如果有一个返回结果不为空,那么结束遍历,因此数组里面元素优先级很重要
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;
}
下面有两个疑问:
- classesToDiscover是什么?
- createLogFromClass干了啥?
classesToDiscover数组里面存储着四种可以使用的日志框架实现技术,并且顺序很重要:
代码语言:javascript复制 private static final String LOGGING_IMPL_LOG4J_LOGGER = "org.apache.commons.logging.impl.Log4JLogger";
private static final String[] classesToDiscover = {
LOGGING_IMPL_LOG4J_LOGGER,
"org.apache.commons.logging.impl.Jdk14Logger",
"org.apache.commons.logging.impl.Jdk13LumberjackLogger",
"org.apache.commons.logging.impl.SimpleLog"
};
createLogFromClass就是拿着当前日志框架的全类名,尝试去实例化,失败了,所有不存在相关依赖,切换下一个
代码语言:javascript复制 private Log createLogFromClass(String logAdapterClassName,
String logCategory,
boolean affectState)
throws LogConfigurationException {
Object[] params = { logCategory };
Log logAdapter = null;
Constructor constructor = null;
Class logAdapterClass = null;
ClassLoader currentCL = getBaseClassLoader();
for(;;) {
// Loop through the classloader hierarchy trying to find
// a viable classloader.
logDiagnostic("Trying to load '" logAdapterClassName "' from classloader " objectId(currentCL));
try {
...
Class c;
try {
//尝试去实例化当前日志框架
c = Class.forName(logAdapterClassName, true, currentCL);
} ...
//实例化成功--那就选择当前日志框架选型
constructor = c.getConstructor(logConstructorSignature);
Object o = constructor.newInstance(params);
if (o instanceof Log) {
logAdapterClass = c;
logAdapter = (Log) o;
break;
}
handleFlawedHierarchy(currentCL, c);
} catch (NoClassDefFoundError e) {
//一般当前日志依赖不存在,都会抛出该异常
....
break;
} catch (ExceptionInInitializerError e) {
...
break;
} catch (LogConfigurationException e) {
...
throw e;
} catch (Throwable t) {
handleThrowable(t); // may re-throw t
handleFlawedDiscovery(logAdapterClassName, currentCL, t);
}
if (currentCL == null) {
break;
}
// try the parent classloader
// currentCL = currentCL.getParent();
currentCL = getParentClassLoader(currentCL);
}
...
//实例化成功,返回结果不为空,否则为空
return logAdapter;
}
SLF4J
门面模式(外观模式)
我们先谈一谈GoF23种设计模式其中之一。
门面模式(Facade Pattern),也称之为外观模式,其核心为:外部与一个子系统的通信必须通过一个统一的外观对象进行,使得子系统更易于使用。
外观模式主要是体现了Java中的一种好的封装性。更简单的说,就是对外提供的接口要尽可能的简单。
日志门面
前面介绍的几种日志框架,每一种日志框架都有自己单独的API,要使用对应的框架就要使用其对应的API,这就大大的增加应用程序代码对于日志框架的耦合性。
为了解决这个问题,就是在日志框架和应用程序之间架设一个沟通的桥梁,对于应用程序来说,无论底层的日志框架如何变,都不需要有任何感知。只要门面服务做的足够好,随意换另外一个日志框架,应用程序不需要修改任意一行代码,就可以直接上线。
常见的日志框架及日志门面
常见的日志实现:JUL、log4j、logback、log4j2
常见的日志门面 :JCL、slf4j
出现顺序 :log4j -->JUL–>JCL–> slf4j --> logback --> log4j2
SLF4J简介
简单日志门面(Simple Logging Facade For Java) SLF4J主要是为了给Java日志访问提供一套标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架,例如log4j和logback等。
当然slf4j自己也提供了功能较为简单的实现,但是一般很少用到。
对于一般的Java项目而言,日志框架会选择slf4j-api作为门面,配上具体的实现框架(log4j、logback等),中间使用桥接器完成桥接。所以我们可以得出SLF4J最重要的两个功能就是对于日志框架的绑定以及日志框架的桥接。
SLF4J桥接技术
通常,我们依赖的某些组件依赖于SLF4J以外的日志API。我们可能还假设这些组件在不久的将来不会切换到SLF4J。为了处理这种情况,SLF4J附带了几个桥接模块,这些模块会将对log4j,JCL和java.util.logging API的调用重定向为行为,就好像是对SLF4J API进行的操作一样
使用演示
代码语言:javascript复制<!--slf4j 核心依赖-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
代码语言:javascript复制 <!--slf4j 自带的简单日志实现 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
</dependency
使用演示:
代码语言:javascript复制 Logger logger = LoggerFactory.getLogger(LogTest.class);
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.debug("debug");
logger.trace("trace");
占位符
代码语言:javascript复制 Logger logger = LoggerFactory.getLogger(LogTest.class);
logger.info("info,{},{}",1,2);
异常打印
直接传入异常对象即可
集成其他日志框架
那么Slf4j是如何完成日志框架的动态选择的呢?—让我们来看看吧
- 下面只会列举关键的代码
public static Logger getLogger(String name) {
//返回的LoggerFactory就已经决定了底层会采用哪种日志框架
//因此我们需要追踪一下getILoggerFactory的实现
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
代码语言:javascript复制 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();
//没有引入任何日志框架的依赖--那么使用NOPLoggerFactory--即啥也不干的日记记录器
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");
}
代码语言:javascript复制 private final static void performInitialization() {
//绑定操作--真正去寻找日志框架依赖的核心逻辑实现
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
versionSanityCheck();
}
}
代码语言:javascript复制 private final static void bind() {
try {
//存放找到日志框架实现的依赖
Set<URL> staticLoggerBinderPathSet = null;
// skip check under android, see also
// http://jira.qos.ch/browse/SLF4J-328
if (!isAndroid()) {
//寻找可用的StaticLoggerBinder--为啥要寻找他,后面会讲
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
//如果同时引入了多个日志框架依赖,这里会进行日志记录
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
...
findPossibleStaticLoggerBinderPathSet是真正去查找的逻辑:
代码语言:javascript复制 private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
代码语言:javascript复制 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);
}
//将所有定位到的日志框架依赖加入staticLoggerBinderPathSet
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,就可以找到引入的所有日志框架依赖呢?
因为slf4j-simple和logback因为遵循了slf4j规范,都存在该静态日志记录绑定器,因此我们可以通过去类路径下搜索该类,来获取到所有依赖包,至于jcl和logback,需要因为桥接模块才能完成,下面会讲
代码语言:javascript复制 //如果同时引入多个日志依赖,那么这里会进行记录
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.");
}
}
继续回到bind方法:
这里有个非常有意思的点:
- StaticLoggerBinder的包路径为import org.slf4j.impl.StaticLoggerBinder;但是我们来看看Slf4j的源码包
当然,带领大家看的是编译打包后的源码包,显然压根不存在org.slf4j.impl.StaticLoggerBinder这样一个类,这是为什么呢?
这里通过调用ant在打包为jar文件前,将package org.slf4j.impl和其下的class都删除掉了。
实际上这里的impl package内的代码,只是用来占位以保证可以编译通过(所谓dummy)。需要在运行时再进行绑定。
在slf4j-simple和logback中都存在对应的路径,这样就可以完成运行时的动态绑定,当然如果没有引入相关依赖,那么运行时这个类的定义压根就找不到,那么就会抛出异常,这也是为什么需要捕获相关异常的原因了
可以看到,如果引入了多个依赖,那么运行时会优先选择先引入的依
nop禁止日志打印
我们也可以导入nop依赖,来强制采用nop实现,即禁止任何日志输出
代码语言:javascript复制 <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.36</version>
</dependency>
集成Log4j
前面说过,logback,simple,和nop都是在SLF4J之后出来的,都遵循器规范API,因此不需要适配器,引入依赖直接可以使用,但是对于log4j和logging来说,因为其出现时间早于slf4j,因此需要通过适配器模块完成适配才可以使用
即,如果我们想要在Slf4j中无缝使用log4j和logging,需要引入适配器模块依赖才可以
代码语言:javascript复制 <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
因为适配器模块里面已经包含了log4j和slf4j-api的依赖,因此我们只需要一个适配器模块依赖就可以了
门面,适配器,日志框架本身依赖
这个时候,只需要把log4j相关配置文件拿过来即可:
代码语言:javascript复制log4j.rootLogger=info,console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.conversionPattern=[%-8p] %r %c %t %d{yyyy-MM-dd HH:mm:ss::SSS} %m%n
原理如下:
集成JDK14做JUL适配器
代码语言:javascript复制 <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.36</version>
</dependency>
同样只需要导入一个依赖即可,因为slf4j-api已经帮我们导入好了,而JUL是java内置的,因此不需要导入
通过桥接模块解决项目日志重构
上面都是通过适配器模式完成的日志适配,但是下面我给出一个需求,大家思考一下该怎么办?
- 有一个老项目,日志使用log4j完成记录,但是此时领导要求将日志框架全部更换为slf4j logback的组合
- 请你在不改动原有日志代码的基础上,完成架构更迭
这个时候就需要使用桥接模块,进行伪装,完成架构替换
其余几个原理类似,我们先来看看具体操作过程,然后再来分析原理:
- 移除log4j的依赖
开始爆红了
- 添加桥接器模块和logback的依赖
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.36</version>
</dependency>
代码语言:javascript复制 <dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
- 测试
源码分析桥接过程
首先我们可以看到,这个所谓的桥接器,只引入了一个slf4j-api的门面依赖,猜测是模拟了log4j的包路径,然后将api最终重定向到了slf4j中,下面我们看看是怎么完成api的重定向的
代码语言:javascript复制//桥接逻辑在getLogger方法中完成,我们来追踪进去看看
Logger logger = Logger.getLogger(LogTest.class.getName());
代码语言:javascript复制 public static Logger getLogger(String name) {
//继续看
return Log4jLoggerFactory.getLogger(name);
}
代码语言:javascript复制 public static Logger getLogger(String name) {
Logger instance = (Logger)log4jLoggers.get(name);
if (instance != null) {
return instance;
} else {
//这里还是先查缓存,然后将结果再放入缓存
//但是我们想知道的是具体狸猫换太子的把戏是在哪里完成的
//其实就是在这个logger的构造函数中完成的
Logger newInstance = new Logger(name);
Logger oldInstance = (Logger)log4jLoggers.putIfAbsent(name, newInstance);
return oldInstance == null ? newInstance : oldInstance;
}
}
Logger构造函数
代码语言:javascript复制 //最终是调用父类Category的构造函数
Category(String name) {
this.name = name;
//LoggerFactory创建的就是slf4j的门面Logger
this.slf4jLogger = LoggerFactory.getLogger(name);
if (this.slf4jLogger instanceof LocationAwareLogger) {
this.locationAwareLogger = (LocationAwareLogger)this.slf4jLogger;
}
}
这里相当于在原本的log4j的Category中增加两个对slf4j的Logger的引用
然后我们再来看看输出日志的时候,做了怎样的桥接工作
代码语言:javascript复制 //在该桥接模块中,所有日志级别的输出,都会委托该方法完成
void differentiatedLog(Marker marker, String fqcn, int level, Object message, Throwable t) {
String m = convertToString(message);
//locationAwareLogger和slf4jLogger引用是相同的,因此最终还是交给了slf4j完成的日志输出
if (locationAwareLogger != null) {
locationAwareLogger.log(marker, fqcn, level, m, null, t);
} else {
//这里就是直接交给了slf4j的Logger进行日志输出
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;
}
}
}