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

2022-05-16 08:53:13 浏览数 (1)

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是如何完成日志框架的动态选择的呢?—让我们来看看吧

  • 下面只会列举关键的代码
代码语言:javascript复制
    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的依赖
代码语言:javascript复制
        <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;
            }
        }
    }

0 人点赞