Java日志框架学习--日志门面--中
Posted 大忽悠爱忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java日志框架学习--日志门面--中相关的知识,希望对你有一定的参考价值。
Java日志框架学习--日志门面--中
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案例
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
Log log = LogFactory.getLog(Log4jTest.class);
log.info("你好");
源码实现
那么具体JCL是如何帮助我们动态完成日志框架底层选型的切换的呢?
LogFactory.getLog(Log4jTest.class);
在上面这段源码的调用链中我们可以看到JCL是如何按照优先级选择合适的日志技术实现的
我们来看看关键的代码:
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数组里面存储着四种可以使用的日志框架实现技术,并且顺序很重要:
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就是拿着当前日志框架的全类名,尝试去实例化,失败了,所有不存在相关依赖,切换下一个
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进行的操作一样
使用演示
<!--slf4j 核心依赖-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!--slf4j 自带的简单日志实现 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
</dependency>
使用演示:
Logger logger = LoggerFactory.getLogger(LogTest.class);
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.debug("debug");
logger.trace("trace");
占位符
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);
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");
private final static void performInitialization()
//绑定操作--真正去寻找日志框架依赖的核心逻辑实现
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION)
versionSanityCheck();
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是真正去查找的逻辑:
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);
//将所有定位到的日志框架依赖加入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,需要因为桥接模块才能完成,下面会讲
//如果同时引入多个日志依赖,那么这里会进行记录
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实现,即禁止任何日志输出
<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,需要引入适配器模块依赖才可以
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
因为适配器模块里面已经包含了log4j和slf4j-api的依赖,因此我们只需要一个适配器模块依赖就可以了
门面,适配器,日志框架本身依赖
这个时候,只需要把log4j相关配置文件拿过来即可:
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 %dyyyy-MM-dd HH:mm:ss::SSS %m%n
原理如下:
集成JDK14做JUL适配器
<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>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
以上是关于Java日志框架学习--日志门面--中的主要内容,如果未能解决你的问题,请参考以下文章
Java日志框架 -- JCL日志门面(JCL概念介绍JCL示例)