如何让父加载器调用子加载器

我们的影子在花园石径上徘徊,比那些活着的人更具生气。
- 双亲委派很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载)。但是如果基础类需要回调用户代码,该怎么办?比如有两个类。《深入理解JVM》一书中给出如下说明。

为了解决这个问题,JAVA设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置。有了线程上下文类加载器,就可以做一些舞弊的事情了,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器去完成类加载的动作。
Thread.currentThread().getContextClassLoader()源码注释为:此方法返回Thread的上下文ClassLoader。它由线程的创建者提供,一遍在加载类和资源时在此线程中运行的代码使用。它默认为父线程的Context ClassLoader. 原始线程的Context ClassLoader通常设置为用于加载应用程序的类加载器。如果存在安全管理器,并且调用者的类加载器不为空,并且与上下文类加载器的祖先不一样,则该方法使用RuntimePermission(“getClassLoader”)权限调用安全管理器的checkPermission方法来验证 允许上下文类加载器的检索。
  • 一个例子
    public class Animal { static{ System.out.println("Animal is loaded"); } static int b =Cat.a; }public class Cat extends Animal{ public static int a = 1; static{System.out.println("Cat is loaded"); } }AnimalLoad.java private String filePath = "../Animalpath/"; protected Class findClass(String name) throws ClassNotFoundException { System.out.println("AnimalLoad load " + name); Class result = null; if(name.contains("Animal")){ String fp = filePath + name + ".class"; try { byte[] bytes = getClassBytes(getClassFile(fp)); result = this.defineClass(name, bytes, 0, bytes.length); } catch (Exception e) { e.printStackTrace(); } }if(result == null){ result = Thread.currentThread().getContextClassLoader().loadClass(name); } return result; }CatLoad.java private String filePath = "../CatPath/"; protected Class findClass(String name) throws ClassNotFoundException { System.out.println("CatLoad load " + name); Class result = null; if (name.contains("Cat")) { String fp = filePath + name + ".class"; try { byte[] bytes = getClassBytes(getClassFile(fp)); result = this.defineClass(name, bytes, 0, bytes.length); } catch (Exception e) { e.printStackTrace(); } } return result; }protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class c = findLoadedClass(name); if (c == null) { //先让子加载器加载所需要的类,否则会形成死循环 c = findClass(name); if (c == null && getParent() != null) {c = getParent().loadClass(name); } } if (resolve) {resolveClass(c); } return c; } }Test:AnimalLoad animalLoad = new AnimalLoad(); CatLoad catLoad = new CatLoad(animalLoad); Thread.currentThread().setContextClassLoader(catLoad); catLoad.loadClass("Cat").newInstance(); 输出:CatLoad load Cat CatLoad load Animal AnimalLoad load Animal Animal is loaded AnimalLoad load Cat CatLoad load java.lang.System CatLoad load java.io.PrintStream Cat is loaded

  • 为什么不让AnimalLoad直接new一个CatLoad呢,因为这是两个不同的加载器,加载出的来的类不是同一个类。
    CatLoad catLoad01 = new CatLoad(); CatLoad catLoad02 = new CatLoad();
    System.out.println(catLoad01.loadClass(“Cat”).isAssignableFrom(catLoad02.loadClass(“Cat”)));
    输出:false
  • 如果有类A,类B。A使用了B,B使用了A。A需要放在本地,通过-cp命令指定路径,B需要通过网络动态加载,则如何做呢?
    【如何让父加载器调用子加载器】ClassLoader.getSystemClassLoader()是一个AppClassLoader。这一点可以通过System.out.println(ClassLoader.getSystemClassLoader())验证。AppClassLoader是URLClassLoader的子类。Bootstrapper通过反射调用URLClassLoader的addURL方法,把B的Class路径添加进去,则系统类加载器可以直接加载A&B。
    public class Bootstrapper { public static void main(String[] args) throws Exception {if(args.length < 3){ throw new Exception("参数个数异常 :args.length is " + args.length); }URL[] urls=buildURLs(args[0],args[1]); Method method = URLClassLoader.class.getDeclaredMethod("addURL", new Class[]{URL.class}); method.setAccessible(true); for(URL url : urls){ method.invoke(ClassLoader.getSystemClassLoader(), new Object[]{url}); }Class start= Class.forName(args[2]); Method main = start.getMethod("main", String[].class); String[] mainArgs = new String[0]; if(args.length > 3){ mainArgs = Arrays.copyOfRange(args, 3, args.length); } main.invoke(null,(Object)mainArgs); }private static URL[] buildURLs(String path, String jarNames) throws MalformedURLException { path = "http://" + path; String[] loadJars = jarNames.split("; "); URL[] urls = new URL[loadJars.length]; for(int i = 0; i < loadJars.length; ++i){ urls[i] = new URL(path + "/" + loadJars[i]); }return urls; } }

    推荐阅读