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

因此猜测是这次开发间接引入新的日志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------

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

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

  1. 注解slf4j的日志该用哪个级别

    slf4j的日志级别分为五种 info.debug.error.warn.trane 常用的是这是三个.         info  一般处理业务逻辑的时候使用,就跟 system.err打印一样,用 ...

  2. 关于log4j、jul、jcl、slf4j等等日志组件的理解

    日志组件: 我们经常在开发项目的时候,需要打印记录项目过程中的一些日志.那我们经常大概会用到 log4j.jul.jcl.slf4j.simple.nop.logback 等等,那我们就详细介绍下这些 ...

  3. 怎样在自己的 Web 中加入强大的日志系统系统?slf4j 的日志插件必须要知道!

    对于程序猿来讲,一个应用程序的日志管理是极为重要的.因为,它可以帮助我们随时查看应用程序的运行状态.执行效果等信息,从而监控软件系统.或是根据日志信息解决一些重要的问题. 但是在 Java 应用程序中 ...

  4. 使用 logback + slf4j 进行日志记录

    此处主要介绍maven web工程下如何使用 logback + slf4j  进行日志记录. logback主要包含三个组成部分:Loggers(日志记录器).Appenders(输出目的在).La ...

  5. 使用svn 的解决 处理svn状态冲突

    当直接只用版本浏览器进行svn的删除操作是,在客户端svn目录里,出现svn版本错误信息提示,使用 svn 解决命令,处理冲突保存 svn resolve --accept=working PATH( ...

  6. eclipse中解决git分支合并冲突

    冲突场景: 在master分支上有文件student.py. 在master上增新一个dev分支 在dev分支上修改文件student.py.增加函数def d():,并commit; 在master ...

  7. 解决logging模块日志信息重复问题

    解决logging模块日志信息重复问题 问题描述 相信大家都知道python的logging模块记录日志信息的步骤: # coding:utf-8 import logging ### 创建logge ...

  8. JAVAEE——SpringBoot日志篇:日志框架SLF4j、日志配置、日志使用、切换日志框架

    Spring Boot 日志篇 1.日志框架(故事引入) 小张:开发一个大型系统: ​ 1.System.out.println(""):将关键数据打印在控制台:去掉?写在一个文件 ...

  9. spring boot使用slf4j输出日志

    spring boot使用slf4j输出日志 https://blog.csdn.net/qq442270636/article/details/79406346 Spring Boot SLF4J日 ...

随机推荐

  1. 第三方api接口

    做为一个软件测试工程师,你要学习接口测试,需要练习,那么就要有调用的api,可以参考以下的文章. 国内7款API供应平台功能对比及详细介绍 https://blog.csdn.net/ishxiao/ ...

  2. genymotion从本地拖拽apk到模拟器失败,报错“An error occured while deploying the file……”-解决方案

    前两篇已经讲过genymotion的安装了,但genymotion构建的安卓模拟器的界面比较简洁,什么软件都没.那么我们进行测试之前,先将需要测试的apk安装到模拟器中,一般来说,直接将apk文件从本 ...

  3. AngularJS的简单实用

    Angular Js 的初步认识和使用    一:        1.模块化            定义模块和控制器  ng-app="myapp"  controller=&qu ...

  4. kubelet源码分析——kubelet简介与启动

    kubelet是k8s集群中一个组件,其作为一个agent的角色分布在各个节点上,无论是master还是worker,功能繁多,逻辑复杂.主要功能有 节点状态同步:kublet给api-server同 ...

  5. C# 显示、隐藏窗口对应的任务栏

    WPF中全屏窗口,会自动隐藏任务栏. 那非全屏窗口如何隐藏任务栏?甚至有没有一种场景,隐藏任务后自定义一套系统任务栏来显示? 以下会分阶段讲述一些概念 1. 主屏任务栏 任务栏,其实也是一个窗口,主屏 ...

  6. 踩坑系列《三》 java -version 报错出现Error occurred during initialization of VM java/lang/NoClassDefFoundError:

    之前导入一个项目,因为该项目Spring版本过低,只能适用于1.7版本,装了jdk1.7版本.安装配置好后,打开cmd窗口,执行 java -version 指令发现没像正常一样显示版本信息,而是报了 ...

  7. Java 爬取 51job 数据 WebMagic实现

    Java 爬取 51job 数据 一.项目Maven环境配置 相关依赖 jar 包配置 <parent> <groupId>org.springframework.boot&l ...

  8. SPA测试

    1.生产端:环境准备为了进行SPA测试,在生产数据库中创建了SPA测试专用用户,避免与其他用户相互混淆与可能产生的误操作. CREATE USER SPA IDENTIFIED BY SPA DEFA ...

  9. Python - __all__ 变量

    import * 当我们向文件导入某个模块时,导入的是该模块中那些名称不以下划线(单下划线 _ 或者双下划线 __ )开头的变量.函数和类 因此,如果不想模块文件中的某个对象被引入到其它文件中使用,可 ...

  10. 极简SpringBoot指南-Chapter03-基于SpringBoot的Web服务

    仓库地址 w4ngzhen/springboot-simple-guide: This is a project that guides SpringBoot users to get started ...