美文网首页
【Java高级】类加载器核心技术,从自定义加载外部jar说起

【Java高级】类加载器核心技术,从自定义加载外部jar说起

作者: 大栗几 | 来源:发表于2020-06-30 23:30 被阅读0次

    本文为原创文章,转载请注明出处
    查看[Java]系列内容请点击:https://www.jianshu.com/nb/45938443

    我们先举个例子,假如我们有如下的类:

    package com.codelifeliwan;
    
    public class Test {
    
        public void test() {
            System.out.println("----------------------BEGIN test--------------------");
            System.out.println("this is thread:" + Thread.currentThread().getId() +
                    "\nContextClassLoader:" + Thread.currentThread().getContextClassLoader() +
                    "\nClassLoader:" + this.getClass().getClassLoader());
        }
    
        // 注意这里是静态方法
        public static void staticTest() {
            System.out.println("----------------------BEGIN staticTest--------------------");
            System.out.println("this is thread:" + Thread.currentThread().getId() +
                    "\nContextClassLoader:" + Thread.currentThread().getContextClassLoader() +
                    "\nClassLoader:" + Test.class.getClassLoader());
        }
    }
    
    

    我们将这个类打成jar包test.jar,那么我们如何在另外一个程序里面加载这个jar包并使用其中的程序?

    双亲委托类加载器

    一个比较通用的方法,是自定义一个类加载器来加载这个jar包,这里我们使用URLClassLoader类加载器:

    import java.io.File;
    import java.net.URL;
    import java.net.URLClassLoader;
    
    public class Main {
        // 全局共享的外部类加载器
        private static ClassLoader classLoader = null;
    
        // 初始化类加载器
        synchronized public static void setClassLoader() throws Exception {
            if (classLoader != null) return;
            classLoader = new URLClassLoader(new URL[]{new File("D:\\test.jar").toURI().toURL()});
            Thread.currentThread().setContextClassLoader(classLoader);
        }
    
        // 测试类加载器
        public static void doTest() throws Exception {
            Class clazz = classLoader.loadClass("com.codelifeliwan.Test"); // 使用URLClassLoader类加载器加载jar中的类
            clazz.getMethod("staticTest").invoke(null, null); // 调用静态的staticTest方法
    
            Object obj = clazz.getConstructor(null).newInstance();
            clazz.getMethod("test").invoke(obj, null); // 调用非静态的test方法
    
            System.out.println("=================================================\n");
        }
    
        public static void main(String[] args) throws Exception {
            setClassLoader(); // 初始化类加载器
            doTest(); // 测试类加载器
        }
    }
    

    注意,这里测试类加载器使用的是反射的方式。

    程序输出:

    ----------------------BEGIN staticTest--------------------
    this is thread:1
    ContextClassLoader:java.net.URLClassLoader@b4c966a
    ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
    ----------------------BEGIN test--------------------
    this is thread:1
    ContextClassLoader:java.net.URLClassLoader@b4c966a
    ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
    =================================================
    

    可以看到,我们能够加载对应的jar中的类,并进行实例化和调用等。

    我们主要看输出的信息:

    ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
    

    可以看到,虽然我们用的是自己定义的URLClassLoader类加载器来进行加载外部的程序的,但是这里实际使用的是应用类加载器AppClassLoader(在创建classLoader的时候自动设置这个对象的类加载器为应用类加载器)。我们跟踪进入ClassLoader类的protected Class<?> loadClass(String name, boolean resolve)方法中查看,发现在加载的时候执行了:

    c = parent.loadClass(name, false);
    

    这里,parent就是应用类加载器,加载完的c不为空则直接返回相应的类,对于每一级类加载器,系统会优先使用父级类加载器,只有当父级类加载器加载失败的时候才使用当前的类加载器。这就是著名的类加载的【双亲委托模型】,相关资料请自行查阅。

    线程上下文类加载器

    现在,我们将测试的程序稍微改一下:

    import java.io.File;
    import java.net.URL;
    import java.net.URLClassLoader;
    
    public class Main {
        // 全局共享的外部类加载器
        private static ClassLoader classLoader = null;
    
        // 初始化类加载器
        synchronized public static void setClassLoader() throws Exception {
            if (classLoader != null) return;
            classLoader = new URLClassLoader(new URL[]{new File("D:\\test.jar").toURI().toURL()});
    
            // 注意这里设置了当前线程的上下文类加载器,这个类加载器会遗传给子线程
            Thread.currentThread().setContextClassLoader(classLoader);
        }
    
        // 测试类加载器
        public static void doTest() throws Exception {
            Class clazz = classLoader.loadClass("com.codelifeliwan.Test");
            clazz.getMethod("staticTest").invoke(null, null); // 调用静态的staticTest方法
    
            Object obj = clazz.getConstructor(null).newInstance();
            clazz.getMethod("test").invoke(obj, null); // 调用非静态的test方法
    
            System.out.println("=================================================\n");
        }
    
        public static void main(String[] args) throws Exception {
            new Thread(new MyRunnable()).start();
            Thread.sleep(1000); // 为了避免打印混乱
            new Thread(new MyRunnable()).start();
            Thread.sleep(1000); // 为了避免打印混乱
        }
    
        public static class MyRunnable implements Runnable {
    
            @Override
            public void run() {
                try {
                    setClassLoader();
                    doTest();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    我们先来看输出结果:

    ----------------------BEGIN staticTest--------------------
    this is thread:14
    ContextClassLoader:java.net.URLClassLoader@24912ef7
    ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
    ----------------------BEGIN test--------------------
    this is thread:14
    ContextClassLoader:java.net.URLClassLoader@24912ef7
    ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
    =================================================
    
    ----------------------BEGIN staticTest--------------------
    this is thread:15
    ContextClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
    ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
    ----------------------BEGIN test--------------------
    this is thread:15
    ContextClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
    ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
    =================================================
    

    我们分别开了14和15两个线程,在14线程中设置了线程的上下文类加载器,那么可以看到,两个线程的ContextClassLoader结果是不一样的,线程设置的上下文类加载器可以遗传到子线程中,然后在子线程中可以使用对应的类加载器来加载其他的程序。

    注意:其实,线程上下文类加载器更应该叫做线程下文类加载器,加载器只会遗传给子线程,对父线程和兄弟线程没有影响。上面的输出结果说明了这一点。

    为了说明线程上下文类加载器会遗传到子线程,我们把初始化类加载器的工作放到主线程中:

    import java.io.File;
    import java.net.URL;
    import java.net.URLClassLoader;
    
    public class Main {
        // 全局共享的外部类加载器
        private static ClassLoader classLoader = null;
    
        // 初始化类加载器
        synchronized public static void setClassLoader() throws Exception {
            if (classLoader != null) return;
            classLoader = new URLClassLoader(new URL[]{new File("D:\\test.jar").toURI().toURL()});
    
            // 注意这里设置了当前线程的上下文类加载器,这个类加载器会遗传给子线程
            Thread.currentThread().setContextClassLoader(classLoader);
        }
    
        // 测试类加载器
        public static void doTest() throws Exception {
            Class clazz = classLoader.loadClass("com.codelifeliwan.Test");
            clazz.getMethod("staticTest").invoke(null, null); // 调用静态的staticTest方法
    
            Object obj = clazz.getConstructor(null).newInstance();
            clazz.getMethod("test").invoke(obj, null); // 调用非静态的test方法
    
            System.out.println("=================================================\n");
        }
    
        public static void main(String[] args) throws Exception {
            setClassLoader(); // 初始化类加载器放在主线程
    
            new Thread(new MyRunnable()).start();
            Thread.sleep(1000); // 为了避免打印混乱
            new Thread(new MyRunnable()).start();
            Thread.sleep(1000); // 为了避免打印混乱
        }
    
        public static class MyRunnable implements Runnable {
    
            @Override
            public void run() {
                try {
                    doTest();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    然后会输出结果:

    ----------------------BEGIN staticTest--------------------
    this is thread:14
    ContextClassLoader:java.net.URLClassLoader@b4c966a
    ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
    ----------------------BEGIN test--------------------
    this is thread:14
    ContextClassLoader:java.net.URLClassLoader@b4c966a
    ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
    =================================================
    
    ----------------------BEGIN staticTest--------------------
    this is thread:15
    ContextClassLoader:java.net.URLClassLoader@b4c966a
    ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
    ----------------------BEGIN test--------------------
    this is thread:15
    ContextClassLoader:java.net.URLClassLoader@b4c966a
    ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
    =================================================
    

    JDBC的驱动就是使用线程上下文类加载器,读者可自行查阅资料

    相关文章

      网友评论

          本文标题:【Java高级】类加载器核心技术,从自定义加载外部jar说起

          本文链接:https://www.haomeiwen.com/subject/lyxqqktx.html