背景和问题
公司在做技术升级,升级日志后遇到应用部署A节点正常,B节点失败诡异情况,而后一段时间再次遇到同类问题,决定有必要一探究竟。
B节点失败的错误信息:
java.lang.NoSuchMethodError: org.slf4j.spi.LocationAwareLogger.log(Lorg/slf4j/Marker;Ljava/lang/String;ILjava/lang/String;[Ljava/lang/Object;Ljava/lang/Throwable;)V
at org.apache.commons.logging.impl.SLF4JLocationAwareLog.info(SLF4JLocationAwareLog.java:155)
at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:304)
at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:107)
at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4753)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5215)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:752)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:728)
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:734)
at org.apache.catalina.startup.HostConfig.deployDirectory(HostConfig.java:1141)
at org.apache.catalina.startup.HostConfig$DeployDirectory.run(HostConfig.java:1875)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:748)
排查过程
- 系统环境不同导致一个成功,一个失败?Linux版本、JDK版本、Tomcat版本、程序都一样
- 根据报错信息,翻代码发现有两个org.slf4j.spi.LocationAwareLogger.class,分别在sl4j-api和activemq-all同时存在,且内部方法略不同,删除activemq-all(已废弃)依赖
- 再次部署成功,问题解决。
为什么不同机器,加载的类不同?
猜测:A节点正常,是因为先加载了sl4j-api.jar中的LocationAwareLogger.class,而B节点失败,是先加载了activemq-all.jar中的LocationAwareLogger.class,启动时程序找不到对应的方法报错。
Tomcat类加载
应用运行在Tomcat容器,翻看类加载部分的源码。slf4j包Logger类加载器是Tomcat的ParallelWebappClassLoader,由父类WebappClassLoaderBase实现类加载功能。
类加载入口:
/**
* Load the class with the specified name. This method searches for
* classes in the same manner as <code>loadClass(String, boolean)</code>
* with <code>false</code> as the second argument.
*
* @param name The binary name of the class to be loaded
*
* @exception ClassNotFoundException if the class was not found
*/
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
关注首次class文件从jar包中找到的过程。调用StandardRoot.getResourceInternal寻找class,顺序就是循环allResources(格式:List<List<WebResourceSet>>)。
protected final WebResource getResourceInternal(String path,
boolean useClassLoaderResources) {
WebResource result = null;
WebResource virtual = null;
WebResource mainEmpty = null;
for (List<WebResourceSet> list : allResources) {
for (WebResourceSet webResourceSet : list) {
if (!useClassLoaderResources && !webResourceSet.getClassLoaderOnly() ||
useClassLoaderResources && !webResourceSet.getStaticOnly()) {
result = webResourceSet.getResource(path);
if (result.exists()) {
return result;
}
if (virtual == null) {
if (result.isVirtual()) {
virtual = result;
} else if (main.equals(webResourceSet)) {
mainEmpty = result;
}
}
}
}
}
...
}
集合classResources存了WEB-INF/lib目录的Jar资源,在Tomcat启动时调用processWebInfLib()方法初始化。
/**
protected void processWebInfLib() throws LifecycleException {
WebResource[] possibleJars = listResources("/WEB-INF/lib", false);
for (WebResource possibleJar : possibleJars) {
if (possibleJar.isFile() && possibleJar.getName().endsWith(".jar")) {
createWebResourceSet(ResourceSetType.CLASSES_JAR,
"/WEB-INF/classes", possibleJar.getURL(), "/");
}
}
}
最终在DirResourceSet类list(String path)方法,其实调用的是java.io.File类list()方法,list调的是UnixFileSystem的native的list()方法。注释及解释;
There is no guarantee that the name strings in the resulting array will appear in any specific order; they are not, in particular, guaranteed to appear in alphabetical order.
无法保证结果数组中的名称字符串将以任何特定的顺序出现;尤其不能保证它们按字母顺序出现。
OpenJDK
翻开jdk8对应的OpenJDK源码,UnixFileSystem的list方法,调用的是目录操作函数opendir.
JNIEXPORT jobjectArray JNICALL
Java_java_io_UnixFileSystem_list(JNIEnv *env, jobject this,
jobject file)
{
DIR *dir = NULL;
struct dirent64 *ptr;
struct dirent64 *result;
int len, maxlen;
jobjectArray rv, old;
WITH_FIELD_PLATFORM_STRING(env, file, ids.path, path) {
dir = opendir(path);
} END_PLATFORM_STRING(env, path);
if (dir == NULL) return NULL;
ptr = malloc(sizeof(struct dirent64) + (PATH_MAX + 1));
if (ptr == NULL) {
JNU_ThrowOutOfMemoryError(env, "heap allocation failed");
closedir(dir);
return NULL;
}
...
}
Linux操作系统
继续向下查操作系统,opendir返回值定义
struct dirent
{
ino_t d_ino; /* Inode number */
off_t d_off; /* Not an offset; see below */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not supported
by all filesystem types */
char d_name[256]; /* Null-terminated filename */
};
通过http://man7.org/linux关于dirent的排序解释。
The order in which filenames are read by successive calls to readdir() depends on the filesystem implementation; it is unlikely that the names will be sorted in any fashion.
对readdir()的连续调用读取文件名的顺序取决于文件系统实现;不太可能以任何方式对文件名进行排序。
命令ll -f
与opendir函数readdir顺序相同。
文件系统
继续向下查是文件系统的实现,CentOS 6使用的是Ext4,文件顺序与目录文件的大小是否超过一个磁盘块和文件系统计算的Hash值有关。
总结
因Java语言的跨平台特性,在class首次从jar中找到对应的文件时,查找的顺序是文件操作系统实现决定,与inode值无关。那么像active-all.jar将依赖一起打包的方式极易出现这类问题!!!
感谢前人栽树: