Java日志框架学习--日志门面--中

x33g5p2x  于2022-05-16 转载在 Java  
字(11.5k)|赞(0)|评价(0)|浏览(688)

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案例

  1. <dependency>
  2. <groupId>commons-logging</groupId>
  3. <artifactId>commons-logging</artifactId>
  4. <version>1.2</version>
  5. </dependency>

  1. Log log = LogFactory.getLog(Log4jTest.class);
  2. log.info("你好");

源码实现

那么具体JCL是如何帮助我们动态完成日志框架底层选型的切换的呢?

  1. LogFactory.getLog(Log4jTest.class);

在上面这段源码的调用链中我们可以看到JCL是如何按照优先级选择合适的日志技术实现的

我们来看看关键的代码:

  1. private Log discoverLogImplementation(String logCategory)
  2. throws LogConfigurationException {
  3. if (isDiagnosticsEnabled()) {
  4. logDiagnostic("Discovering a Log implementation...");
  5. }
  6. initConfiguration();
  7. Log result = null;
  8. // See if the user specified the Log implementation to use
  9. //如果用户自己指定了具体的日志框架选型的话,就优先采用用户自己指定的
  10. String specifiedLogClassName = findUserSpecifiedLogClassName();
  11. if (specifiedLogClassName != null) {
  12. ....
  13. return result;
  14. }
  15. //如果用户没有特殊指定,那么就挨个遍历classesToDiscover数组,寻找可以用的日志框架实现
  16. //如果有一个返回结果不为空,那么结束遍历,因此数组里面元素优先级很重要
  17. for(int i=0; i<classesToDiscover.length && result == null; ++i) {
  18. result = createLogFromClass(classesToDiscover[i], logCategory, true);
  19. }
  20. if (result == null) {
  21. throw new LogConfigurationException
  22. ("No suitable Log implementation");
  23. }
  24. return result;
  25. }

下面有两个疑问:

  • classesToDiscover是什么?
  • createLogFromClass干了啥?

classesToDiscover数组里面存储着四种可以使用的日志框架实现技术,并且顺序很重要:

  1. private static final String LOGGING_IMPL_LOG4J_LOGGER = "org.apache.commons.logging.impl.Log4JLogger";
  2. private static final String[] classesToDiscover = {
  3. LOGGING_IMPL_LOG4J_LOGGER,
  4. "org.apache.commons.logging.impl.Jdk14Logger",
  5. "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
  6. "org.apache.commons.logging.impl.SimpleLog"
  7. };

createLogFromClass就是拿着当前日志框架的全类名,尝试去实例化,失败了,所有不存在相关依赖,切换下一个

  1. private Log createLogFromClass(String logAdapterClassName,
  2. String logCategory,
  3. boolean affectState)
  4. throws LogConfigurationException {
  5. Object[] params = { logCategory };
  6. Log logAdapter = null;
  7. Constructor constructor = null;
  8. Class logAdapterClass = null;
  9. ClassLoader currentCL = getBaseClassLoader();
  10. for(;;) {
  11. // Loop through the classloader hierarchy trying to find
  12. // a viable classloader.
  13. logDiagnostic("Trying to load '" + logAdapterClassName + "' from classloader " + objectId(currentCL));
  14. try {
  15. ...
  16. Class c;
  17. try {
  18. //尝试去实例化当前日志框架
  19. c = Class.forName(logAdapterClassName, true, currentCL);
  20. } ...
  21. //实例化成功--那就选择当前日志框架选型
  22. constructor = c.getConstructor(logConstructorSignature);
  23. Object o = constructor.newInstance(params);
  24. if (o instanceof Log) {
  25. logAdapterClass = c;
  26. logAdapter = (Log) o;
  27. break;
  28. }
  29. handleFlawedHierarchy(currentCL, c);
  30. } catch (NoClassDefFoundError e) {
  31. //一般当前日志依赖不存在,都会抛出该异常
  32. ....
  33. break;
  34. } catch (ExceptionInInitializerError e) {
  35. ...
  36. break;
  37. } catch (LogConfigurationException e) {
  38. ...
  39. throw e;
  40. } catch (Throwable t) {
  41. handleThrowable(t); // may re-throw t
  42. handleFlawedDiscovery(logAdapterClassName, currentCL, t);
  43. }
  44. if (currentCL == null) {
  45. break;
  46. }
  47. // try the parent classloader
  48. // currentCL = currentCL.getParent();
  49. currentCL = getParentClassLoader(currentCL);
  50. }
  51. ...
  52. //实例化成功,返回结果不为空,否则为空
  53. return logAdapter;
  54. }

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进行的操作一样

使用演示

  1. <!--slf4j 核心依赖-->
  2. <dependency>
  3. <groupId>org.slf4j</groupId>
  4. <artifactId>slf4j-api</artifactId>
  5. <version>1.7.25</version>
  6. </dependency>
  1. <!--slf4j 自带的简单日志实现 -->
  2. <dependency>
  3. <groupId>org.slf4j</groupId>
  4. <artifactId>slf4j-simple</artifactId>
  5. <version>1.7.25</version>
  6. </dependency>

使用演示:

  1. Logger logger = LoggerFactory.getLogger(LogTest.class);
  2. logger.error("error");
  3. logger.warn("warn");
  4. logger.info("info");
  5. logger.debug("debug");
  6. logger.trace("trace");

占位符

  1. Logger logger = LoggerFactory.getLogger(LogTest.class);
  2. logger.info("info,{},{}",1,2);

异常打印

直接传入异常对象即可

集成其他日志框架

那么Slf4j是如何完成日志框架的动态选择的呢?—让我们来看看吧

  • 下面只会列举关键的代码
  1. public static Logger getLogger(String name) {
  2. //返回的LoggerFactory就已经决定了底层会采用哪种日志框架
  3. //因此我们需要追踪一下getILoggerFactory的实现
  4. ILoggerFactory iLoggerFactory = getILoggerFactory();
  5. return iLoggerFactory.getLogger(name);
  6. }
  1. public static ILoggerFactory getILoggerFactory() {
  2. //不重复进行初始化
  3. if (INITIALIZATION_STATE == UNINITIALIZED) {
  4. synchronized (LoggerFactory.class) {
  5. if (INITIALIZATION_STATE == UNINITIALIZED) {
  6. INITIALIZATION_STATE = ONGOING_INITIALIZATION;
  7. //真正选择的逻辑在这里实现
  8. performInitialization();
  9. }
  10. }
  11. }
  12. switch (INITIALIZATION_STATE) {
  13. //初始化成功
  14. case SUCCESSFUL_INITIALIZATION:
  15. return StaticLoggerBinder.getSingleton().getLoggerFactory();
  16. //没有引入任何日志框架的依赖--那么使用NOPLoggerFactory--即啥也不干的日记记录器
  17. case NOP_FALLBACK_INITIALIZATION:
  18. return NOP_FALLBACK_FACTORY;
  19. //初始化失败--抛出异常
  20. case FAILED_INITIALIZATION:
  21. throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
  22. case ONGOING_INITIALIZATION:
  23. // support re-entrant behavior.
  24. // See also http://jira.qos.ch/browse/SLF4J-97
  25. return SUBST_FACTORY;
  26. }
  27. throw new IllegalStateException("Unreachable code");
  28. }
  1. private final static void performInitialization() {
  2. //绑定操作--真正去寻找日志框架依赖的核心逻辑实现
  3. bind();
  4. if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
  5. versionSanityCheck();
  6. }
  7. }
  1. private final static void bind() {
  2. try {
  3. //存放找到日志框架实现的依赖
  4. Set<URL> staticLoggerBinderPathSet = null;
  5. // skip check under android, see also
  6. // http://jira.qos.ch/browse/SLF4J-328
  7. if (!isAndroid()) {
  8. //寻找可用的StaticLoggerBinder--为啥要寻找他,后面会讲
  9. staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
  10. //如果同时引入了多个日志框架依赖,这里会进行日志记录
  11. reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
  12. }
  13. ...

findPossibleStaticLoggerBinderPathSet是真正去查找的逻辑:

  1. private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
  1. static Set<URL> findPossibleStaticLoggerBinderPathSet() {
  2. Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
  3. try {
  4. ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
  5. Enumeration<URL> paths;
  6. if (loggerFactoryClassLoader == null) {
  7. paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
  8. } else {
  9. //去类路径下寻找所有org/slf4j/impl/StaticLoggerBinder.class
  10. paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
  11. }
  12. //将所有定位到的日志框架依赖加入staticLoggerBinderPathSet
  13. while (paths.hasMoreElements()) {
  14. URL path = paths.nextElement();
  15. staticLoggerBinderPathSet.add(path);
  16. }
  17. } catch (IOException ioe) {
  18. Util.report("Error getting resources from path", ioe);
  19. }
  20. return staticLoggerBinderPathSet;
  21. }

为什么通过去类路径下寻找所有的org/slf4j/impl/StaticLoggerBinder.class,就可以找到引入的所有日志框架依赖呢?

因为slf4j-simple和logback因为遵循了slf4j规范,都存在该静态日志记录绑定器,因此我们可以通过去类路径下搜索该类,来获取到所有依赖包,至于jcl和logback,需要因为桥接模块才能完成,下面会讲

  1. //如果同时引入多个日志依赖,那么这里会进行记录
  2. private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
  3. if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
  4. Util.report("Class path contains multiple SLF4J bindings.");
  5. for (URL path : binderPathSet) {
  6. Util.report("Found binding in [" + path + "]");
  7. }
  8. Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
  9. }
  10. }

继续回到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实现,即禁止任何日志输出

  1. <dependency>
  2. <groupId>org.slf4j</groupId>
  3. <artifactId>slf4j-nop</artifactId>
  4. <version>1.7.36</version>
  5. </dependency>

同时引入三个实现,但是按照依赖引入顺序,只有第一个会生效

集成Log4j

前面说过,logback,simple,和nop都是在SLF4J之后出来的,都遵循器规范API,因此不需要适配器,引入依赖直接可以使用,但是对于log4j和logging来说,因为其出现时间早于slf4j,因此需要通过适配器模块完成适配才可以使用

即,如果我们想要在Slf4j中无缝使用log4j和logging,需要引入适配器模块依赖才可以

  1. <dependency>
  2. <groupId>org.slf4j</groupId>
  3. <artifactId>slf4j-log4j12</artifactId>
  4. <version>1.7.25</version>
  5. </dependency>

因为适配器模块里面已经包含了log4j和slf4j-api的依赖,因此我们只需要一个适配器模块依赖就可以了

门面,适配器,日志框架本身依赖

这个时候,只需要把log4j相关配置文件拿过来即可:

  1. log4j.rootLogger=info,console
  2. log4j.appender.console=org.apache.log4j.ConsoleAppender
  3. log4j.appender.console.layout=org.apache.log4j.PatternLayout
  4. log4j.appender.console.layout.conversionPattern=[%-8p] %r %c %t %d{yyyy-MM-dd HH:mm:ss::SSS} %m%n

原理如下:

集成JDK14做JUL适配器

  1. <dependency>
  2. <groupId>org.slf4j</groupId>
  3. <artifactId>slf4j-jdk14</artifactId>
  4. <version>1.7.36</version>
  5. </dependency>

同样只需要导入一个依赖即可,因为slf4j-api已经帮我们导入好了,而JUL是java内置的,因此不需要导入

通过桥接模块解决项目日志重构

上面都是通过适配器模式完成的日志适配,但是下面我给出一个需求,大家思考一下该怎么办?

  • 有一个老项目,日志使用log4j完成记录,但是此时领导要求将日志框架全部更换为slf4j+logback的组合
  • 请你在不改动原有日志代码的基础上,完成架构更迭

这个时候就需要使用桥接模块,进行伪装,完成架构替换

其余几个原理类似,我们先来看看具体操作过程,然后再来分析原理:

  • 移除log4j的依赖

开始爆红了

  • 添加桥接器模块和logback的依赖
  1. <dependency>
  2. <groupId>org.slf4j</groupId>
  3. <artifactId>log4j-over-slf4j</artifactId>
  4. <version>1.7.36</version>
  5. </dependency>
  1. <dependency>
  2. <groupId>ch.qos.logback</groupId>
  3. <artifactId>logback-classic</artifactId>
  4. <version>1.2.11</version>
  5. </dependency>
  • 测试

源码分析桥接过程

首先我们可以看到,这个所谓的桥接器,只引入了一个slf4j-api的门面依赖,猜测是模拟了log4j的包路径,然后将api最终重定向到了slf4j中,下面我们看看是怎么完成api的重定向的

  1. //桥接逻辑在getLogger方法中完成,我们来追踪进去看看
  2. Logger logger = Logger.getLogger(LogTest.class.getName());
  1. public static Logger getLogger(String name) {
  2. //继续看
  3. return Log4jLoggerFactory.getLogger(name);
  4. }
  1. public static Logger getLogger(String name) {
  2. Logger instance = (Logger)log4jLoggers.get(name);
  3. if (instance != null) {
  4. return instance;
  5. } else {
  6. //这里还是先查缓存,然后将结果再放入缓存
  7. //但是我们想知道的是具体狸猫换太子的把戏是在哪里完成的
  8. //其实就是在这个logger的构造函数中完成的
  9. Logger newInstance = new Logger(name);
  10. Logger oldInstance = (Logger)log4jLoggers.putIfAbsent(name, newInstance);
  11. return oldInstance == null ? newInstance : oldInstance;
  12. }
  13. }

Logger构造函数

  1. //最终是调用父类Category的构造函数
  2. Category(String name) {
  3. this.name = name;
  4. //LoggerFactory创建的就是slf4j的门面Logger
  5. this.slf4jLogger = LoggerFactory.getLogger(name);
  6. if (this.slf4jLogger instanceof LocationAwareLogger) {
  7. this.locationAwareLogger = (LocationAwareLogger)this.slf4jLogger;
  8. }
  9. }

这里相当于在原本的log4j的Category中增加两个对slf4j的Logger的引用

然后我们再来看看输出日志的时候,做了怎样的桥接工作

  1. //在该桥接模块中,所有日志级别的输出,都会委托该方法完成
  2. void differentiatedLog(Marker marker, String fqcn, int level, Object message, Throwable t) {
  3. String m = convertToString(message);
  4. //locationAwareLogger和slf4jLogger引用是相同的,因此最终还是交给了slf4j完成的日志输出
  5. if (locationAwareLogger != null) {
  6. locationAwareLogger.log(marker, fqcn, level, m, null, t);
  7. } else {
  8. //这里就是直接交给了slf4j的Logger进行日志输出
  9. switch (level) {
  10. case LocationAwareLogger.TRACE_INT:
  11. slf4jLogger.trace(marker, m);
  12. break;
  13. case LocationAwareLogger.DEBUG_INT:
  14. slf4jLogger.debug(marker, m);
  15. break;
  16. case LocationAwareLogger.INFO_INT:
  17. slf4jLogger.info(marker, m);
  18. break;
  19. case LocationAwareLogger.WARN_INT:
  20. slf4jLogger.warn(marker, m);
  21. break;
  22. case LocationAwareLogger.ERROR_INT:
  23. slf4jLogger.error(marker, m);
  24. break;
  25. }
  26. }
  27. }

相关文章