如何排查找不到Jar包的问题?

有时候我们会面临明明已经把某个jar加入到了环境里,可以运行的时候还是找不到。 那么我们有没有一种方法,可以直接看到各个类加载器加载了哪些jar,以及把哪些路径加到了classpath里?答案是肯定的,代码如下:

package top.zsmile.jvm.classloader;

import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;

public class JvmClassLoaderPrintPath {
    public static void main(String[] args) {
        // 启动类加载器
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        System.out.println("启动类加载器");
        for (URL url : urls) {
            System.out.println(" ==> " + url.toExternalForm());
        }

        // 扩展类加载器
        printClassLoader("扩展类加载器", JvmClassLoaderPrintPath.class.getClassLoader().getParent());

        // 应用类加载器
        printClassLoader("应用类加载器", JvmClassLoaderPrintPath.class.getClassLoader());
    }


    public static void printClassLoader(String name, ClassLoader CL) {
        if (CL != null) {
            System.out.println(name + " ClassLoader ‐> " + CL.toString());
            printURLForClassLoader(CL);
        } else {
            System.out.println(name + " ClassLoader ‐> null");
        }
    }

    public static void printURLForClassLoader(ClassLoader CL) {
        Object ucp = insightField(CL, "ucp");
        Object path = insightField(ucp, "path");
        ArrayList ps = (ArrayList) path;
        for (Object p : ps) {
            System.out.println(" ==> " + p.toString());
        }
    }

    private static Object insightField(Object obj, String fName) {
        try {
            Field f = null;
            if (obj instanceof URLClassLoader) {
                f = URLClassLoader.class.getDeclaredField(fName);
            } else {
                f = obj.getClass().getDeclaredField(fName);
            }
            f.setAccessible(true);
            return f.get(obj);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

从打印结果,我们可以看到三种类加载器各自默认加载了哪些jar包和包含了哪些 classpath的路径。

如何排查类的方法不一致的问题?

假如我们确定一个jar或者class已经在classpath里了,但是却总是提示 java.lang.NoSuchMethodError ,这是怎么回事呢?很可能是加载了错误的或者重复加载了不同版本的jar包。这时候,用前面的方法就可以先排查一下,加载了具体什么jar,然后是不是不同路径下有重复的class文件,但是版本不一样。

怎么看到加载了哪些类,以及加载顺序?

还是针对上一个问题,假如有两个地方有Hello.class,一个是新版本,一个是旧的, 怎么才能直观地看到他们的加载顺序呢?也没有问题,我们可以直接打印加载的类清 单和加载顺序。

只需要在类的启动命令行参数加上 ‐XX:+TraceClassLoading 或者 ‐verbose 即 可,注意需要加载java命令之后,要执行的类名之前,不然不起作用。例如:

$ java ‐XX:+TraceClassLoading jvm.HelloClassLoader

怎么调整或修改ext和本地加载路径?

从前面的例子我们可以看到,假如什么都不设置,直接执行java命令,默认也会加载 非常多的jar包,怎么可以自定义加载哪些jar包呢?比如我的代码很简单,只加载rt.jar 行不行?答案是肯定的。

java ‐Dsun.boot.class.path="D:\Program Files\Java\jre1.8.0_231\lib\rt.jar"

其中命令行参数 ‐Dsun.boot.class.path 表示我们要指定启动类加载器加载什么, 最基础的东西都在rt.jar这个包了里,所以一般配置它就够了。 参数 ‐Djava.ext.dirs 表示扩展类加载器要加载什么,一般情况下不需要的话可以直接配置为空即可。

怎么运行期加载额外的jar包或者class呢?

简单说就是不使用命令行参数的情况下,怎么用代码来运行时改变加载类的路径和方 式。假如说,在 d:/app/jvm 路径下,有我们刚才使用过的Hello.class文件,怎么在 代码里能加载这个Hello类呢?

  • 一个是前面提到的自定义ClassLoader的方式
  • 还有一个就是直接在当前 的应用类加载器里,使用 URLClassLoader类的方法addURL,不过这个方法是protected的,需要反射处理一下,然后又因为程序在启动时并没有显示加载Hello类,所以在添加完了classpath以后,没法直接显式初始化,需要使用Class.forName的方式来拿到已经加载的Hello类(Class.forName("jvm.Hello")默认会初始化并执行静态代码块)
package top.zsmile.jvm.classloader;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class JvmAppClassLoaderAddUrl {
    public static void main(String[] args) {
        String appPath = "file:/d:/app/";
        URLClassLoader urlClassLoader = (URLClassLoader) JvmAppClassLoaderAddUrl.class.getClassLoader();
        try {
            Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);

            addURL.setAccessible(true);
            URL url = new URL(appPath);
            addURL.invoke(urlClassLoader, url);
            Class.forName("top.zsmile.jvm.classloader.Hello"); // 效果跟Class.forName("jvm.Hello").newInstance()一样
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}