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

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

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

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


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

彻底解决SLF4J的日志冲突的问题_第1张图片

一、起因

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

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

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

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

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

彻底解决SLF4J的日志冲突的问题_第2张图片

彻底解决SLF4J的日志冲突的问题_第3张图片

在这里插入图片描述

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

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

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,就不会打入包中。
从而实现需要的包以我们声明的为准,需要排掉的包也不会被间接依赖影响


  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------




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


你可能感兴趣的:(彻底解决SLF4J的日志冲突的问题)