彻底解决SLF4J的日志冲突的问题

2023-11-07

今天公司同事上线时发现,有的机器打印了日志,而有的机器则一条日志也没有打。以往都是没有问题的。

因此猜测是这次开发间接引入新的日志jar包,日志冲突导致未打印。

排查代码发现,系统使用的是SLF4J框架打印log4j2的日志。查看系统中引入的jar包发现果然有多个SLF4J的桥接包。于是排掉冲突jar包,然后上线时所有机器都正常打印日志


先上一张关系图:SLF4J框架、各种具体日志实现以及相应桥接包的关系图

在这里插入图片描述

一、起因

由于线上系统要接入很多中间件,因此系统中会有各种各样的日志打印形式(例如:log4j2、JCL、logback等等)。

为了能整合所有日志并进行统一打印,最常用的就是SLF4J框架。

SLF4J框架作为门面框架,并没有日志的具体实现。而是通过和其他具体日志实现进行关联转换,并在系统中配置一种日志实现进行打印。

于是就很容易造成jar包引入冲突,导致有多个日志实现。当SLF4J框架选择的日志实现和我们配置的不一致时,就会打印不出日志。

SLF4J框架发现有多个日志实现时,是会打印提示信息的。但由于是标准错误输出,会在控制台(Tomcat的catalina.out)中打印【当业务日志文件中没有日志打印时,可以查看catalina.out是否有提示
在这里插入图片描述在这里插入图片描述在这里插入图片描述

二、为什么只有部分机器打印

因为每个SLF4J的桥接包都有org.slf4j.impl.StaticLoggerBinder

SLF4J则会随机选择一个使用。当选择的跟系统配置的一样时就可以打印日志,否则就打印不出。

图片


三、快速感知到多种SLF4J桥接包

如上图所示findPossibleStaticLoggerBinderPathSet方法,当有多个日志桥接包时会返回一个Set集合且提示一条信息。

由于这个信息提示并不强烈,不易感知。我们可以根据这一点,使用反射来获取到系统中实际的桥接包数量,并做自定义的提示

1、实现spring的BeanFactoryPostProcessor,并将其交由spring管理。保证系统启动后,自动进行日志冲突校验

2、使用反射获取LoggerFactory的实例以及findPossibleStaticLoggerBinderPathSet方法的返回结果

3、根据桥接包数量判断是否异常,进行自定义报警

4、根据报警信息,进行排包

<bean class="LogJarConflictCheck" />

/**
 * 日志jar包冲突校验
 */
public class LogJarConflictCheck implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        try {
            Class<LoggerFactory> loggerFactoryClazz = LoggerFactory.class;
            Constructor<LoggerFactory> constructor = loggerFactoryClazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            LoggerFactory instance = constructor.newInstance();
            Method method = loggerFactoryClazz.getDeclaredMethod("findPossibleStaticLoggerBinderPathSet");
            // 强制进入
            method.setAccessible(true);
            Set<URL> staticLoggerBinderPathSet = (Set<URL>)method.invoke(instance);
            if (CollectionUtils.isEmpty(staticLoggerBinderPathSet)) {
                handleLogJarConflict(staticLoggerBinderPathSet, "Class path is Empty.添加对应日志jar包");
            }
            if (staticLoggerBinderPathSet.size() == 1) {
                return;
            }
            handleLogJarConflict(staticLoggerBinderPathSet, "Class path contains multiple SLF4J bindings. 注意排包");
        } catch (Throwable t) {
            t.getStackTrace();
        }
    }
    /**
     * 日志jar包冲突报警
     * @param staticLoggerBinderPathSet jar包路径
     * @param tip 提示语
     */
    private void handleLogJarConflict (Set<URL> staticLoggerBinderPathSet, String tip) {
        String ip = getLocalHostIp();
        StringBuilder detail = new StringBuilder();
        detail.append("ip为").append(ip).append("; 提示语为").append(tip);
        if (CollectionUtils.isNotEmpty(staticLoggerBinderPathSet)) {
            String path = JsonUtils.toJson(staticLoggerBinderPathSet);
            detail.append("; 重复的包路径分别为 ").append(path);
        }
        String logDetail = detail.toString();
        
        //TODO 使用自定义报警通知logDetail信息
    }

    private String getLocalHostIp() {
        String ip;
        try {
            InetAddress addr = InetAddress.getLocalHost();
            ip = addr.getHostAddress();
        } catch (Exception var2) {
            ip = "";
        }
        return ip;
    }

}

四、一次配置,终生可靠

上面的方式也只是帮助我们快速感知到日志jar包冲突,仍需手动排包。

是否存在一种解决方法,能帮忙我们彻底解决这种问题呢?

答案是有
即将我们需要引入的jar包和需要排掉的jar包声明到maven的最上层,将需要排掉的包声明为provided即可

这种方案是利用maven的扫包策略:

1、依赖最短路径优先原则;

2、依赖路径相同时,申明顺序优先原则

当我们将所有jar包声明为直接依赖后,会优先被使用。
而我们需要排掉的包只要声明为provided,就不会打入包中。
从而实现需要的包以我们声明的为准,需要排掉的包也不会被间接依赖影响

<properties>
  <slf4j.version>1.7.7</slf4j.version>
    <logback.version>1.2.3</logback.version>
    <log4j.version>1.2.17</log4j.version>
    <log4j2.version>2.3</log4j2.version>
    <jcl.version>1.2</jcl.version>
</properties>


<dependencies>
    <!--系统使用log4j2作为系统日志实现 slf4J作为门面 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${slf4j.version}</version>
    </dependency>

    <!--使用log4j2作为实际的日志实现-->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>${log4j2.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>${log4j2.version}</version>
    </dependency>

    <!--将log4j、logback、JCL的jar包设置为provided,不打入包中-->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>${log4j.version}</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>${logback.version}</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>${jcl.version}</version>
        <scope>provided</scope>
    </dependency>


    <!--为防止循环转换,排掉log4j2转slf4j的桥接包-->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-to-slf4j</artifactId>
        <version>${log4j2.version}</version>
        <scope>provided</scope>
    </dependency>

    <!--声明log4j、JCL、JUL转slf4j的桥接包,代码中对应日志可以转成SLF4J-->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>log4j-over-slf4j</artifactId>
        <version>${slf4j.version}</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <version>${slf4j.version}</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jul-to-slf4j</artifactId>
        <version>${slf4j.version}</version>
    </dependency>

    <!--声明slf4j转SLF4J的桥接包-->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>${log4j2.version}</version>
    </dependency>

    <!--排掉slf4j转log4j、JCL、JUL转slf4j的桥接包的桥接包,防止日志实现jar包冲突-->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>${slf4j.version}</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-jdk14</artifactId>
        <version>${slf4j.version}</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-jcl</artifactId>
        <version>${slf4j.version}</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

总结

第三步的方案:启动时感知系统是否存在日志jar包冲突,冲突后手动排包

第四步的方案:一次声明所需的所有日志jar包配置,无需在担心冲突问题

------The End------

如果这个办法对您有用,或者您希望持续关注,也可以扫描下方二维码或者在微信公众号中搜索【码路无涯】

在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

彻底解决SLF4J的日志冲突的问题 的相关文章

  • 在文件复制/上传未完成时读取文件内容

    例如 每 5 秒 服务器检查文件是否已添加到特定目录 如果是 它会读取并处理它们 相关文件可能非常大 例如 100 Mo 因此将它们复制 上传到上述目录可能会很长 如果服务器尝试访问尚未完成复制 上传的文件怎么办 JAVA是如何管理这些并发
  • Java排序列表

    我在java中得到了一个列表 我从 SQL 查询中获取值 public void ReloadPages throws Exception try Connection conn Framework GetDatabaseManager G
  • 程序无法访问扩展 JPanel 类的 PaintComponent() 方法

    这是JFrame package client connection import java awt Dimension import java io ObjectInputStream import java io ObjectOutpu
  • 返回音频文件类型列表

    在回答这个问题时 我想制作一个java程序 其中有一个组合框 显示文件夹中所有可用文件的标题 https stackoverflow com questions 6516869 i want to make a java program i
  • 写入静态字段 - 在这种情况下 FindBugs 是否错误?

    我有一个像这样的Java类 public class Foo public static int counter 0 public void bar int counter Foo counter counter FindBugs 警告我有
  • Spring - Path 的工厂方法

    我正在尝试生成一个代表的 beanjava nio file Path使用静态方法Paths get String path 我当前的 Spring 设置如下
  • 时间序列中的峰值检测

    我目前正在开展一个小项目 我想在其中比较两个时间序列 相似性度量确实很模糊 如果两个时间序列大致具有相同的形状 则它们被认为是相似的 所以我心想 如果它们只需要具有相同的形状 我只需比较两个时间序列的峰值 如果峰值位于相同的位置 那么时间序
  • Java:如何像 C++ 一样存储和检索内存地址

    我有 C 背景 在 C 中 我可以存储我刚刚在全局数组中新建的内存地址 并在以后重新使用它 例如 假设我有两个类 X Y 并且我创建了两个对象 x y 全局数组 StoreAddresses 2 定义为 uint32 t StoreAddr
  • 使用 enum.values() 与字符串数组相比,性能是否会受到影响?

    我正在使用枚举来替换String我的 java 应用程序 JRE 1 5 中的常量 当我在不断调用的方法中将枚举视为名称的静态数组时 例如 在渲染 UI 时 是否会对性能造成影响 我的代码看起来有点像这样 public String get
  • 无法在 Eclipse IDE 中使用 java 建立与 SQL Server 2008 的数据库连接

    我正在尝试在 Eclipse IDE 中使用 Java 代码连接到 HP Operations Manager 数据库 我能够通过 Microsoft SQL Server Management Studio 2008 成功连接 但通过代码
  • 使用 JaxRS 自定义 JSON 序列化

    在 Web 服务调用中 我想返回具有此 JSON 结构的对象 date 30 06 2014 price val 12 50 curr EUR 我想将此 JSON 代码映射到此 Java 结构 使用乔达时间 http www joda or
  • 如何在 Netbeans 中调试 Java Web 应用程序?

    我曾多次在 Netbeans 中调试 Java 桌面应用程序 但从未调试过 Java Web 应用程序 我尝试以同样的方式调试它 但它不起作用 我制作了一个index html网页 该页面上有一个 表格 用户提交表单后 请求将发送至 ser
  • API 调用 datastore_v3.Put() 的请求太大

    我正在使用 google cloud sql 和 appengine 我正进入 状态com google apphosting api ApiProxy RequestTooLargeException The request to API
  • Grails 3.x bootRun 失败

    我正在尝试在 grails 3 1 11 中运行一个项目 但出现错误 失败 构建失败并出现异常 什么地方出了错 任务 bootRun 执行失败 进程 命令 C Program Files Java jdk1 8 0 111 bin java
  • Spring Batch 多线程 - 如何使每个线程读取唯一的记录?

    这个问题在很多论坛上都被问过很多次了 但我没有看到适合我的答案 我正在尝试在我的 Spring Batch 实现中实现多线程步骤 有一个包含 100k 条记录的临时表 想要在 10 个线程中处理它 每个线程的提交间隔为 300 因此在任何时
  • Play框架运行应用程序问题

    每当我尝试运行使用以下命令创建的新 Web 应用程序时 我都会收到以下错误Play http www playframework org Error occurred during initialization of VM Could no
  • 在 java 类和 android 活动之间传输时音频不清晰

    我有一个android活动 它连接到一个java类并以套接字的形式向它发送数据包 该类接收声音数据包并将它们扔到 PC 扬声器 该代码运行良好 但在 PC 扬声器中播放声音时会出现持续的抖动 中断 安卓活动 public class Sen
  • 给定两个 SSH2 密钥,我如何检查它们是否属于 Java 中的同一密钥对?

    我正在尝试找到一种方法来验证两个 SSH2 密钥 一个私有密钥和一个公共密钥 是否属于同一密钥对 我用过JSch http www jcraft com jsch 用于加载和解析私钥 更新 可以显示如何从私钥 SSH2 RSA 重新生成公钥
  • JAXb、Hibernate 和 beans

    目前我正在开发一个使用 Spring Web 服务 hibernate 和 JAXb 的项目 1 我已经使用IDE hibernate代码生成 生成了hibernate bean 2 另外 我已经使用maven编译器生成了jaxb bean
  • 控制Android的前置LED灯

    我试图在用户按下某个按钮时在前面的 LED 上实现 1 秒红色闪烁 但我很难找到有关如何访问和使用前置 LED 的文档 教程甚至代码示例 我的意思是位于 自拍 相机和触摸屏附近的 LED 我已经看到了使用手电筒和相机类 已弃用 的示例 但我

随机推荐

  • CUDA10.0 官方手册 章三 CUDA编程接口

    因为这章内容比较碎 不好提炼 大部分为原文翻译 人工翻译 不是机器翻译 抵制不负责任的机翻从你我做起 翻译不易 转载贴上出处 630056108 qq com 目录 3 1 用NVCC编译 3 1 1 编译工作流 3 1 2 二进制兼容性
  • IPv6表示方法与配置案例

    1 IPv6地址格式 IPv4地址长度32位 IPv6的地址长度为128位 是IPv4地址长度的4倍 采用十六进制表示 表示方式如下 冒号分隔十六进制表示法 格式为X X X X X X X X 其中每个X表示地址中的16个二进制B 十六进
  • Tkinter批量截取log

    http automap bj bcebos com mapautozip 5 3 0 20201020 repacked path for 10x adas BaiduNavi china zip usr bin python codin
  • react获取经纬度

    useEffect gt if navigator geolocation navigator geolocation getCurrentPosition position gt const latitude position coord
  • Kubernetes学习之路(一)之概念和架构解析和证书创建和分发

    1 Kubernetes的重要概念 转自 CloudMan老师公众号 每天5分钟玩转Kubernetes https item jd com 26225745440 html Cluster Cluster 是计算 存储和网络资源的集合 K
  • C++Builder下利用TImage制作二维条码PDF417打印控件(五)

    接上文 字节压缩6 void TPDF417 byteCompaction6 pPdf417class p int start int length 6 char text p gt param gt text int ret p gt p
  • el-date-picker日期选择器

    先看效果 给绑定事件change或blur事件具体看需求
  • 计算机网络——主机IP地址、子网掩码、广播地址、网络数、主机数计算方法

    目录 一 概念 1 1 主机IP地址 1 2 子网掩码 1 3 广播地址 1 4 子网划分 二 计算 2 1 已知IP地址和子网掩码 计算网络地址和主机地址 2 2 已知IP地址和子网掩码 计算广播地址 2 3 已知子网掩码 计算主机数 2
  • DCMTK读取dcm图像

    医学图像处理中的图像格式通常为dcm格式 在C 中读取使用可以通过DCMTK库中的接口函数来实现 通常文件的读取步骤如下 使用DcmFileFormat类构造最基本的文件对象 使用loadFile 函数读取文件 并通过OFCondition
  • Python 思维锻炼

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 程序1 数字组合 程序描述 有四个数字 1 2 3 4 能组成多少个互不相同且无重复数字的三位数 各是多少 程序分析 可填在百位 十位 个位的数字都是1 2 3 4 组成所
  • uniapp 实现下载pdf格式文件

    processContractUrl params then res gt 调后端接口 返给url console log res uni downloadFile url res data url 调接口返给的url success fu
  • Vivado-FIFO Generator

    很全很详细的FIFO Generator IP核的使用规则 FIFO Generator IP核的使用 1 概述 1 最大支持500M 2 支持三种接口 Native interface FIFOs AXI Memory Mapped in
  • ansible lookup例子

    lookup 读取csv 通过lookup方式 将用户密码把独立到credentials csv文件中 credentials csv Credentials File Hostname Password web server Passw0
  • shell脚本输入密码

    平时在控制台输入指令如 sudo ssh ftp或者修改admin权限的文件时候都会要求输入password 但是在she ll脚本运行过程中该如何交互实现自动输入密码呢 下面总结三种实现方法 一 重定向 用重定向方法实现交互的前提是指令需
  • Linux 基础语法 -2

    如果我们以后再Linux当中 写了一些命名 导致程序我们不能进行操作了 如这个死循环 他就会一直输出 hello Linux 我们就使用 ctrl c 来终止因为程序或者指令异常 而导致我们无法进行指令输入 通配符 它的意思是所有 比如我们
  • python合并音频Couldn‘t find ffprobe or avprobe解决办法

    1 cmd指令pip install pydub 安装pydub库 当然 还需要ffmpeg库 pip install ffmpeg安装或者下载ffmpeg https ffmpeg zeranoe com builds 解压安装后将路径复
  • 解析C++中不能重载为友元函数的四个运算符

    C 规定有四个运算符 gt 不可以是全局域中的重载 即不能重载为友员函数 这是为什么呢 现在先说说赋值运算符 的重载 C 规定赋值运算符 只能重载为类的非静态成员函数 而不可以重载为类的友元函数 不能重载为类的静态成员应该比较容易理解 因为
  • @SpyBean 和 @MockBean 区别,以及@Spy 和 @Mock的区别

    2019独角兽企业重金招聘Python工程师标准 gt gt gt spy对象和mock对象的两点区别 Spy 和 Mock的 两点 区别 SpyBean 和 MockBean 的两点区别 1 默认行为的不同 对于未指定mock的方法 sp
  • centos7 安装/卸载 任意版本的mariadb(mysql)

    mysql官网的链接 https downloads mysql com archives community 安装 mariadb是mysql的一个开源分支 安装mariadb后使用的命令依旧是mysql的 因此这里博主使用centos7
  • 彻底解决SLF4J的日志冲突的问题

    今天公司同事上线时发现 有的机器打印了日志 而有的机器则一条日志也没有打 以往都是没有问题的 因此猜测是这次开发间接引入新的日志jar包 日志冲突导致未打印 排查代码发现 系统使用的是SLF4J框架打印log4j2的日志 查看系统中引入的j