39

关于Jar加载顺序的问题分析 - 简书

 4 years ago
source link: https://www.jianshu.com/p/dcad5330b06f?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

关于Jar加载顺序的问题分析

0.292019.11.18 00:18:20字数 662阅读 1,140

背景和问题

公司在做技术升级,升级日志后遇到应用部署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)


  1. 系统环境不同导致一个成功,一个失败?Linux版本、JDK版本、Tomcat版本、程序都一样
  2. 根据报错信息,翻代码发现有两个org.slf4j.spi.LocationAwareLogger.class,分别在sl4j-api和activemq-all同时存在,且内部方法略不同,删除activemq-all(已废弃)依赖
  3. 再次部署成功,问题解决。

为什么不同机器,加载的类不同?

猜测: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将依赖一起打包的方式极易出现这类问题!!!

感谢前人栽树:

http://man7.org/linux/man-pages/man3/readdir.3.html

https://blog.csdn.net/peter_cloud/article/details/9240317


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK