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示例)

Java日志框架 -- JCL日志门面(JCL概念介绍JCL示例)

日志门面框架Slf4j

设计模式_门面模式

一篇文章带你搞定 Java 日志框架 slf4j

Java日志框架:slf4j作用及其实现原理