徹底解決SLF4J的日志沖突的問題


今天公司同事上線時發現,有的機器打印了日志,而有的機器則一條日志也沒有打。以往都是沒有問題的。

因此猜測是這次開發間接引入新的日志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------




如果這個辦法對您有用,或者您希望持續關注,也可以掃描下方二維碼或者在微信公眾號中搜索【碼路無涯】



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM