一、现象
通过几轮开发、测试和验证后,在上预发环境时,应用突然无法启动,查看 tomcat 报错原因,发现是 类转换失败 ClassCastException
1.1、报错原因
Class path contains multiple SLF4J binding
23-Mar-2024 16:04:25.300 INFO [localhost-startStop-1] org.apache.jasper.servlet.TldScanner.scanJars At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/home/admin/xxx/WEB-INF/lib/slf4j-log4j12-1.6.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/home/admin/xxx/WEB-INF/lib/logback-classic-1.1.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
org.slf4j.impl.Log4jLoggerFactory cannot be cast to ch.qos.logback.classic.LoggerContext
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cn.com.xxx.framework.log.integration.LogbackInitializer#0' defined in class path resource [spring/spring-log-init.xml]: Invocation of init method failed; nested exception is java.lang.ClassCastException: org.slf4j.impl.Log4jLoggerFactory cannot be cast to ch.qos.logback.classic.LoggerContext
...
Caused by: java.lang.ClassCastException: org.slf4j.impl.Log4jLoggerFactory cannot be cast to ch.qos.logback.classic.LoggerContext
# 出问题的加载地方
at ch.qos.logback.ext.spring.LogbackConfigurer.initLogging(LogbackConfigurer.java:72)
at cn.com.xxx.framework.log.integration.LogbackInitializer.init(LogbackInitializer.java:49)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeCustomInitMethod(AbstractAutowireCapableBeanFactory.java:1706)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1645)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1574)
... 26 more
23-Mar-2024 15:59:12.398 SEVERE [localhost-startStop-1] org.apache.catalina.core.StandardContext.startInternal One or more listeners failed to start. Full details will be found in the appropriate container log file
1.2、查看报错代码
public static void initLogging(String location) throws FileNotFoundException, JoranException {
String resolvedLocation = SystemPropertyUtils.resolvePlaceholders(location);
URL url = ResourceUtils.getURL(resolvedLocation);
LoggerContext loggerContext = (LoggerContext)StaticLoggerBinder.getSingleton().getLoggerFactory();
loggerContext.reset();
new ContextInitializer(loggerContext).configureByResource(url);
}
可以看到,通过 StaticLoggerBinder.getSingleton().getLoggerFactory()
获取 logger 上下文这段代码报错了,通过仔细定位,发现了有两个 StaticLoggerBinder
类
更重要的是,他们两兄弟竟然虽然不是同一个 jar 包,但是包路径和名称都一模一样!!!
二、解决办法
-
通过 POM 文件排查包冲突
-
安装 IDEA 的插件
Maven Helper
-
定位到编译 WAR 包的 POM 文件(我们框架定义的在 Deploy 模块中)
-
在搜索框中,输入搜索内容,点击右键可以看到选项框
-
Jump To Source(跳转到源文件处)
-
Exclude(排除掉)
-
例如我点击了 Exclude
,就能看到 pom 文件中,这个依赖就被排除掉了
<dependency>
<groupId>com.nio.ado</groupId>
<artifactId>ado-subscribe-common</artifactId>
<exclusions>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
三、思考
包冲突解决是简单的,通过 maven 插件可以精确找到依赖,然后进行 Exclude,可是在本地开发、测试环境都没有出现的问题,却在预发环境出现了,所以排除了业务逻辑代码的原因,简单考虑了几个因素和原因:
-
jdk 版本
-
tomcat 版本
-
类加载机制
-
第三方 jar 互相依赖
由于 jdk 和 tomcat 这两者没有明显的报错原因,所以先去排查类的加载机制。
四、类加载机制
4.1、Class Loader 分类
首先Java的类加载器可被分为四类
-
启动类加载器Bootstrap ClassLoader
-
用于加载Java的核心类,由底层的 C++ 实现。启动类加载器不属于 Java 类库,无法被 Java 程序直接引用。
-
Bootstrap ClassLoader 的 parent 属性为 null
-
-
标准扩展类加载器 Extension ClassLoader
-
由 sun.misc.Launcher$ExtClassLoader 实现
-
负责加载 JAVA_HOME 下 libext 目录下的或者被 java.ext.dirs 系统变量所指定的路径中的所有类库
-
-
应用类加载器 Application ClassLoader
-
由 sun.misc.Launcher$AppClassLoader 实现
-
负责在 JVM 启动时加载用户类路径上的指定类库
-
-
用户自定义类加载器 User ClassLoader
-
当上述 3 种类加载器不能满足开发需求时,用户可以自定义加载器
-
自定义类加载器时,需要继承 java.lang.ClassLoader 类。如果不想打破双亲委派模型,那么只需要重写 findClass 方法即可;如果想打破双亲委派模型,则需要重写 loadClass 方法
-
Bootstrap
引导类加载器 → Extension
拓展类加载器 → Application
系统类加载器 → User
自定义类加载器
4.2、Class Loader 加载机制
Java 使用的是双亲委派加载机制,通过查看 ClassLoader
类,可以对此有所了解。
类被成功加载后,将被放入到内存中,内存中存放 Class 实例对象。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先,检查 class 是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
// 如果没有被加载
long t0 = System.nanoTime();
try {
if (parent != null) {
// 寻找 parent 加载器
c = parent.loadClass(name, false);
} else {
// 如果父加载器不存在,则委托给启动类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
// 如果仍然无法加载,才会尝试自身加载
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
我们写的 Java 应用代码,一般是通过 App ClassLoader
应用加载器进行加载,它不会自己先去加载它,而是通过 Extension ClassLoader
扩展类加载器进行加载(其中扩展类加载器又会去找 Bootstrap ClassLoader
启动类加载器进行加载),只有父加载器无法加载情况下,才会让下级加载器进行加载。
4.3、类加载顺序
从代码中了解到,如果某个名字的类被加载后,类加载器是不会再重新加载,所以我们的问题根本原因可以是出现在:
先加载了 org.slf4j
包的 org.slf4j.impl.StaticLoggerBinder
,同名的 ch.qos.logback
包下的 StaticLoggerBinder
类没有被加载
通过查阅文章:
-
跟JAR文件的文件名有关。按照字母的顺序加载JAR文件。有了这个类以后,后面的类则不会加载了。 jvm 加载包名和类名相同的类时,先加载classpath中jar路径放在前面的,包名类名都相同,那jvm没法区分了,如果使用ide一般情况下是会提示发生冲突而报错,若不报错,只有第一个包被引入(在classpath路径下排在前面的包),第二个包会在classloader加载类时判断重复而忽略。
-
而这个顺序实际上是由文件系统决定的,linux内部是用inode来指示文件的。 这种储存文件元信息的区域就叫做inode,中文译名为”索引节点”。每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。
-
Unix/linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。
五、总结
5.1、冲突提示信息
-
java.lang.ClassNotFoundException :类型转换错误,这个报错跟我这次遇到的一样,本应该引入的是
logback
包的类,但是实际引入的是slf4j
下的同名类,导致类型转换错误 -
java.lang.NoSuchMethodError :找不到特定方法,如果有两个同名的包但是不同版本,例如 xxx-1.1和 xxx-1.2包同时存在,先加载了 1.1 版本的类,但是 1.2 版本中才提供了新方法,导致提示找不到特定方法
-
java.lang.NoClassDefFoundError,java.lang.LinkageError
5.2、排查思路
1、查看 catalina.sh
堆栈信息,找到有问题的类
2、通过 IDEA ,在打包的 POM 文件中,使用 Maven Helper
插件找出冲突的依赖,确定项目需要的 jar 包,Exclude
掉不需要的依赖。
5.3、提前预防
1、使用工具检查依赖冲突
冲突检测插件 :maven-enforcer-plugin
引用新的第三方依赖(工具包或者框架包),通过 Maven 插件检查一下 conflict 依赖,提前进行 Exclude
2、统一服务器版本
在测试阶段,准备好和生产环境一样的服务器,提前进行测试,避免依赖冲突的 WAR
包上传到生产环境,例如我们有一台 UAT 服务器,与生产环境一样配置,提前测试,暴露风险和解决问题。