日志-slf4j桥接原理分析

背景

日志是进行软件开发必不可少的一项功能,也存在很多开源日志库可供使用,如slf4j,log4j,log4j2,logback,这些开源日志库直接有什么联系呢?我们应该如何使用它们呢?

现有日志体系

此部分内容来源于log4j-over-slf4j与slf4j-log4j12共存stack overflow异常分析
这里写图片描述
上图不是非常精准,但是能够比较清晰地展示现有Java日志体系的主体架构.Java日志体系大体可以分为三个部分:日志门面接口,适配器,日志框架具体实现.

  1. 日志框架具体实现:Java日志框架有很多种,最简单的是Java自带的java.util.logging,而最经典的是log4j,后来又出现了一个比log4j性能更好的logback,应用程序可以直接使用这些具体日志框架的API来满足日志输出需求,但是由于各个日志框架之间的API通常是不兼容的,这样做就使得应用程序丧失了更换日志框架的灵活性。
  2. 日志门面:比直接使用具体日志框架API更合理的选择是使用日志门面接口.日志门面接口提供了一套独立于具体日志框架实现的API,应用程序通过使用这些独立的API就能够实现与具体日志框架的解耦,如slf4j。
  3. 适配器:日志门面接口本身通常并没有实际的日志输出能力,它底层还是需要去调用具体的日志框架API的,也就是实际上它需要跟具体的日志框架结合使用。由于具体日志框架比较多,而且互相也大都不兼容,日志门面接口要想实现与任意日志框架结合可能需要对应的s器,说白了,所谓“桥接器”,不过就是对某套API的伪实现。这种实现并不是直接去完成API所声明的功能,而是去调用有类似功能的别的API。这样就完成了从“某套API”到“别的API”的转调

需要注意的是,前面说过,上图并不精准,这只是主要部分,实际情况并不总是简单的“日志门面接口–>桥接器–>日志框架”这一条单向线。实际上,独立的桥接器有时候是不需要的,而且也并不是只有将日志门面API转调到具体日志框架API的桥接器,也存在将日志框架API转调到日志门面API的桥接器。

桥接器实现原理分析

环境

此小节介绍桥接器的实现原理,我们以slf4j+log4j为例.
日志门面为slf4j,maven如下:

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.25</version>
    </dependency>

注:slf4j-api的1.7.X版本与1.8.X版本获取ILoggerFactory的方式不同,桥接器实现也不同,我们以1.7.X版本为例,下桥接器版本必须是1.7.25,与此一致.
日志框架具体实现为log4j,maven如下:

    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>

桥接器使用slf4j->log4j桥接器,maven如下啊:

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.25</version>
    </dependency>

分析

注:在下文代码注释中,会表明此方法是日志门面还是桥接器的代码
我们从最简单的获取logger开始

//Logger和LoggerFactory都是日志门面jar包中的类
private static Logger logger = LoggerFactory.getLogger(Main.class);

继续看源码:

    //日志门面
    public static Logger getLogger(String name) {
        ILoggerFactory iLoggerFactory = getILoggerFactory();
        return iLoggerFactory.getLogger(name);
    }

获取ILoggerFactory

重点在getILoggerFactory(),此过程又分为3个步骤:

第一个步骤加载org/slf4j/impl/StaticLoggerBinder.class文件
    /**
      * 日志门面
      * 通过ClassLoader加载"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 {
                paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }
            while (paths.hasMoreElements()) {
                URL path = paths.nextElement();
                staticLoggerBinderPathSet.add(path);
            }
        } catch (IOException ioe) {
            Util.report("Error getting resources from path", ioe);
        }
        return staticLoggerBinderPathSet;
    }

注意:虽然StaticLoggerBinder是在org.slf4j.impl包下,但日志门面jar包中并无此类,此类是由桥接器实现的.比如在我的电脑上,URL就是file:/E:/mavenRepo/org/slf4j/slf4j-log4j12/1.7.25/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class,可以看出,其是在slf4j-log4j12-1.7.25.jar中.
桥接器的原理其实就是在其jar包中存在StaticLoggerBinder.class,这样就可在运行时动态绑定底层的日志实现框架.

第二个步骤选取一个StaticLoggerBinder.class来创建一个单例

当项目中有多个桥接器时,其会找到多个StaticLoggerBinder.class,比如我们增加一个slf4j和log4j2的桥接器,其maven如下:

   <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.9.1</version>
    </dependency>

这时就会检测到两个StaticLoggerBinder.class的文件路径,代码如下:

    /**
     * 日志门面
     * 若在class path中找到多个绑定类,则打印警告信息
     */
    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.");
        }
    }

此时slf4j会打印警告信息:

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/E:/mavenRepo/org/slf4j/slf4j-log4j12/1.7.25/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/E:/mavenRepo/org/apache/logging/log4j/log4j-slf4j-impl/2.9.1/log4j-slf4j-impl-2.9.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.

此时代码继续:

    private final static void bind() {
            ...
            StaticLoggerBinder.getSingleton();
            ...
    }

由于存在多个org.slf4j.impl.StaticLogerBinder,这时会调用哪一个类的代码呢?
经过实践发现,其会调用在class path中较早出现的org.slf4j.impl.StaticLogerBinder的代码.如在maven中,会调用在pom.xml中定义较靠前的桥接器的代码.

第三个步骤返回一个ILoggerFactory实例
return StaticLoggerBinder.getSingleton().getLoggerFactory();

返回桥接器中指定的ILoggerFactory,在此例中就是org.slf4j.impl.Log4jLoggerFactory,其在桥接器中定义.

获取Logger

    /**
      * 桥接器
      */
    public Logger getLogger(String name) {
        Logger slf4jLogger = loggerMap.get(name);
        if (slf4jLogger != null) {
            return slf4jLogger;
        } else {
            org.apache.log4j.Logger log4jLogger;
            //调用日志框架实现生成"org.apache.log4j.Logger "
            if (name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME))
                log4jLogger = LogManager.getRootLogger();
            else
                log4jLogger = LogManager.getLogger(name);
			//使用适配器包装"org.apache.log4j.Logger "
            Logger newInstance = new Log4jLoggerAdapter(log4jLogger);
            Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
            return oldInstance == null ? newInstance : oldInstance;
        }
    }

桥接器会调用的日志框架实现的相关代码生成其内部的Logger(此Logger与org.slf4j.Logger)不兼容,再通过适配器包装日志框架实现内部的Logger.

总结

  1. 建议在应用中使用日志门面API打印日志,日后可以通过调整桥接器和日志框架实现方便的修改日志实现
  2. slf4j在获取日志框架实现时扫描class path,寻找org.slf4j.impl.StaticLoggerBinder,桥接器就是通过提供该类实现桥接功能

参考

  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值