本文目录导读:

- 前置知识:核心概念
- 案例一:项目用 SLF4J + Logback,但引用了老库(使用 Log4j 1.x)
- 案例二:项目用 SLF4J + Logback,但代码或依赖用了 JCL (Apache Commons Logging)
- 案例三:项目用 SLF4J + Logback,但代码直接调用了 java.util.logging (JUL)
- 案例四:反向桥接(项目用 Log4j2,但代码硬编码了 SLF4J)
- 案例五:最全综合排除法(检查“多余”的桥接包)
- 总结对照表
在Java生态中,日志系统的混乱常常源于日志门面(Facade)与日志实现(Implementation)的冲突,以及多个日志框架共存导致的输出丢失或重复。
以下通过几个典型场景的案例,展示如何通过桥接解决这些问题。
前置知识:核心概念
- 日志门面:
SLF4J、Apache Commons Logging (JCL)—— 只提供API,不输出日志。 - 日志实现:
Logback、Log4j2、java.util.logging (JUL)—— 真正输出日志的框架。 - 桥接包:用来重定向/欺骗某个框架,让它的调用走另一个框架的通道。
项目用 SLF4J + Logback,但引用了老库(使用 Log4j 1.x)
问题:你的项目用 SLF4J + Logback 输出日志,但第三方依赖(如老版 Apache HttpClient)内部调用的是 Log4j 1.x API,由于项目没有配置 log4j.properties,该库的日志会全部丢失。
解决方案:使用 log4j-over-slf4j 桥接包(Log4j 1.x → SLF4J)。
操作步骤:
- 移除原有的
log4j:log4j:1.2.x依赖(防止类冲突)。 - 添加桥接包:
<dependency> <groupId>org.slf4j</groupId> <artifactId>log4j-over-slf4j</artifactId> <version>1.7.36</version> <!-- 版本与SLF4J一致 --> </dependency> - 确保
slf4j-api和logback-classic在 classpath 中。
原理:
桥接包内有一个 org.apache.log4j.Logger 类(完全替换了原Log4j的同名类),其内部实现调用了 SLF4J 的 API,第三方库以为自己在用Log4j,实际日志输出到了Logback。
项目用 SLF4J + Logback,但代码或依赖用了 JCL (Apache Commons Logging)
问题:Spring Framework 旧版本默认使用 JCL 输出日志,如果不桥接,Spring的日志无法打印,或者输出到 System.out 而不是Logback。
解决方案:使用 jcl-over-slf4j 桥接包(JCL → SLF4J)。
操作步骤:
- 排除项目中传递过来的
commons-logging:commons-logging依赖。 - 添加桥接包:
<dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.36</version> </dependency> - 若使用 Maven,在 Spring 依赖中排除原生JCL:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <exclusions> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions> </dependency>
验证: Spring 的日志条目会出现在 Logback 配置的 appender 中,且格式统一。
项目用 SLF4J + Logback,但代码直接调用了 java.util.logging (JUL)
问题:JDK自带的 JUL 可能是某个库(如某些金融SDK)的日志实现,JUL 默认配置只输出 WARNING 级别到控制台,且格式与 Logback 不一致。
解决方案:使用 jul-to-slf4j 桥接包(JUL → SLF4J)。
操作步骤:
-
添加桥接包:
<dependency> <groupId>org.slf4j</groupId> <artifactId>jul-to-slf4j</artifactId> <version>1.7.36</version> </dependency> -
在应用启动时(main方法或ServletContextListener),安装桥接器:
import org.slf4j.bridge.SLF4JBridgeHandler; public class Main { public static void main(String[] args) { // 移除JUL默认的根处理器,避免重复输出 SLF4JBridgeHandler.removeHandlersForRootLogger(); // 安装桥接处理器 SLF4JBridgeHandler.install(); // 启动你的应用... } }
注意:如果不禁用JUL默认处理器,日志会重复输出(一条来自JUL,一条来自Logback桥接)。
反向桥接(项目用 Log4j2,但代码硬编码了 SLF4J)
问题:你的团队实际使用 Log4j2 作为实现(因为异步性能好),但内部框架直接依赖了 SLF4J 接口。
解决方案:使用 log4j-slf4j-impl 适配层(SLF4J → Log4j2)。
操作步骤:
- 添加 Log4j2 和适配器,不要添加 Logback:
<!-- 核心:Log4j2 API + Core --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.20.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.20.0</version> </dependency> <!-- 桥接:让 SLF4J 调用 Log4j2 --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.20.0</version> </dependency> - 配置
log4j2.xml文件,而不是logback.xml。
最全综合排除法(检查“多余”的桥接包)
致命错误:同时引入 log4j-over-slf4j 和 slf4j-log4j12,会导致无限递归(栈溢出异常)。
排查建议:使用 Maven 插件检查依赖树。
mvn dependency:tree -Dincludes=org.slf4j,ch.qos.logback,log4j
检查点:
- 是否出现两个
slf4j绑定(Binding)?- 如果出现
slf4j-log4j12和logback-classic,只能留一个。
- 如果出现
- 是否出现两个
log4j实现?- 如果出现
log4j原始包和log4j-over-slf4j,必须移除原始包。
- 如果出现
总结对照表
| 项目目标实现 | 场景中调用的API | 需要添加的桥接包 | 需要移除的包 |
|---|---|---|---|
| Logback | Log4j 1.x | log4j-over-slf4j |
原始 log4j:log4j |
| Logback | JCL | jcl-over-slf4j |
commons-logging |
| Logback | JUL | jul-to-slf4j + 代码安装 |
无需移除(需禁用JUL默认处理器) |
| Log4j2 | SLF4J | log4j-slf4j-impl |
logback-classic |
| Log4j2 | JCL | log4j-jcl |
commons-logging |
| Log4j2 | Log4j 1.x | log4j-1.2-api |
原始 log4j:log4j |
核心口诀:
- 想让谁输出,就把谁放在classpath最深处(作为实现)。
- 桥接包的本质是 “假装自己是那个库的API,但内部转发给SLF4J”。
- 确保 classpath 中只能有一个日志实现绑定到 SLF4J。