log4j-over-slf4j与slf4j-log4j12共存stack overflow异常分析

本文深入分析了SLF4J与Log4J在桥接过程中可能导致的堆栈溢出异常现象,阐述了Java日志系统的整体架构,并提供了具体的代码示例与调用序列图。

注:下文中的“桥接”、“转调”、“绑定”等词基本都是同一个概念。

log4j-over-slf4j和slf4j-log4j12是跟java日志系统相关的两个jar包,当它们同时出现在classpath下时,就可能会引起堆栈溢出异常。异常信息大致如下(摘自slf4j官网文档Detected both log4j-over-slf4j.jar AND slf4j-log4j12.jar on the class path, preempting StackOverflowError ):

Exception in thread "main" java.lang.StackOverflowError
  at java.util.Hashtable.containsKey(Hashtable.java:306)
  at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:36)
  at org.apache.log4j.LogManager.getLogger(LogManager.java:39)
  at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:73)
  at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:249)
  at org.apache.log4j.Category.<init>(Category.java:53)
  at org.apache.log4j.Logger..<init>(Logger.java:35)
  at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:39)
  at org.apache.log4j.LogManager.getLogger(LogManager.java:39)
  at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:73)
  at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:249)
  at org.apache.log4j.Category..<init>(Category.java:53)
  at org.apache.log4j.Logger..<init>(Logger.java:35)
  at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:39)
  at org.apache.log4j.LogManager.getLogger(LogManager.java:39)
  subsequent lines omitted...

现有日志体系

分析这个异常出现的具体原因之前,有必要先快速了解一下现有的Java日志体系。下图是现有Java日志体系的一个示意:

上图不是非常精准,但是能够比较清晰地展示现有Java日志体系的主体架构。Java日志体系大体可以分为三个部分:日志门面接口、桥接器、日志框架具体实现。

Java日志框架有很多种,最简单的是Java自带的java.util.logging,而最经典的是log4j,后来又出现了一个比log4j性能更好的logback,其他的日志框架就不怎么常用了。应用程序直接使用这些具体日志框架的API来满足日志输出需求当然是可以的,但是由于各个日志框架之间的API通常是不兼容的,这样做就使得应用程序丧失了更换日志框架的灵活性。

比直接使用具体日志框架API更合理的选择是使用日志门面接口。日志门面接口提供了一套独立于具体日志框架实现的API,应用程序通过使用这些独立的API就能够实现与具体日志框架的解耦,这跟JDBC是类似的。最早的日志门面接口是commons-logging,但目前最受欢迎的是slf4j。

日志门面接口本身通常并没有实际的日志输出能力,它底层还是需要去调用具体的日志框架API的,也就是实际上它需要跟具体的日志框架结合使用。由于具体日志框架比较多,而且互相也大都不兼容,日志门面接口要想实现与任意日志框架结合可能需要对应的桥接器,就好像JDBC与各种不同的数据库之间的结合需要对应的JDBC驱动一样。

需要注意的是,前面说过,上图并不精准,这只是主要部分,实际情况并不总是简单的“日志门面接口-->桥接器-->日志框架”这一条单向线。实际上,独立的桥接器有时候是不需要的,而且也并不是只有将日志门面API转调到具体日志框架API的桥接器,也存在将日志框架API转调到日志门面API的桥接器。

说白了,所谓“桥接器”,不过就是对某套API的伪实现。这种实现并不是直接去完成API所声明的功能,而是去调用有类似功能的别的API。这样就完成了从“某套API”到“别的API”的转调。如果同时存在A-to-B.jar和B-to-A.jar这两个桥接器,那么可以想象当应用程序开始调用A或者B的API时,会发生什么事。这就是最开始引出的那个stack overflow异常的基本原理。

slf4j的转接绑定

上面只是从整体上大概说了下Java现有日志体系,还看无法详细说明问题所在,需要进一步了解一下slf4j与具体日志框架的桥接情况。

slf4j桥接到具体日志框架

下图来自slf4j官网文档 Binding with a logging framework at deployment time :

可以看到slf4j与具体日志框架结合的方案有很多种。当然,每种方案的最上层(绿色的应用层)都是统一的,它们向下都是直接调用slf4j提供的API(浅蓝色的抽象API层),依赖slf4j-api.jar。然后slf4j API向下再怎么做就非常自由了,几乎可以使用所有的具体日志框架。注意图中的第二层是浅蓝色的,看左下角的图例可知这代表抽象日志API,也就是说它们不是具体实现。如果像左边第一种方案那样下层没有跟任何具体日志框架实现相结合,那么日志是无法输出来的( 这里不确定是否可能会默认输出到标准输出 )。

图中第三层明显就不如第一、二层那么整齐划一了,因为这里已经开始涉及到了具体的日志框架。

首先看第三层中间的两个湖蓝色块,这是适配层,也就是桥接器。左边的slf4j-log4j12.jar桥接器看名字就知道是slf4j到log4j的桥接器,同样,右边的slf4j-jdk14.jar就是slf4j到Java原生日志实现的桥接器了。它们的下一层分别是对应的日志框架实现,log4j的实现代码是log4j.jar,而jul实现代码已经包含在了JVM runtime中,不需要单独的jar包。

再看第三层其余的三个深蓝色块。它们三个也是具体的日志框架实现,但是却不需要桥接器,因为它们本身就已经直接实现了slf4j API。slf4j-simple.jar和slf4j-nop.jar这两个不用多说,看名字就知道一个是slf4j的简单实现,一个是slf4j的空实现,平时用处也不大。而logback之所以也实现了slf4j API,据说是因为logback和slf4j出自同一人之手,这人同时也是log4j的作者。

第三层所有的灰色jar包都带有红框,这表示它们都直接实现了slf4j API,只是湖蓝色的桥接器对slf4j API的实现并不是直接输出日志,而是转去调用别的日志框架的API。

其它日志框架API转调回slf4j

如果只存在上面这些从sfl4j到其他日志框架的桥接器,可能还不会出什么问题。但是实际上还有另外一类桥接器,它们的作用跟上面的恰好相反,它们将其它日志框架的API转调到slf4j的API上。下图来自slf4j官网文档 Bridging legacy APIs :

上图展示了目前为止能安全地从别的日志框架API转调回slf4j的所有三种情形。

以左上角第一种情形为例,当slf4j底层桥接到logback框架的时候,上层允许桥接回slf4j的日志框架API有log4j和jul。jcl虽然不是什么日志框架的具体实现,但是它的API仍然是能够被转调回slf4j的。要想实现转调,方法就是图上列出的用特定的桥接器jar替换掉原有的日志框架jar。需要注意的是这里不包含logback API到slf4j API的转调,因为logback本来就是slf4j API的实现。

看完三种情形以后,会发现几乎所有其他日志框架的API,包括jcl的API,都能够随意的转调回slf4j。 但是有一个唯一的限制就是转调回slf4j的日志框架不能跟slf4j当前桥接到的日志框架相同。 这个限制就是为了防止A-to-B.jar跟B-to-A.jar同时出现在类路径中,从而导致A和B一直不停地互相递归调用,最后堆栈溢出。目前这个限制并不是通过技术保证的,仅仅靠开发者自己保证,这也是为什么slf4j官网上要强调所有合理的方式只有上图的三种情形。

到这里,在开始所展示的那个异常的原理基本已经清楚了。此外,通过上图还可以看出可能会出现类似异常的组合不仅仅是log4j-over-slf4j和slf4j-log4j12,slf4j官网还指出了另外一对: jcl-over-slf4j.jar和slf4j-jcl.jar

代码示例

前面的分析都是理论上的,实际代码中即便同时使用了log4j-over-slf4j和slf4j-log4j12,也未必一定会出现异常。下面的代码调用slf4j的API输出日志,slf4j底层桥接到log4j:

package test;

public class HelloWorld {
  public static void main(String[] args) {
    org.apache.log4j.BasicConfigurator.configure();
    org.slf4j.Logger logger = org.slf4j.LoggerFactory
        .getLogger(HelloWorld.class);
    logger.info("Hello World");
  }
}

配置classpath上的jar包为(注意log4j在log4j-over-slf4j之前):

在这种情况下运行测试程序是能够正常输出日志的,不会出现stack overflow异常。但是如果调整classpath上的jar顺序为:

再运行测试程序就出现类似于本文最开始的stack overflow异常了,可以看到明显的周期性重复:

序列图分析

上图是堆栈溢出的详细调用过程序列图。从调用1开始,依次调用1.1、1.1.1……最后到了1.1.1.1.1.1(图中最后一个调用)的时候,发现它跟1是完全一样的,那么后续的过程就是完全一样的重复了。

需要特别说明的是最开始的导火索并不只有图中所示的LoggerFactory.getLogger()一种,应用程序中能够触发堆栈溢出异常的直接调用还有好几种其它的,比如前面示例代码中触发异常的实际上是第一条语句org.apache.log4j.BasicConfigurator.configure(),但后续的互相无限递归调用过程基本都是跟上图相同的过程。

<think>嗯,用户遇到了Hive Metastore连接MySQL时的两个问题:SLF4J多个绑定和ClassNotFoundException com.mysql.jdbc.Driver。我需要先理清这两个问题的原因,再想解决办法。 首先,SLF4J的多个绑定问题。SLF4J是一个日志门面,它需要具体的日志实现,比如Log4j或者Logback。但如果类路径中有多个SLF4J绑定,比如同时存在slf4j-log4j12slf4j-simple,就会报错。用户提到有slf4j-log4j12-1.7.25.jar包冲突,这可能是因为Hadoop和Hive的库中有重复的绑定。需要检查相关目录,比如Hive的lib、Hadoop的common/lib,找出多余的绑定并删除其中一个。比如,保留Hive使用的版本,删除Hadoop中的旧版本,或者反过来,根据实际情况调整。 然后是ClassNotFoundException,找不到MySQL的驱动类。这通常是因为Hive没有正确加载MySQL的JDBC驱动。用户需要确认mysql-connector-java.jar是否已经放置在Hive的lib目录下,比如/opt/hive/lib/。另外,还要检查驱动包的版本是否兼容当前使用的MySQL版本。有时候,文件名不正确或者位置不对也会导致这个问题。可能需要重新下载驱动,并确保文件名正确,比如mysql-connector-java-8.0.28.jar,然后复制到正确的位置。 接下来,解决步骤应该分两部分。首先处理SLF4J冲突,需要定位冲突的jar包位置,删除多余的。比如用户引用5中提到Hadoop的common/lib下有冲突的包,可以删除那里的slf4j-log4j12.jar,或者替换成Hive兼容的版本。然后处理驱动问题,确保驱动jar在Hive的lib下,并且路径正确。另外,可能需要检查Hive的配置文件hive-site.xml,确认连接MySQL的JDBC URL和驱动类名是否正确,比如是否写成com.mysql.jdbc.Driver或者新的com.mysql.cj.jdbc.Driver,根据驱动版本调整。 用户可能还忽略了环境变量或Hive服务重启的问题。修改配置后,需要重启Hive服务,让配置生效。此外,驱动包是否有读取权限也可能影响加载,需要检查文件权限是否正确。 综合以上,解决方案应该是先解决日志绑定冲突,然后确保驱动正确安装,并验证配置文件和重启服务。同时,可能需要检查各个组件的版本兼容性,避免因版本不匹配导致的其他问题。</think>### 分步解决方案:Hive Metastore连接MySQL的SLF4J多绑定驱动类未找到问题 #### **问题一:SLF4J多绑定冲突** **原因** SLF4J作为日志门面框架,要求项目中仅存在一个具体的日志实现绑定(如`slf4j-log4j12`或`slf4j-simple`)。若Hive、Hadoop或其他组件的依赖路径中存在多个绑定(如引用[5]中提到的`slf4j-log4j12-1.7.25.jar`其他版本冲突),则会触发警告或错误。 **解决步骤** 1. **定位冲突的JAR包** - 检查以下目录中的JAR文件: - Hive的`lib`目录:`/opt/hive/lib/` - Hadoop的公共库目录:`/opt/hadoop/share/hadoop/common/lib/`(如引用[5]中的路径) - Sqoop的`lib`目录(若涉及Sqoop操作):`/opt/sqoop/lib/` - 使用命令搜索重复绑定: ```bash find /opt/hive/lib/ /opt/hadoop/share/hadoop/common/lib/ -name "slf4j-*.jar" ``` 2. **删除多余的绑定** - 保留**一个**Hive兼容的绑定(如`slf4j-log4j12-1.7.30.jar`),删除其他版本: ```bash # 示例:删除Hadoop公共库中的旧版本 rm /opt/hadoop/share/hadoop/common/lib/slf4j-log4j12-1.7.25.jar ``` 3. **验证日志配置** - 确保`log4j.properties`文件存在于Hive配置目录(`/opt/hive/conf/`),且内容完整[^5]。 --- #### **问题二:ClassNotFoundException: com.mysql.jdbc.Driver** **原因** Hive未正确加载MySQL的JDBC驱动,可能原因包括: 1. MySQL驱动文件(`mysql-connector-java.jar`)未放置到Hive的`lib`目录[^3]; 2. 驱动文件版本MySQL服务不兼容; 3. 配置文件`hive-site.xml`中驱动类名拼写错误。 **解决步骤** 1. **下载并部署MySQL驱动** - 从MySQL官网下载对应版本的驱动(如`mysql-connector-java-8.0.28.jar`); - 将驱动文件复制到Hive的`lib`目录: ```bash cp mysql-connector-java-8.0.28.jar /opt/hive/lib/ ``` 2. **检查配置文件`hive-site.xml`** - 确保以下配置正确(注意驱动类名): ```xml <property> <name>javax.jdo.option.ConnectionDriverName</name> <value>com.mysql.jdbc.Driver</value> <!-- MySQL 5.x使用此值 --> <!----> <value>com.mysql.cj.jdbc.Driver</value> <!-- MySQL 8.x使用此值 --> </property> <property> <name>javax.jdo.option.ConnectionURL</name> <value>jdbc:mysql://<mysql-host>:3306/hive_metastore?useSSL=false</value> </property> ``` 3. **验证驱动加载** - 重启Hive Metastore服务,检查日志中是否包含`Driver registered successfully`信息: ```bash hive --service metastore ``` --- #### **附加建议** - **版本兼容性**:MySQL驱动版本需MySQL服务版本匹配(如MySQL 8.x推荐使用`mysql-connector-java-8.x.x.jar`); - **权限检查**:确保Hive用户对驱动文件有读取权限; - **全局部署**:若使用Ambari集群,可通过命令统一配置驱动(参考引用[2])。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值