今天公司同事上線時發現,有的機器列印了日志,而有的機器則一條日志也沒有打,以往都是沒有問題的,
因此猜測是這次開發間接引入新的日志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 />
/**
* 日志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------
如果這個辦法對您有用,或者您希望持續關注,也可以掃描下方二維碼或者在微信公眾號中搜索【碼路無涯】

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/348188.html
標籤:其他
