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

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

先上一张关系图:SLF4J框架、各种具体日志实现以及相应桥接包的关系图 彻底解决SLF4J的日志冲突的问题
文章图片

一、起因 由于线上系统要接入很多中间件,因此系统中会有各种各样的日志打印形式(例如:log4j2、JCL、logback等等)。
为了能整合所有日志并进行统一打印,最常用的就是SLF4J框架。
SLF4J框架作为门面框架,并没有日志的具体实现。而是通过和其他具体日志实现进行关联转换,并在系统中配置一种日志实现进行打印。
于是就很容易造成jar包引入冲突,导致有多个日志实现。当SLF4J框架选择的日志实现和我们配置的不一致时,就会打印不出日志。
SLF4J框架发现有多个日志实现时,是会打印提示信息的。但由于是标准错误输出,会在控制台(Tomcat的catalina.out)中打印【当业务日志文件中没有日志打印时,可以查看catalina.out是否有提示】
彻底解决SLF4J的日志冲突的问题
文章图片

彻底解决SLF4J的日志冲突的问题
文章图片

彻底解决SLF4J的日志冲突的问题
文章图片



二、为什么只有部分机器打印 因为每个SLF4J的桥接包都有org.slf4j.impl.StaticLoggerBinder
SLF4J则会随机选择一个使用。当选择的跟系统配置的一样时就可以打印日志,否则就打印不出。
彻底解决SLF4J的日志冲突的问题
文章图片


三、快速感知到多种SLF4J桥接包 如上图所示findPossibleStaticLoggerBinderPathSet方法,当有多个日志桥接包时会返回一个Set集合且提示一条信息。
由于这个信息提示并不强烈,不易感知。我们可以根据这一点,使用反射来获取到系统中实际的桥接包数量,并做自定义的提示。

1、实现spring的BeanFactoryPostProcessor,并将其交由spring管理。保证系统启动后,自动进行日志冲突校验
2、使用反射获取LoggerFactory的实例以及findPossibleStaticLoggerBinderPathSet方法的返回结果
3、根据桥接包数量判断是否异常,进行自定义报警
4、根据报警信息,进行排包

/** * 日志jar包冲突校验 */ public class LogJarConflictCheck implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { try { Class loggerFactoryClazz = LoggerFactory.class; Constructor constructor = loggerFactoryClazz.getDeclaredConstructor(); constructor.setAccessible(true); LoggerFactory instance = constructor.newInstance(); Method method = loggerFactoryClazz.getDeclaredMethod("findPossibleStaticLoggerBinderPathSet"); // 强制进入 method.setAccessible(true); Set staticLoggerBinderPathSet = (Set)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 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,就不会打入包中。
从而实现需要的包以我们声明的为准,需要排掉的包也不会被间接依赖影响
【彻底解决SLF4J的日志冲突的问题】1.7.7 1.2.3 1.2.17 2.3 1.2 org.slf4j slf4j-api ${slf4j.version} org.apache.logging.log4j log4j-core ${log4j2.version} org.apache.logging.log4j log4j-api ${log4j2.version} log4j log4j ${log4j.version} provided ch.qos.logback logback-classic ${logback.version} provided commons-logging commons-logging ${jcl.version} provided org.apache.logging.log4j log4j-to-slf4j ${log4j2.version} provided org.slf4j log4j-over-slf4j ${slf4j.version} org.slf4j jcl-over-slf4j ${slf4j.version} org.slf4j jul-to-slf4j ${slf4j.version} org.apache.logging.log4j log4j-slf4j-impl ${log4j2.version} org.slf4j slf4j-log4j12 ${slf4j.version} provided org.slf4j slf4j-jdk14 ${slf4j.version} provided org.slf4j slf4j-jcl ${slf4j.version} provided


总结 第三步的方案:启动时感知系统是否存在日志jar包冲突,冲突后手动排包
第四步的方案:一次声明所需的所有日志jar包配置,无需在担心冲突问题

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


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


    推荐阅读