SLF4J日志绑定原理分析

news2024/12/24 21:04:37

前言

最近做项目,实际上也知道日志冲突的事,不过涉及MDC NDC数据传递,当日志框架冲突后,MDC和NDC就失效了,这里就涉及slf4j-api的MDC的绑定的过程,顺便分析了日志冲突实际生效的原因,理解SLF4J的设计思想。官方网址:SLF4J

Simple Logging Facade for Java(SLF 4J)作为一个简单的facade或抽象,用于各种日志框架(例如logback、log4j2),从而允许最终用户在部署时插入所需的日志记录框架。 简称门面模式,就是接口外加绑定实现。

click to enlarge

 实际上slf4j-api原生支持的日志框架主要有2种logback和log4j2,其他JDK日志log4j1、commons-logging等都是桥接到slf4j-api的。

 桥接都是slf4j自己实现的,对应上面的架构图,MIT协议,所以商业用途可以修改源代码。

slf4j-api 1.x

直接code吧,源码说明问题,先使用slf4j-api 1.7.36版本为例

public class Main {

    private static final Logger LOGGER = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) {
        System.out.println("hello log");
        LOGGER.info("first slf4j-api logger!");
    }
}

很简单的代码,执行后

 

 出现红字,那么就来分析红字的来源,从getLogger开始,因为info这些方法,实际上是接口方法,关键还是创建的slf4j-api的Logger的实现

  // Support for detecting mismatched logger names.
    static final String DETECT_LOGGER_NAME_MISMATCH_PROPERTY = "slf4j.detectLoggerNameMismatch";
    static final String JAVA_VENDOR_PROPERTY = "java.vendor.url";

    //通过系统变量读取slf4j.detectLoggerNameMismatch
    static boolean DETECT_LOGGER_NAME_MISMATCH = Util.safeGetBooleanSystemProperty(DETECT_LOGGER_NAME_MISMATCH_PROPERTY);
    
    static final String CODES_PREFIX = "http://www.slf4j.org/codes.html";
    static final String LOGGER_NAME_MISMATCH_URL = CODES_PREFIX + "#loggerNameMismatch";

    public static Logger getLogger(Class<?> clazz) {
        Logger logger = getLogger(clazz.getName()); //关键代码,获取Logger对象,class默认是类全面,也可以自定义字符串,那就是另外的API
        if (DETECT_LOGGER_NAME_MISMATCH) {
            //通过调用关系取class,securityManager.getClassContext();
            //定义slf4j.detectLoggerNameMismatch为true才会生效,默认布尔为false
            Class<?> autoComputedCallingClass = Util.getCallingClass();
            if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
                //这里report就是system.err就是控制台红字
                Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
                                autoComputedCallingClass.getName()));
                Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
            }
        }
        return logger;
    }

然后继续看getLogger

    public static Logger getLogger(String name) {
        ILoggerFactory iLoggerFactory = getILoggerFactory();
        return iLoggerFactory.getLogger(name);
    }

没什么讲的,过度代码,获取factory接口实现,然后获取logger实现,ILoggerFactory实际上也是需要logback或者log4j2自己需要实现的,实现的来源于StaticLoggerBinder

    public static ILoggerFactory getILoggerFactory() {
        //状态机制
        if (INITIALIZATION_STATE == UNINITIALIZED) {
            //未初始化,同步初始化
            synchronized (LoggerFactory.class) {
                if (INITIALIZATION_STATE == UNINITIALIZED) {
                    INITIALIZATION_STATE = ONGOING_INITIALIZATION; //更新状态
                    performInitialization(); //关键逻辑,初始化factory实现
                }
            }
        }
        switch (INITIALIZATION_STATE) {
        case SUCCESSFUL_INITIALIZATION: //成功
            //注意这个StaticLoggerBinder在slf4j-api是没有定义的,绑定器单例获取factory,这里很可能NoClassDefFound抛异常
            return StaticLoggerBinder.getSingleton().getLoggerFactory(); 
        case NOP_FALLBACK_INITIALIZATION:
            return NOP_FALLBACK_FACTORY; //失败,但有默认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; //重入行为,再次创建factory
        }
        throw new IllegalStateException("Unreachable code");
    }

源码可以看到这个类飘红了,jar包里面是没有这个类的,但是类不存在编译不过,猜测是slf4j-api有依赖类,打包剔除的,等源码分析后看github看看

关键还是performInitialization

    private final static void performInitialization() {
        bind();
        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
            versionSanityCheck();//成功初始化检查版本号
        }
    }

看看检查版本号,推测slf4j-api的1.6和1.7是兼容的,但是不兼容slf4j-api 2.x

 

关键还是bind方法

    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()) {//判断系统变量java.vendor.url是否包含android字符串
                //查找可能的StaticLoggerBinder
                staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet(); 
                //如果找到多个就打印在控制台
                reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
            }
            // the next line does the binding
            // 见上面,StaticLoggerBinder没有实现发现就会报错NoClassDefFound
            StaticLoggerBinder.getSingleton();
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION; //状态更新
            reportActualBinding(staticLoggerBinderPathSet); //打印实际绑定的binder
        } catch (NoClassDefFoundError ncde) { //就是处理刚刚说的异常
            String msg = ncde.getMessage();// 下面的信息就是demo在控制台打印的NOP
            if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
                // no-operation (NOP) logger implementation
                INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
                Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
                Util.report("Defaulting to no-operation (NOP) logger implementation");
                Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
            } else {
                failedBinding(ncde);
                throw ncde;
            }
        } catch (java.lang.NoSuchMethodError nsme) {
            String msg = nsme.getMessage(); 
            //很明显就是1.6和1.7不兼容1.5的情况,方法变了
            if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
                INITIALIZATION_STATE = FAILED_INITIALIZATION;
                Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
                Util.report("Your binding is version 1.5.5 or earlier.");
                Util.report("Upgrade your binding to version 1.6.x.");
            }
            throw nsme;
        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        } finally {
            postBindCleanUp();//替代日志factory,报告事件,实际上就是控制台打印
        }
    }

查找逻辑也很简单,关键在于类定义和查找,实际上Spring也经常有这种逻辑处理,类只有类名,只定义标准

 

如果有多个就打印出来

    private static boolean isAmbiguousStaticLoggerBinderPathSet(Set<URL> binderPathSet) {
        return binderPathSet.size() > 1;
    }

    /**
     * Prints a warning message on the console if multiple bindings were found
     * on the class path. No reporting is done otherwise.
     * 
     */
    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.");
        }
    }

实际使用那个Binder

 

MDC同理

 

 同时石锤slf4j-api开发过程中有实现Binder

打包去掉类,或许我们也可以这样,在特定时刻,方便框架开发,业务实现一般不会这样用。

 

slf4j-api 2.x

为什么会单独介绍2.x版本,架构变了,笔者在查看github,发现slf4j-api的master即2.x的最新代码,真的使用SPI了,同时意味着刚刚的Binder实现类在源码移除了

截止现在slf4j-api最新版是2.0.7,查看源码

   public static ILoggerFactory getILoggerFactory() {
        return getProvider().getLoggerFactory();
    }

    static SLF4JServiceProvider getProvider() {
        if (INITIALIZATION_STATE == 0) {
            Class var0 = LoggerFactory.class;
            synchronized(LoggerFactory.class) {
                if (INITIALIZATION_STATE == 0) {
                    INITIALIZATION_STATE = 1;
                    performInitialization();
                }
            }
        }

        switch (INITIALIZATION_STATE) {
            case 1:
                return SUBST_PROVIDER;
            case 2:
                throw new IllegalStateException("org.slf4j.LoggerFactory in failed state. Original exception was thrown EARLIER. See also https://www.slf4j.org/codes.html#unsuccessfulInit");
            case 3:
                return PROVIDER;
            case 4:
                return NOP_FALLBACK_SERVICE_PROVIDER;
            default:
                throw new IllegalStateException("Unreachable code");
        }
    }

实际上根1.x是一样的,方法名称变了,具体是否使用SPI,还要看performInitialization

    private final static void bind() {
        try {
            // 找到SLF4JServiceProvider
            List<SLF4JServiceProvider> providersList = findServiceProviders();
            reportMultipleBindingAmbiguity(providersList); // 多个打印
            if (providersList != null && !providersList.isEmpty()) { 
                // 多个实现,使用SPI的第一个
                PROVIDER = providersList.get(0);
                // SLF4JServiceProvider.initialize() is intended to be called here and nowhere else.
                PROVIDER.initialize(); //初始化,因为判空的缘故,所以正常情况不会出现类未定义的异常
                INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
                reportActualBinding(providersList); //实际绑定
            } else {
                // 没有的时候,就是1.x控制台不引入任何日志jar的内容,提示有变化
                INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
                Util.report("No SLF4J providers were found.");
                Util.report("Defaulting to no-operation (NOP) logger implementation");
                Util.report("See " + NO_PROVIDERS_URL + " for further details.");

                //老式查找,因为SPI了,所以忽略加载了
                Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                reportIgnoredStaticLoggerBinders(staticLoggerBinderPathSet);
            }
            postBindCleanUp();//替代factory,reportEvent
        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        }
    }

Java SPI加载逻辑,加载后取第一个生效

 

MDC同理,实际上看1.x的源码,还有一个Marker的接口也是同样的道理

 

没有任何日志jar的打印

 

NOP模式 

 logback实现

因为logback和log4j2实现都差不多,所以以logback为例,因为Springboot内置

以slf4j-api 2.0.7和logback 1.4.8为例

先看logback classic的SPI配置

 文件内容就是实现类

ch.qos.logback.classic.spi.LogbackServiceProvider

先看initialize方法,在slf4j-api的bind方法中会被调用

    public void initialize() {
        defaultLoggerContext = new LoggerContext();
        defaultLoggerContext.setName(CoreConstants.DEFAULT_CONTEXT_NAME);
        initializeLoggerContext();
        defaultLoggerContext.start();
        markerFactory = new BasicMarkerFactory();
        mdcAdapter = new LogbackMDCAdapter();
        // set the MDCAdapter for the defaultLoggerContext immediately
        defaultLoggerContext.setMDCAdapter(mdcAdapter);
    }

初始化日志套件,关键是LoggerContext,有Marker和MDC的工厂或者适配器的创建,这2个也是日志的SPI机制附加的

public static ILoggerFactory getILoggerFactory() {
    return getProvider().getLoggerFactory();
}

logback的工厂就是initialize的defaultLoggerContext

然后创建logger,这个实际上就是层级创建,因为这个设计,所以logger天然适合MDC传输数据,比如线程id

 

log4j2实际上也差不多,据测试log4j2的性能比logback强一些,笔者没测试过,实际应用也不会特别考虑这个原因,因为Boot集成了,而且logback性能也不差。另外logback和log4j2实际上不同配置也会造成性能差异,比如是否阻塞和是否丢日志的抉择等,可以查询官方,适配详细参数。

 

日志冲突分析

日志冲突跟slf4j-api加载多个实现或者桥接器有关,表现为加载多个Binder或者SPI实现,但是实际加载的不是我们想要的,如果加载log4j1这样的,很可能slf4j-api的System.err都不会打印,只能删掉除了slf4j-api的其他所有日志jar,一个一个排查,如果fatjar压入了日志jar的源码,那么只能class扫描才行。

日志冲突跟slf4j-api加载的过程有关:

  1. 1.6、1.7的版本是类实际加载的时间有关,那么这个就跟Maven依赖顺序,打Spring Boot包是否排序有关
  2. 2.x的版本跟Java SPI的配置文件加载时间有关,也是跟jar的加载顺序有关

最长见的是外置Tomcat,非embed tomcat,tomcat7会对lib目录排序,tomcat8的lib目录就是解压后的操作系统决定顺序,导致不同机器运行后日志冲突不一致,解决办法也简单,外置tomcat也支持不解压运行,可以看tomcat源码,或者使用Spring boot这样内嵌tomcat,打包就把顺序固定。

笔者故意冲突

实际就会打印

 

如果我调换依赖顺序

 

因为我没有log4j的实现,所以日志是打印不出来的。 

总结

实际上这些原理一般不需要了解也可以正常打印日志,现在开发都考虑到各种问题了,所以有了Spring Boot这种一体jar,包打好了就固定了,不过出问题需要会查问题即可。另外吐槽golang的日志框架,真的五花八门,一般用的Uber-go的zap包,但是K8S docker又是另外的,实际上有接口注入还好,可以自定义,如果没提供接口注入就彻底不能动了,只能在框架重新实现,就是没有slf4j-api的缘故,门面模式实际上在框架和中间件等开发还是有很关键的作用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/868677.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaScript类

JavaScript 类(class) 类是用于创建对象的模板。 我们使用 class 关键字来创建一个类&#xff0c;类体在一对大括号 {} 中&#xff0c;我们可以在大括号 {} 中定义类成员的位置&#xff0c;如方法或构造函数。 每个类中包含了一个特殊的方法 constructor()&#xff0c;它是类…

395. 至少有 K 个重复字符的最长子串

395. 至少有 K 个重复字符的最长子串 C代码&#xff1a;滑动窗口 ---- 不是吧&#xff0c;阿sir&#xff0c;这也能滑&#xff1f; // 返回滑动窗口的长度 // 满足条件的种类数量的可能为 [1, 26], 所以需要遍历26中情况的窗口长度 // 当 区间内所有种类数量 满足要求的种类数…

【Anaconda】安装及使用

知识目录 前言一、 Anaconda是什么二、为什么使用Anaconda三、安装步骤3.1 下载安装3.2 配置conda源 结语 前言 大家好&#xff01;我是向阳花花花花&#xff0c;本期给大家带来的是 Anaconda 安装及使用。 每日金句分享&#xff1a;故事不长&#xff0c;也不难讲。』—— 「…

深度学习(37)—— 图神经网络GNN(2)

深度学习&#xff08;37&#xff09;—— 图神经网络GNN&#xff08;2&#xff09; 这一期主要是一些简单示例&#xff0c;针对不同的情况&#xff0c;使用的数据都是torch_geometric的内置数据集 文章目录 深度学习&#xff08;37&#xff09;—— 图神经网络GNN&#xff08…

SpringCloudGateway配置跨域设置以及如何本地测试跨域

问题背景 有个服务A &#xff0c;自身对外提供服务&#xff0c;几个系统的前端页面也在调用&#xff0c;使用springboot 2.6.8开发的&#xff0c;自身因为有前端直接调用已经配置了跨域。 现在有网关服务&#xff0c;一部分前端通过网关访问服务A&#xff08;因为之前没有网关…

Android系统-ServiceManager2

目录 引言&#xff1a; 获取ServiceManager 流程图 注册系统服务 获取系统服务 引言&#xff1a; 注册或使用服务之前&#xff0c;需要通过ServiceManager这个DNS来找到对应的服务。那怎么找到ServiceManager呢&#xff1f; 怎么注册系统服务&#xff1f; 怎么获取系统…

分享一组天气组件

先看效果&#xff1a; CSS部分代码&#xff08;查看更多&#xff09;&#xff1a; <style>:root {--bg-color: #E9F5FA;--day-text-color: #4DB0D3;/* 多云 */--cloudy-background: #4DB0D3;--cloudy-temperature: #E6DF95;--cloudy-content: #D3EBF4;/* 晴 */--sunny-b…

Docker高级篇_DockerFile

目录 DockerFile简介构建DockerFile构建过程解析Docker执行Dockerfile的大致流程 DockerFile常用保留字指令FROMMAINTAINERRUNEXPOSEWORKDIRUSERENVVOLUMEADDCOPYCMDENTRYPOINT案例使用虚悬镜像 Docker微服务 DockerFile简介 Dockerfile是用来构建Docker镜像的文本文件&#x…

七、解析应用程序——枚举内容与功能

文章目录 1、web抓取2、发现隐藏内容2.1 蛮力技巧2.2 通过公布的内容进行推测2.3 利用公共信息 3、应用程序页面和功能路径4、发现隐藏参数 攻击应用程序的第一步是收集和分析与其有关的一些关键信息&#xff0c;以清楚了解攻击目标。解析过程首先是枚举应用程序的内容与功能&a…

HTML详解连载(2)

HTML详解连载&#xff08;2&#xff09; 专栏链接 [link](http://t.csdn.cn/xF0H3)下面进行专栏介绍 开始喽超链接作用代码示例解释经验分享 音频标签代码示例注意强调 视频标签代码示例注意强调 列表作用&#xff1a;布局内容排列整齐的区域。分类&#xff1a;无序列表&#x…

基于Googlenet深度学习网络的人员行为动作识别matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 1. 原理 1.1 深度学习与卷积神经网络&#xff08;CNN&#xff09; 1.2 GoogLeNet 2. 实现过程 2.1 数据预处理 2.2 构建网络模型 2.3 数据输入与训练 2.4 模型评估与调优 3. 应用领域…

Eclipse(STS)Enum 格式化配置

需求 枚举类 的字段&#xff0c;一般长度较小&#xff0c;格式化后&#xff0c;多个字段会变为一行。 需求&#xff1a;希望每个字段独立一行。 格式化后的样式&#xff0c;如下图所示&#xff1a; 配置 1. Windows - Preferences 2. Java-Formatter 如果没有自定义的格…

C++ 循环

有的时候&#xff0c;可能需要多次执行同一块代码。一般情况下&#xff0c;语句是顺序执行的&#xff1a;函数中的第一个语句先执行&#xff0c;接着是第二个语句&#xff0c;依此类推。 编程语言提供了允许更为复杂的执行路径的多种控制结构。 循环语句允许我们多次执行一个…

DVWA暴力破解高级模式宏爆破

先将安全等级调至高级&#xff0c;点击submit提交 浏览器开启bp代理 kali开启bp 工具&#xff0c;开启Proxy 点击Brute Force这个选项卡 bp拦截到请求的数据包 宏设置 如果是有的bp版本比较旧&#xff0c;在旧版本的上面菜单栏有一个Project options点击去选择Session&#xff…

Android系统-进程-Binder1-概述

目录 引言&#xff1a; 一次拷贝 Binder一次通信过程 应用启动中的binder交互 C/S模式&#xff1a; Binder COMMAND BWR数据结构&#xff1a; 引言&#xff1a; Android的binder是从openbinder发展过来的。包括了binder驱动&#xff0c;Native层&#xff0c;Java层。用…

【Matlab智能算法】RBF神经网络-遗传算法(RBF-GA)函数极值寻优——非线性函数求极值

上一篇博客介绍了GRNN-GA&#xff1a;GRNN神经网络遗传算法(GRNN-GA)函数极值寻优——非线性函数求极值&#xff0c;神经网络用的是GRNN神经网络&#xff0c;RBF神经网络&#xff08;径向基函数神经网络&#xff09;和GRNN神经网络有相似之处。本篇博客将GRNN神经网络替换成RBF…

安装istio和部署实例以及仪表盘

安装Istio 接下来我们将介绍如何在 Kubernetes 集群中安装 Istio&#xff0c;这里我们使用的是最新的 1.10.3 版本。 下面的命令可以下载指定的 1.10.3 版本的 Istio: ➜ ~ curl -L https://istio.io/downloadIstio | ISTIO_VERSION1.10.3 sh -如果安装失败&#xff0c;可以…

SolidWorks不能使用选择如允许此选择将生成有冲突的前后关系

SolidWorks不能使用选择如允许此选择将生成有冲突的前后关系 1 SolidWorks不能使用选择如允许此选择将生成有冲突的前后关系 1 SolidWorks不能使用选择如允许此选择将生成有冲突的前后关系 https://www.swrjzxw.com/1556.html SolidWorks装配体时 显示 不能使用选择如允许此选…

Linux 使用gdb调试C程序

一、gdb的一些基础命令 l&#xff1a;显示代码 l n&#xff1a;跳转到当前代码页的第n行的代码 l filename.c &#xff1a;n&#xff1a;跳转到filename.c文件的第n行代码 b 行号&#xff1a;加断点 info break&#xff1a;查看断点信息 delete 断点编号&#xff1a;删除断点 …

力扣 518. 零钱兑换 II

题目来源&#xff1a;https://leetcode.cn/problems/coin-change-ii/description/ C题解&#xff08;来源代码随想录&#xff09;&#xff1a; 这是一道典型的背包问题&#xff0c;一看到钱币数量不限&#xff0c;就知道这是一个完全背包。但本题和纯完全背包不一样&#xff0c…